Skip to content

Commit

Permalink
Merge branch 'tr/bl5rc4ld' into 'master'
Browse files Browse the repository at this point in the history
Added implement is recording presence

See merge request kchat/webapp!832
  • Loading branch information
antonbuks committed Aug 12, 2024
2 parents e261a35 + b6a5826 commit 97bac41
Show file tree
Hide file tree
Showing 26 changed files with 287 additions and 67 deletions.
22 changes: 17 additions & 5 deletions webapp/channels/src/actions/global_actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ export function sendAddToChannelEphemeralPost(user: UserProfile, addedUsername:
}

let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId: string, parentPostId: string) {
let recordingInterval: ReturnType<typeof setInterval> | null = null;
export function emitLocalUserTypingEvent(eventType = 'typing', channelId: string, parentPostId: string) {
const userTyping: ActionFuncAsync = async (actionDispatch, actionGetState) => {
const state = actionGetState();
const config = getConfig(state);
Expand All @@ -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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,9 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
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<TextboxElement>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -901,9 +901,9 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
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']) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextboxElement> | React.KeyboardEvent<TextboxElement>) => void;
postMsgKeyPress: (e: React.KeyboardEvent<TextboxElement>) => void;
handleChange: (e: React.ChangeEvent<TextboxElement>) => void;
Expand Down Expand Up @@ -263,6 +263,9 @@ const AdvanceTextEditor = ({
onUploadError={handleUploadError}
onRemoveDraft={removePreview}
onSubmit={handleSubmit}
onStarted={emitTypingEvent}
onCancel={emitTypingEvent}
onComplete={emitTypingEvent}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import {convertSecondsToMSS} from 'utils/datetime';

interface Props {
theme: Theme;
onCancel: () => void;
onComplete: (audioFile: File) => Promise<void>;
onCancel: (eventType: string) => void;
onComplete: (audioFile: File, eventType: string) => Promise<void>;
onStarted: (eventType: string) => void;
}

function VoiceMessageRecordingStarted(props: Props) {
Expand Down Expand Up @@ -61,6 +62,9 @@ function VoiceMessageRecordingStarted(props: Props) {

useEffect(() => {
startRecording();
if (typeof props.onStarted === 'function') {
props.onStarted('recording');
}

return () => {
cleanPostRecording(true);
Expand All @@ -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');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element>) => void;
onComplete: (eventType: string) => void;
onCancel: (eventType: string) => void;
onStarted: (eventType: string) => void;
}

const VoiceMessageAttachment = (props: Props) => {
Expand Down Expand Up @@ -131,6 +134,7 @@ const VoiceMessageAttachment = (props: Props) => {
async function handleCompleteRecordingClicked(audioFile: File) {
audioFileRef.current = audioFile;
uploadRecording(audioFile);
props.onComplete?.('stop');
}

function handleCancelRecordingClicked() {
Expand All @@ -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) {
Expand All @@ -148,6 +153,7 @@ const VoiceMessageAttachment = (props: Props) => {
theme={theme}
onCancel={handleCancelRecordingClicked}
onComplete={handleCompleteRecordingClicked}
onStarted={props.onStarted}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`components/MsgTyping should match snapshot, on multiple users recording 1`] = `
<span
className="msg-typing"
>
<MemoizedFormattedMessage
defaultMessage="{users} and {last} are recording..."
id="msg_recording.areRecording"
values={
Object {
"last": "another.user",
"users": "test.user, other.test.user",
}
}
/>
</span>
`;

exports[`components/MsgTyping should match snapshot, on multiple users typing 1`] = `
<span
className="msg-typing"
Expand All @@ -23,6 +40,22 @@ exports[`components/MsgTyping should match snapshot, on nobody typing 1`] = `
/>
`;

exports[`components/MsgTyping should match snapshot, on one user recording 1`] = `
<span
className="msg-typing"
>
<MemoizedFormattedMessage
defaultMessage="{user} is recording..."
id="msg_recording.isRecording"
values={
Object {
"user": "test.user",
}
}
/>
</span>
`;

exports[`components/MsgTyping should match snapshot, on one user typing 1`] = `
<span
className="msg-typing"
Expand All @@ -38,3 +71,9 @@ exports[`components/MsgTyping should match snapshot, on one user typing 1`] = `
/>
</span>
`;

exports[`components/MsgTyping should should match snapshot, on nobody recording 1`] = `
<span
className="msg-typing"
/>
`;
68 changes: 37 additions & 31 deletions webapp/channels/src/components/msg_typing/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,35 +17,33 @@ function getTimeBetweenTypingEvents(state: GlobalState) {

return config.TimeBetweenUserTypingUpdatesMilliseconds === undefined ? 0 : parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds, 10);
}
const createUserStartedAction = (action: ValueOf<typeof WebsocketEvents>, callback: ReturnType<typeof createUserStoppedAction>) =>
(userId: string, channelId: string, rootId: string, now: number): ThunkActionFunc<void> =>
(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<void> {
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) => {
Expand All @@ -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<typeof WebsocketEvents>) =>
(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);
7 changes: 6 additions & 1 deletion webapp/channels/src/components/msg_typing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -17,19 +17,24 @@ 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,
};
};
}

const mapDispatchToProps = {
userStartedTyping,
userStoppedTyping,
userStartedRecording,
userStoppedRecording,
};

export default connect(makeMapStateToProps, mapDispatchToProps)(MsgTyping);
24 changes: 24 additions & 0 deletions webapp/channels/src/components/msg_typing/msg_typing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -35,4 +38,25 @@ describe('components/MsgTyping', () => {
const wrapper = shallow(<MsgTyping {...props}/>);
expect(wrapper).toMatchSnapshot();
});

test('should should match snapshot, on nobody recording', () => {
const wrapper = shallow(<MsgTyping {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});

test('should match snapshot, on one user recording', () => {
const recordingUsers = ['test.user'];
const props = {...baseProps, recordingUsers};

const wrapper = shallow(<MsgTyping {...props}/>);
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(<MsgTyping {...props}/>);
expect(wrapper).toMatchSnapshot();
});
});
Loading

0 comments on commit 97bac41

Please sign in to comment.