Skip to content

Commit

Permalink
Merge branch 'feature/transcription-in-post-data' into 'master'
Browse files Browse the repository at this point in the history
render voice messages as a postType using new transcription field in post meta

See merge request kchat/webapp!956
  • Loading branch information
antonbuks committed Oct 28, 2024
2 parents 5b4f2e8 + 8b20e1f commit 118270a
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import VoiceMessageRecordingStarted from 'components/advanced_text_editor/voice_
import VoiceMessageUploadingFailed from 'components/advanced_text_editor/voice_message_attachment/components/upload_failed';
import VoiceMessageUploadingStarted from 'components/advanced_text_editor/voice_message_attachment/components/upload_started';
import type {FilePreviewInfo} from 'components/file_preview/file_preview';
import VoiceMessageAttachmentPlayer from 'components/voice_message_attachment_player';
import VoiceMessageAttachmentPlayer from 'components/voice/post_type';

import {AudioFileExtensions, Locations} from 'utils/constants';
import {VoiceMessageStates} from 'utils/post_utils';
Expand Down Expand Up @@ -185,7 +185,6 @@ const VoiceMessageAttachment = (props: Props) => {
<div className='file-preview__container'>
<VoiceMessageAttachmentPlayer
fileId={src}
onCancel={handleRemoveAfterUpload}
isPreview={true}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import FileAttachment from 'components/file_attachment';
import FilePreviewModal from 'components/file_preview_modal';
import SingleImageView from 'components/single_image_view';
import VoiceMessageAttachmentPlayer from 'components/voice_message_attachment_player';

import Constants, {FileTypes, ModalIdentifiers} from 'utils/constants';
import {getFileType} from 'utils/utils';
Expand Down Expand Up @@ -42,13 +41,7 @@ export default function FileAttachmentList(props: Props) {

const sortedFileInfos = useMemo(() => sortFileInfos(fileInfos ? [...fileInfos] : [], locale), [fileInfos, locale]);
if (post.type === Constants.PostTypes.VOICE && fileInfos.length === 1) {
return (
<VoiceMessageAttachmentPlayer
postId={post.id}
fileId={fileInfos?.[0]?.id ?? ''}
inPost={true}
/>
);
return null;
} else if (fileInfos && fileInfos.length === 1 && !fileInfos[0].archived) {
const fileType = getFileType(fileInfos[0].extension);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import CombinedSystemMessage from 'components/post_view/combined_system_message'
import GMConversionMessage from 'components/post_view/gm_conversion_message/gm_conversion_message';
import PostAddChannelMember from 'components/post_view/post_add_channel_member';
import PostNotifyChannelMember from 'components/post_view/post_notify_channel_member/post_notify_channel_member';
import VoiceMessageAttachmentPlayer from 'components/voice/post_type';

import {t} from 'utils/i18n';
import type {TextFormattingOptions} from 'utils/text_formatting';
Expand Down Expand Up @@ -371,6 +372,12 @@ function renderCallNotificationMessage(post: Post): ReactNode {
);
}

function renderVoiceMessage(post: Post): ReactNode {
return (
<VoiceMessageAttachmentPlayer post={post}/>
);
}

function renderMeMessage(post: Post): ReactNode {
// Trim off the leading and trailing asterisk added to /me messages
const message = post.message.replace(/^\*|\*$/g, '');
Expand Down Expand Up @@ -411,6 +418,7 @@ const systemMessageRenderers = {
[Posts.POST_TYPES.CHANNEL_UNARCHIVED]: renderChannelUnarchivedMessage,
[Posts.POST_TYPES.ME]: renderMeMessage,
[Posts.POST_TYPES.CALL]: renderCallNotificationMessage,
[Posts.POST_TYPES.VOICE]: renderVoiceMessage,
[Posts.POST_TYPES.SYSTEM_POST_REMINDER]: renderReminderSystemBotMessage,
[Posts.POST_TYPES.CHANGE_CHANNEL_PRIVACY]: renderChangeChannelPrivacyMessage,
};
Expand Down
258 changes: 258 additions & 0 deletions webapp/channels/src/components/voice/post_type/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {
PlayIcon,
PauseIcon,
DotsVerticalIcon,
DownloadOutlineIcon,
} from '@infomaniak/compass-icons/components';
import React, {useState, useEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';

import type {Post} from '@mattermost/types/posts';

import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils';

import {
AudioPlayerState,
useAudioPlayer,
} from 'components/common/hooks/useAudioPlayer';
import * as Menu from 'components/menu';
import TranscriptComponent from 'components/transcript/transcript';
import TranscriptSpinner from 'components/widgets/loading/loading_transcript_spinner';

import {convertSecondsToMSS} from 'utils/datetime';

export interface Props {
post?: Post;
isPreview?: boolean;
fileId?: string;
}

function VoiceMessageAttachmentPlayer(props: Props) {
const {post} = props;
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
if (post?.metadata && post.metadata.files && post.metadata.files.length > 0) {
setIsLoading(false);
}
}, [post?.metadata]);

const [anchorEl, setAnchorEl] = React.useState<HTMLAnchorElement | null>(null);
const [isTranscriptModalOpen, setIsTranscriptModalOpen] = useState(true);
const fileId = props.fileId ? props.fileId : post?.file_ids![0]; // There is always one file id for type voice.
const transcript = (props.fileId || isLoading) ? null : post?.metadata?.files[0]?.transcript;
const {formatMessage} = useIntl();
const {playerState, duration, elapsed, togglePlayPause} = useAudioPlayer(fileId ? `/api/v4/files/${fileId}` : '');
const progressValue = elapsed === 0 || duration === 0 ? '0' : (elapsed / duration).toFixed(2);
const open = Boolean(anchorEl);

function downloadFile() {
if (!fileId) {
return;
}
window.location.assign(getFileDownloadUrl(fileId));
}

const toggleTranscriptModal = () => {
setIsTranscriptModalOpen(!isTranscriptModalOpen);
};

const handleClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

const handleTranscriptClick = () => {
if (typeof transcript === 'object') {
toggleTranscriptModal();
}
};

const transcriptReady = (
<FormattedMessage
id='vocals.transcript_title'
defaultMessage='Audio Transcript (auto-generated)'
/>
);

const loadingMessage = (
<FormattedMessage
id='vocals.transcript_loading'
defaultMessage='Audio transcription in progress..'
/>
);

const toggle = (
<button
key='toggle'
className='style--none single-image-view__toggle'
aria-label='Toggle Embed Visibility'
onClick={toggleTranscriptModal}
>
{!props.fileId && (
isLoading ? (
<div style={{paddingRight: '3px'}}>
<TranscriptSpinner/>
</div>
) : (
<span className={`icon ${(isTranscriptModalOpen) ? 'icon-menu-down' : 'icon-menu-right'}`}/>
)
)}
</button>
);

const transcriptHeader = (
<div
className='image-header'
style={{cursor: 'pointer', display: 'flex'}}
onClick={handleTranscriptClick}
>
{toggle}
<div
data-testid='image-name'
className='image-name'
>
<div id='image-name-text'>
{props.isPreview ? null : (
<>
{isLoading && !props.fileId ? (
loadingMessage
) : (
transcriptReady
)}
</>
)}
</div>
</div>
</div>
);

return (
<>
<div className='post-image__column post-image__column--audio'>
<div className='post-image__thumbnail'>
<div
className='post-image__icon-background'
onClick={togglePlayPause}
>
{playerState === AudioPlayerState.Playing ? (
<PauseIcon
size={24}
color='var(--button-bg)'
/>
) : (
<PlayIcon
size={24}
color='var(--button-bg)'
/>
)}
</div>
</div>
<div className='post-image__details'>
<div className='post-image__detail_wrapper'>
<div className='post-image__detail'>
<div className='temp__audio-seeker'>
<progress
value={progressValue}
max='1'
/>
</div>
</div>
</div>
<div className='post-image__elapsed-time'>
{playerState === AudioPlayerState.Playing || playerState === AudioPlayerState.Paused ? convertSecondsToMSS(elapsed) : convertSecondsToMSS(duration)}
</div>
{props.post && (
<Menu.Container
menu={{id: 'dropdown-menu-dotmenu',
transformOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
anchorOrigin: {
vertical: 'top',
horizontal: 'left',
},
}}
menuButton={{
id: 'post-image-end-button',
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'post-image__end-button',
children: (
<DotsVerticalIcon
size={18}
color='currentColor'
/>),
}}
>
<Menu.Item
id={`download_${post?.id}`}
leadingElement={(
<DownloadOutlineIcon
size={18}
color='currentColor'
/>)}
labels={(
<FormattedMessage
id='single_image_view.download_tooltip'
defaultMessage='Download'
/>
)}
onClick={downloadFile}
/>
</Menu.Container>
)}
</div>
</div>
<div>
<div>
<div className='file-view--single'>
<div className='file__image'>
{transcriptHeader}
</div>
</div>
{!props.isPreview && !isLoading && (
<div >
<>
{typeof transcript === 'object' && transcript && transcript.text && transcript.text.length !== 0 && isTranscriptModalOpen && (
<div style={{paddingTop: '5px'}}>
{transcript?.text?.length > 300 ? transcript?.text.substring(0, 300) + '... ' : transcript?.text + ' '}
<a
className='transcript-link-view'
onClick={(event) => {
handleClick(event);
}}
>
<FormattedMessage
id={'vocals.loading_transcript'}
defaultMessage={'View the transcript'}
/>
</a>
</div>
)}
{typeof transcript === 'object' && transcript && transcript.text && transcript.text.length !== 0 && (
<TranscriptComponent
transcriptDatas={transcript}
handleClose={handleClose}
open={open}
anchorEl={anchorEl}
/>
)}
</>
</div>
) }
</div>

{/* {error && <div className='transcript-error'>{error}</div>} */}
</div>
</>
);
}

export default VoiceMessageAttachmentPlayer;
3 changes: 3 additions & 0 deletions webapp/platform/types/src/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import { TranscriptData } from "./transcript";

export type FileInfo = {
id: string;
user_id: string;
Expand All @@ -17,6 +19,7 @@ export type FileInfo = {
clientId: string;
post_id?: string;
mini_preview?: string;
transcript?: TranscriptData;
archived: boolean;
link?: string;
onlyoffice?: OnlyofficeMeta;
Expand Down

0 comments on commit 118270a

Please sign in to comment.