diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index e2d60d35c2..99c353ab27 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -217,7 +217,8 @@ export function sendAddToChannelEphemeralPost(user: UserProfile, addedUsername: } let lastTimeTypingSent = 0; -export function emitLocalUserTypingEvent(channelId: string, parentPostId: string) { +let recordingInterval: ReturnType | null = null; +export function emitLocalUserTypingEvent(eventType = 'typing', channelId: string, parentPostId: string) { const userTyping: ActionFuncAsync = async (actionDispatch, actionGetState) => { const state = actionGetState(); const config = getConfig(state); @@ -237,10 +238,21 @@ export function emitLocalUserTypingEvent(channelId: string, parentPostId: string const timeBetweenUserTypingUpdatesMilliseconds = Utils.stringToNumber(config.TimeBetweenUserTypingUpdatesMilliseconds); const maxNotificationsPerChannel = Utils.stringToNumber(config.MaxNotificationsPerChannel); - if (((t - lastTimeTypingSent) > timeBetweenUserTypingUpdatesMilliseconds) && - (membersInChannel < maxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) { - WebSocketClient.userTyping(channelId, userId, parentPostId); - lastTimeTypingSent = t; + if (eventType === 'typing') { + if (((t - lastTimeTypingSent) > timeBetweenUserTypingUpdatesMilliseconds) && + (membersInChannel < maxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) { + WebSocketClient.userTyping(channelId, userId, parentPostId); + lastTimeTypingSent = t; + } + } else if (eventType === 'recording') { + const TIMER = 1000; + recordingInterval = setInterval(() => { + WebSocketClient.userRecording(channelId, userId, parentPostId); + }, TIMER); + } else if (eventType === 'stop') { + if (recordingInterval !== null) { + clearInterval(recordingInterval); + } } return {data: true}; diff --git a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx index f543c08238..bfe59242ed 100644 --- a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx +++ b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx @@ -766,9 +766,9 @@ class AdvancedCreateComment extends React.PureComponent { emitShortcutReactToLastPostFrom(Locations.RHS_ROOT); }; - emitTypingEvent = () => { + emitTypingEvent = (eventType = 'typing') => { const {channelId, rootId} = this.props; - GlobalActions.emitLocalUserTypingEvent(channelId, rootId); + GlobalActions.emitLocalUserTypingEvent(eventType, channelId, rootId); }; handleChange = (e: React.ChangeEvent) => { diff --git a/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx b/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx index 7910cb78ed..1bbf8119cc 100644 --- a/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx +++ b/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx @@ -901,9 +901,9 @@ class AdvancedCreatePost extends React.PureComponent { this.emitTypingEvent(); }; - emitTypingEvent = () => { + emitTypingEvent = (eventType = 'typing') => { const channelId = this.props.currentChannel.id; - GlobalActions.emitLocalUserTypingEvent(channelId, ''); + GlobalActions.emitLocalUserTypingEvent(eventType, channelId, ''); }; setDraftAsPostType = (channelId: Channel['id'], draft: PostDraft, postType?: PostDraft['postType']) => { diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx index 80a8830edd..149793f6e4 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx @@ -94,7 +94,7 @@ type Props = { enableGifPicker: boolean; handleBlur: () => void; handlePostError: (postError: React.ReactNode) => void; - emitTypingEvent: () => void; + emitTypingEvent: (eventType: string) => void; handleMouseUpKeyUp: (e: React.MouseEvent | React.KeyboardEvent) => void; postMsgKeyPress: (e: React.KeyboardEvent) => void; handleChange: (e: React.ChangeEvent) => void; @@ -263,6 +263,9 @@ const AdvanceTextEditor = ({ onUploadError={handleUploadError} onRemoveDraft={removePreview} onSubmit={handleSubmit} + onStarted={emitTypingEvent} + onCancel={emitTypingEvent} + onComplete={emitTypingEvent} /> ); diff --git a/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/components/recording_started/index.tsx b/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/components/recording_started/index.tsx index 7f96d62f2c..aad1379c47 100644 --- a/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/components/recording_started/index.tsx +++ b/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/components/recording_started/index.tsx @@ -25,8 +25,9 @@ import {convertSecondsToMSS} from 'utils/datetime'; interface Props { theme: Theme; - onCancel: () => void; - onComplete: (audioFile: File) => Promise; + onCancel: (eventType: string) => void; + onComplete: (audioFile: File, eventType: string) => Promise; + onStarted: (eventType: string) => void; } function VoiceMessageRecordingStarted(props: Props) { @@ -61,6 +62,9 @@ function VoiceMessageRecordingStarted(props: Props) { useEffect(() => { startRecording(); + if (typeof props.onStarted === 'function') { + props.onStarted('recording'); + } return () => { cleanPostRecording(true); @@ -69,14 +73,14 @@ function VoiceMessageRecordingStarted(props: Props) { async function handleRecordingCancelled() { await cleanPostRecording(true); - props.onCancel(); + props.onCancel('stop'); } async function handleRecordingComplete() { const audioFile = await stopRecording(); if (audioFile) { - props.onComplete(audioFile); + props.onComplete(audioFile, 'stop'); } } diff --git a/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/index.tsx b/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/index.tsx index 00ed0138be..66d936737a 100644 --- a/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/index.tsx +++ b/webapp/channels/src/components/advanced_text_editor/voice_message_attachment/index.tsx @@ -48,6 +48,9 @@ interface Props { onUploadError: (err: string | ServerError, clientId?: string, channelId?: Channel['id'], rootId?: Post['id']) => void; onRemoveDraft: (fileInfoIdOrClientId: FileInfo['id'] | string) => void; onSubmit: (e: FormEvent) => void; + onComplete: (eventType: string) => void; + onCancel: (eventType: string) => void; + onStarted: (eventType: string) => void; } const VoiceMessageAttachment = (props: Props) => { @@ -131,6 +134,7 @@ const VoiceMessageAttachment = (props: Props) => { async function handleCompleteRecordingClicked(audioFile: File) { audioFileRef.current = audioFile; uploadRecording(audioFile); + props.onComplete?.('stop'); } function handleCancelRecordingClicked() { @@ -140,6 +144,7 @@ const VoiceMessageAttachment = (props: Props) => { if (props.location === Locations.RHS_COMMENT) { props.setDraftAsPostType(props.rootId, props.draft); } + props.onCancel?.('stop'); } if (props.vmState === VoiceMessageStates.RECORDING) { @@ -148,6 +153,7 @@ const VoiceMessageAttachment = (props: Props) => { theme={theme} onCancel={handleCancelRecordingClicked} onComplete={handleCompleteRecordingClicked} + onStarted={props.onStarted} /> ); } diff --git a/webapp/channels/src/components/msg_typing/__snapshots__/msg_typing.test.tsx.snap b/webapp/channels/src/components/msg_typing/__snapshots__/msg_typing.test.tsx.snap index 6c9926da45..2fc9183ef1 100644 --- a/webapp/channels/src/components/msg_typing/__snapshots__/msg_typing.test.tsx.snap +++ b/webapp/channels/src/components/msg_typing/__snapshots__/msg_typing.test.tsx.snap @@ -1,5 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`components/MsgTyping should match snapshot, on multiple users recording 1`] = ` + + + +`; + exports[`components/MsgTyping should match snapshot, on multiple users typing 1`] = ` `; +exports[`components/MsgTyping should match snapshot, on one user recording 1`] = ` + + + +`; + exports[`components/MsgTyping should match snapshot, on one user typing 1`] = ` `; + +exports[`components/MsgTyping should should match snapshot, on nobody recording 1`] = ` + +`; diff --git a/webapp/channels/src/components/msg_typing/actions.ts b/webapp/channels/src/components/msg_typing/actions.ts index b2f37e0047..15d799dc79 100644 --- a/webapp/channels/src/components/msg_typing/actions.ts +++ b/webapp/channels/src/components/msg_typing/actions.ts @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import type {GlobalState} from '@mattermost/types/store'; +import type {ValueOf} from '@mattermost/types/utilities'; import {getMissingProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; import {General, Preferences, WebsocketEvents} from 'mattermost-redux/constants'; @@ -16,35 +17,33 @@ function getTimeBetweenTypingEvents(state: GlobalState) { return config.TimeBetweenUserTypingUpdatesMilliseconds === undefined ? 0 : parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds, 10); } +const createUserStartedAction = (action: ValueOf, callback: ReturnType) => + (userId: string, channelId: string, rootId: string, now: number): ThunkActionFunc => + (dispatch, getState) => { + const state = getState(); + if ( + isPerformanceDebuggingEnabled(state) && + getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TYPING_MESSAGES) + ) { + return; + } -export function userStartedTyping(userId: string, channelId: string, rootId: string, now: number): ThunkActionFunc { - return (dispatch, getState) => { - const state = getState(); + dispatch({ + type: action, + data: { + id: channelId + rootId, + userId, + now, + }, + }); - if ( - isPerformanceDebuggingEnabled(state) && - getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TYPING_MESSAGES) - ) { - return; - } + // Ideally this followup loading would be done by someone else + dispatch(fillInMissingInfo(userId)); - dispatch({ - type: WebsocketEvents.TYPING, - data: { - id: channelId + rootId, - userId, - now, - }, - }); - - // Ideally this followup loading would be done by someone else - dispatch(fillInMissingInfo(userId)); - - setTimeout(() => { - dispatch(userStoppedTyping(userId, channelId, rootId, now)); - }, getTimeBetweenTypingEvents(state)); - }; -} + setTimeout(() => { + dispatch(callback(userId, channelId, rootId, now)); + }, getTimeBetweenTypingEvents(state)); + }; function fillInMissingInfo(userId: string): ActionFuncAsync { return async (dispatch, getState) => { @@ -69,13 +68,20 @@ function fillInMissingInfo(userId: string): ActionFuncAsync { }; } -export function userStoppedTyping(userId: string, channelId: string, rootId: string, now: number) { - return { - type: WebsocketEvents.STOP_TYPING, +const createUserStoppedAction = (action: ValueOf) => + (userId: string, channelId: string, rootId: string, now: number) => ({ + type: action, data: { id: channelId + rootId, userId, now, }, - }; -} + }); + +export const userStoppedTyping = createUserStoppedAction(WebsocketEvents.STOP_TYPING); + +export const userStoppedRecording = createUserStoppedAction(WebsocketEvents.STOP_RECORDING); + +export const userStartedTyping = createUserStartedAction(WebsocketEvents.TYPING, userStoppedTyping); + +export const userStartedRecording = createUserStartedAction(WebsocketEvents.RECORDING, userStoppedRecording); diff --git a/webapp/channels/src/components/msg_typing/index.ts b/webapp/channels/src/components/msg_typing/index.ts index 9c73553fcb..9983eda050 100644 --- a/webapp/channels/src/components/msg_typing/index.ts +++ b/webapp/channels/src/components/msg_typing/index.ts @@ -7,7 +7,7 @@ import {makeGetUsersTypingByChannelAndPost} from 'mattermost-redux/selectors/ent import type {GlobalState} from 'types/store'; -import {userStartedTyping, userStoppedTyping} from './actions'; +import {userStartedRecording, userStartedTyping, userStoppedRecording, userStoppedTyping} from './actions'; import MsgTyping from './msg_typing'; type OwnProps = { @@ -17,12 +17,15 @@ type OwnProps = { function makeMapStateToProps() { const getUsersTypingByChannelAndPost = makeGetUsersTypingByChannelAndPost(); + const getUsersRecordingByChannelAndPost = makeGetUsersTypingByChannelAndPost('recording'); return function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const typingUsers = getUsersTypingByChannelAndPost(state, {channelId: ownProps.channelId, postId: ownProps.postId}); + const recordingUsers = getUsersRecordingByChannelAndPost(state, {channelId: ownProps.channelId, postId: ownProps.postId}); return { typingUsers, + recordingUsers, }; }; } @@ -30,6 +33,8 @@ function makeMapStateToProps() { const mapDispatchToProps = { userStartedTyping, userStoppedTyping, + userStartedRecording, + userStoppedRecording, }; export default connect(makeMapStateToProps, mapDispatchToProps)(MsgTyping); diff --git a/webapp/channels/src/components/msg_typing/msg_typing.test.tsx b/webapp/channels/src/components/msg_typing/msg_typing.test.tsx index 2c83757187..9e1ec800d5 100644 --- a/webapp/channels/src/components/msg_typing/msg_typing.test.tsx +++ b/webapp/channels/src/components/msg_typing/msg_typing.test.tsx @@ -9,10 +9,13 @@ import MsgTyping from 'components/msg_typing/msg_typing'; describe('components/MsgTyping', () => { const baseProps = { typingUsers: [], + recordingUsers: [], channelId: 'test', postId: '', userStartedTyping: jest.fn(), userStoppedTyping: jest.fn(), + userStartedRecording: jest.fn(), + userStoppedRecording: jest.fn(), }; test('should match snapshot, on nobody typing', () => { @@ -35,4 +38,25 @@ describe('components/MsgTyping', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); + + test('should should match snapshot, on nobody recording', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, on one user recording', () => { + const recordingUsers = ['test.user']; + const props = {...baseProps, recordingUsers}; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, on multiple users recording', () => { + const recordingUsers = ['test.user', 'other.test.user', 'another.user']; + const props = {...baseProps, recordingUsers}; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/webapp/channels/src/components/msg_typing/msg_typing.tsx b/webapp/channels/src/components/msg_typing/msg_typing.tsx index 004b2c4dd1..d7b8a8f548 100644 --- a/webapp/channels/src/components/msg_typing/msg_typing.tsx +++ b/webapp/channels/src/components/msg_typing/msg_typing.tsx @@ -13,21 +13,31 @@ type Props = { channelId: string; postId: string; typingUsers: string[]; + recordingUsers: string[]; userStartedTyping: (userId: string, channelId: string, rootId: string, now: number) => void; userStoppedTyping: (userId: string, channelId: string, rootId: string, now: number) => void; + userStoppedRecording: (userId: string, channelId: string, rootId: string, now: number) => void; + userStartedRecording: (userId: string, channelId: string, rootId: string, now: number) => void; } export default function MsgTyping(props: Props) { - const {userStartedTyping, userStoppedTyping} = props; + const {userStartedTyping, userStoppedTyping, userStoppedRecording, userStartedRecording} = props; + useWebSocket({ handler: useCallback((msg: WebSocketMessage) => { - if (msg.event === SocketEvents.TYPING) { + if (msg.event === SocketEvents.TYPING || msg.event === SocketEvents.RECORDING) { const channelId = msg.data.data.channel_id; const rootId = msg.data.data.parent_id; const userId = msg.data.data.user_id; - - if (props.channelId === channelId && props.postId === rootId) { + switch (msg.event) { + case SocketEvents.TYPING: userStartedTyping(userId, channelId, rootId, Date.now()); + break; + case SocketEvents.RECORDING: + userStartedRecording(userId, channelId, rootId, Date.now()); + break; + default: + break; } } else if (msg.event === SocketEvents.POSTED) { const post = msg.data.post; @@ -38,27 +48,25 @@ export default function MsgTyping(props: Props) { if (props.channelId === channelId && props.postId === rootId) { userStoppedTyping(userId, channelId, rootId, Date.now()); + userStoppedRecording(userId, channelId, rootId, Date.now()); } } - }, [props.channelId, props.postId, userStartedTyping, userStoppedTyping]), + }, [props.channelId, props.postId, userStartedTyping, userStartedRecording, userStoppedTyping, + userStoppedRecording]), }); - const getTypingText = () => { - let users: string[] = []; - let numUsers = 0; - if (props.typingUsers) { - users = [...props.typingUsers]; - numUsers = users.length; - } - + const getInputText = (users: string[], eventType = 'typing') => { + const numUsers = users.length; if (numUsers === 0) { return ''; } + const {simpleMessage, multipleMessage, defaultSimpleMessage, defaultMultipleMessage} = getMessages(eventType); + if (numUsers === 1) { return ( { + switch (eventType) { + case 'typing': + return { + simpleMessage: 'msg_typing.isTyping', + multipleMessage: 'msg_typing.areTyping', + defaultSimpleMessage: '{user} is typing...', + defaultMultipleMessage: '{users} and {last} are typing...', + }; + case 'recording': + return { + simpleMessage: 'msg_recording.isRecording', + multipleMessage: 'msg_recording.areRecording', + defaultSimpleMessage: '{user} is recording...', + defaultMultipleMessage: '{users} and {last} are recording...', + }; + default: + throw new Error('no messages found'); + } + }; + + const typingText = getInputText([...props.typingUsers], 'typing'); + const recordingText = getInputText([...props.recordingUsers], 'recording'); + + if (typingText) { + return {typingText}; + } return ( - {getTypingText()} + {recordingText} ); } diff --git a/webapp/channels/src/components/textbox/textbox.tsx b/webapp/channels/src/components/textbox/textbox.tsx index 6abcc29b8e..7fb43957d4 100644 --- a/webapp/channels/src/components/textbox/textbox.tsx +++ b/webapp/channels/src/components/textbox/textbox.tsx @@ -38,7 +38,7 @@ export type Props = { value: string; onChange: (e: ChangeEvent) => void; onKeyPress: (e: KeyboardEvent) => void; - onComposition?: () => void; + onComposition?: (eventType: 'typing' | 'recording' | 'stop') => void; // infomaniak onHeightChange?: (height: number, maxHeight: number) => void; onWidthChange?: (width: number) => void; createMessage: string; diff --git a/webapp/channels/src/i18n/de.json b/webapp/channels/src/i18n/de.json index 4a89618f2a..92938c5ee2 100644 --- a/webapp/channels/src/i18n/de.json +++ b/webapp/channels/src/i18n/de.json @@ -4422,6 +4422,8 @@ "move_thread_modal.title": "Unterhaltung verschieben", "msg_typing.areTyping": "{users} und {last} tippen gerade...", "msg_typing.isTyping": "{user} tippt...", + "msg_recording.isRecording" : "{user} nimmt auf..", + "msg_recording.areRecording": "{users} und {last} nehmen auf...", "multiselect.add": "Hinzufügen", "multiselect.addChannelsPlaceholder": "Kanäle suchen und hinzufügen", "multiselect.addGroupMembers": "{number} Personen hinzufügen", diff --git a/webapp/channels/src/i18n/en-AU.json b/webapp/channels/src/i18n/en-AU.json index 6cbc2fc821..a2485c0c89 100644 --- a/webapp/channels/src/i18n/en-AU.json +++ b/webapp/channels/src/i18n/en-AU.json @@ -4254,6 +4254,8 @@ "move_thread_modal.title": "Move thread", "msg_typing.areTyping": "{users} and {last} are typing...", "msg_typing.isTyping": "{user} is typing...", + "msg_recording.isRecording" : "{user} is recording...", + "msg_recording.areRecording": "{users} et {last} are recording...", "multiselect.add": "Add", "multiselect.addChannelsPlaceholder": "Search and add channels", "multiselect.addGroupMembers": "Add {number} people", diff --git a/webapp/channels/src/i18n/es.json b/webapp/channels/src/i18n/es.json index f9b1866992..8cc915cc1e 100644 --- a/webapp/channels/src/i18n/es.json +++ b/webapp/channels/src/i18n/es.json @@ -3796,6 +3796,8 @@ "more_direct_channels.title": "Mensajes Directos", "msg_typing.areTyping": "{users} y {last} están escribiendo...", "msg_typing.isTyping": "{user} está escribiendo...", + "msg_recording.isRecording" : "{user} está grabando...", + "msg_recording.areRecording": "{users} et {last} están grabando...", "multiselect.add": "Agregar", "multiselect.addChannelsPlaceholder": "Buscar y agregar canales", "multiselect.addGroupsPlaceholder": "Buscar y agregar grupos", diff --git a/webapp/channels/src/i18n/fr.json b/webapp/channels/src/i18n/fr.json index c696d398aa..d9c7635a41 100644 --- a/webapp/channels/src/i18n/fr.json +++ b/webapp/channels/src/i18n/fr.json @@ -3735,6 +3735,8 @@ "more_direct_channels.title": "Messages personnels", "msg_typing.areTyping": "{users} et {last} sont en train d'écrire...", "msg_typing.isTyping": "{user} est en train d'écrire...", + "msg_recording.isRecording" : "{user} est en train d'enregistrer...", + "msg_recording.areRecording": "{users} et {last} sont en train d'enregistrer...", "multiselect.add": "Ajouter", "multiselect.addChannelsPlaceholder": "Rechercher et ajouter des canaux", "multiselect.addGroupMembers": "Ajouter {number} personne(s)", diff --git a/webapp/channels/src/i18n/it.json b/webapp/channels/src/i18n/it.json index 8c6a17ec9e..f831ce87fe 100644 --- a/webapp/channels/src/i18n/it.json +++ b/webapp/channels/src/i18n/it.json @@ -2900,6 +2900,8 @@ "more_direct_channels.title": "Messaggio Privato", "msg_typing.areTyping": "{users} e {last} stanno scrivendo...", "msg_typing.isTyping": "{user} sta scrivendo...", + "msg_recording.isRecording" : "{user} sta registrando", + "msg_recording.areRecording": "{users} et {last} stanno registrando...", "multiselect.add": "Aggiungi", "multiselect.addChannelsPlaceholder": "Cerca e aggiungi canali", "multiselect.addGroupsPlaceholder": "Cerca e aggiungi gruppi", diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts index f04e88ff9c..06f24f0326 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts @@ -27,6 +27,8 @@ const WebsocketEvents = { ROLE_UPDATED: 'role_updated', TYPING: 'client-user_typing', STOP_TYPING: 'stop_typing', + RECORDING: 'client-user_recording', + STOP_RECORDING: 'stop_recording', PREFERENCE_CHANGED: 'preference_changed', PREFERENCES_CHANGED: 'preferences_changed', PREFERENCES_DELETED: 'preferences_deleted', diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts index 19b955560d..ba11702d9c 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts @@ -21,6 +21,7 @@ import jobs from './jobs'; import limits from './limits'; import posts from './posts'; import preferences from './preferences'; +import recording from './recording'; import roles from './roles'; import schemes from './schemes'; import search from './search'; @@ -40,6 +41,7 @@ export default combineReducers({ files, preferences, typing, + recording, integrations, emojis, gifs, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/recording.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/recording.ts new file mode 100644 index 0000000000..8a52c9ee17 --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/recording.ts @@ -0,0 +1,54 @@ +import type {AnyAction} from 'redux'; + +import type {Recording} from '@mattermost/types/recording'; + +import {WebsocketEvents} from 'mattermost-redux/constants'; + +export default function recording(state: Recording = {}, action: AnyAction): Recording { + const { + data, + type, + } = action; + + switch (type) { + case WebsocketEvents.RECORDING: { + const { + id, + userId, + now, + } = data; + + if (id && userId) { + return { + ...state, + [id]: { + ...state[id], + [userId]: now, + }, + }; + } + + return state; + } + case WebsocketEvents.STOP_RECORDING: { + const { + id, + userId, + now, + } = data; + + if (state[id] && state[id][userId] <= now) { + const nextState = {...state}; + + delete nextState[id]; + + return nextState; + } + + return state; + } + + default: + return state; + } +} diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/typing.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/typing.ts index 4d4a9c8a83..1ffa378d74 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/typing.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/typing.ts @@ -27,14 +27,14 @@ const getUsersTypingImpl = (profiles: IDMappedObjects, teammateName return []; }; -export function makeGetUsersTypingByChannelAndPost(): (state: GlobalState, props: {channelId: string; postId: string}) => string[] { +export function makeGetUsersTypingByChannelAndPost(eventType: 'typing' | 'recording' = 'typing'): (state: GlobalState, props: {channelId: string; postId: string}) => string[] { return createSelector( 'makeGetUsersTypingByChannelAndPost', getUsers, getTeammateNameDisplaySetting, (state: GlobalState, options: {channelId: string; postId: string}) => options.channelId, (state: GlobalState, options: {channelId: string; postId: string}) => options.postId, - (state: GlobalState) => state.entities.typing, + (state: GlobalState) => state.entities[eventType], getUsersTypingImpl, ); } diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 987b8e885c..b443d5f3ab 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -154,6 +154,7 @@ const state: GlobalState = { // isLimitedResults: -1, }, typing: {}, + recording: {}, roles: { roles: {}, pending: new Set(), diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index bde98db565..fa8f697147 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -701,6 +701,7 @@ export const SocketEvents = { ROLE_REMOVED: 'role_removed', ROLE_UPDATED: 'role_updated', TYPING: 'client-user_typing', + RECORDING: 'client-user_recording', PREFERENCE_CHANGED: 'preference_changed', PREFERENCES_CHANGED: 'preferences_changed', PREFERENCES_DELETED: 'preferences_deleted', diff --git a/webapp/platform/client/src/websocket.ts b/webapp/platform/client/src/websocket.ts index c7eeb918df..6bc7e9f4bc 100644 --- a/webapp/platform/client/src/websocket.ts +++ b/webapp/platform/client/src/websocket.ts @@ -695,6 +695,15 @@ export default class WebSocketClient { this.sendPresenceMessage('client-user_typing', data, callback); } + userRecording(channelId: string, userId: string, parentId: string, callback?: () => void) { + const data = { + channel_id: channelId, + parent_id: parentId, + user_id: userId, + }; + this.sendPresenceMessage('client-user_recording', data, callback); + } + updateActiveChannel(channelId: string, callback?: (msg: any) => void) { const data = { channel_id: channelId, diff --git a/webapp/platform/types/src/recording.ts b/webapp/platform/types/src/recording.ts new file mode 100644 index 0000000000..731d25e2da --- /dev/null +++ b/webapp/platform/types/src/recording.ts @@ -0,0 +1,5 @@ +export type Recording = { + [x: string]: { + [x: string]: number; + }; +}; \ No newline at end of file diff --git a/webapp/platform/types/src/store.ts b/webapp/platform/types/src/store.ts index d8cb8c1565..058750f039 100644 --- a/webapp/platform/types/src/store.ts +++ b/webapp/platform/types/src/store.ts @@ -32,6 +32,7 @@ import {AppsState} from './apps'; import {InsightsState} from './insights'; import {GifsState} from './gifs'; import {LimitsState} from './limits'; +import {Recording} from './recording'; export type GlobalState = { entities: { @@ -57,6 +58,7 @@ export type GlobalState = { files: FilesState; emojis: EmojisState; typing: Typing; + recording: Recording; roles: { roles: { [x: string]: Role;