Skip to content

Commit fac6472

Browse files
authored
feat: Sidebar draft group (#38225)
1 parent 305a242 commit fac6472

14 files changed

Lines changed: 144 additions & 17 deletions

File tree

.changeset/red-maps-wink.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@rocket.chat/ui-client': minor
3+
'@rocket.chat/i18n': minor
4+
'@rocket.chat/meteor': minor
5+
---
6+
7+
Adds a new "Drafts" group to the sidebar, providing quick access to all rooms with unfinished messages.
8+
> This feature is available under the `Drafts in sidebar` feature preview and needs to be enabled in settings to be tested.

apps/meteor/app/api/server/v1/rooms.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { createDiscussion } from '../../../discussion/server/methods/createDiscu
6464
import { FileUpload } from '../../../file-upload/server';
6565
import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage';
6666
import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired';
67+
import { notifyOnSubscriptionChanged } from '../../../lib/server/lib/notifyListener';
6768
import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom';
6869
import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory';
6970
import { executeGetRoomRoles } from '../../../lib/server/methods/getRoomRoles';
@@ -414,6 +415,54 @@ const roomsSaveNotificationEndpoint = API.v1.post(
414415
},
415416
);
416417

418+
const saveDraftBodySchema = ajv.compile<{ rid: IRoom['_id']; draft: string }>({
419+
type: 'object',
420+
properties: {
421+
rid: { type: 'string', minLength: 1 },
422+
draft: { type: 'string' },
423+
},
424+
required: ['rid', 'draft'],
425+
additionalProperties: false,
426+
});
427+
428+
const saveDraftResponseSchema = ajv.compile<void>({
429+
type: 'object',
430+
properties: {
431+
success: { type: 'boolean', enum: [true] },
432+
},
433+
required: ['success'],
434+
additionalProperties: false,
435+
});
436+
437+
const roomsSaveDraftEndpoint = API.v1.post(
438+
'rooms.saveDraft',
439+
{
440+
authRequired: true,
441+
body: saveDraftBodySchema,
442+
response: {
443+
200: saveDraftResponseSchema,
444+
400: validateBadRequestErrorResponse,
445+
401: validateUnauthorizedErrorResponse,
446+
},
447+
},
448+
async function action() {
449+
const { rid, draft } = this.bodyParams;
450+
451+
if (draft.length > (settings.get<number>('Message_MaxAllowedSize') ?? 0)) {
452+
return API.v1.failure('error-message-size-exceeded');
453+
}
454+
455+
const subscription = await Subscriptions.updateDraftByRoomIdAndUserId(rid, this.userId, draft || undefined);
456+
if (!subscription) {
457+
throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription');
458+
}
459+
460+
void notifyOnSubscriptionChanged(subscription);
461+
462+
return API.v1.success();
463+
},
464+
);
465+
417466
API.v1.post(
418467
'rooms.cleanHistory',
419468
{
@@ -1630,7 +1679,8 @@ export const roomEndpoints = API.v1
16301679
);
16311680
type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints> &
16321681
ExtractRoutesFromAPI<typeof roomDeleteEndpoint> &
1633-
ExtractRoutesFromAPI<typeof roomsSaveNotificationEndpoint>;
1682+
ExtractRoutesFromAPI<typeof roomsSaveNotificationEndpoint> &
1683+
ExtractRoutesFromAPI<typeof roomsSaveDraftEndpoint>;
16341684

16351685
declare module '@rocket.chat/rest-typings' {
16361686
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface

apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { IMessage } from '@rocket.chat/core-typings';
22
import { Emitter } from '@rocket.chat/emitter';
3-
import { Accounts } from 'meteor/accounts-base';
43
import { Tracker } from 'meteor/tracker';
54
import type { RefObject } from 'react';
65

@@ -13,7 +12,8 @@ import { withDebouncing } from '../../../../lib/utils/highOrderFunctions';
1312

1413
export const createComposerAPI = (
1514
input: HTMLTextAreaElement,
16-
storageID: string,
15+
persistDraft: (value: string) => void,
16+
initialDraft: string,
1717
quoteChainLimit: number,
1818
composerRef: RefObject<HTMLElement>,
1919
{ rid, tmid }: { rid: string; tmid?: string },
@@ -40,20 +40,13 @@ export const createComposerAPI = (
4040
let _quotedMessages: IMessage[] = [];
4141

4242
const persist = withDebouncing({ wait: 300 })(() => {
43-
if (input.value) {
44-
Accounts.storageLocation.setItem(storageID, input.value);
45-
return;
46-
}
47-
48-
Accounts.storageLocation.removeItem(storageID);
43+
persistDraft(input.value);
4944
});
5045

5146
const notifyQuotedMessagesUpdate = (): void => {
5247
emitter.emit('quotedMessagesUpdate');
5348
};
5449

55-
input.addEventListener('input', persist);
56-
5750
const setText = (
5851
text: string,
5952
{
@@ -282,10 +275,12 @@ export const createComposerAPI = (
282275

283276
const insertNewLine = (): void => insertText('\n');
284277

285-
setText(Accounts.storageLocation.getItem(storageID) ?? '', {
278+
setText(initialDraft, {
286279
skipFocus: true,
287280
});
288281

282+
input.addEventListener('input', persist);
283+
289284
// Gets the text that is connected to the cursor and replaces it with the given text
290285
const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => {
291286
const { selectionStart, selectionEnd } = input;

apps/meteor/client/sidebar/hooks/useRoomList.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings';
22
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
3+
import { useFeaturePreview } from '@rocket.chat/ui-client';
34
import type { SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts';
45
import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts';
56
import { useVideoConfIncomingCalls } from '@rocket.chat/ui-video-conf';
@@ -19,6 +20,7 @@ const order = [
1920
'Open_Livechats',
2021
'On_Hold_Chats',
2122
'Unread',
23+
'Drafts',
2224
'Favorites',
2325
'Teams',
2426
'Discussions',
@@ -40,6 +42,7 @@ export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] })
4042
const showOmnichannel = useOmnichannelEnabled();
4143
const sidebarGroupByType = useUserPreference('sidebarGroupByType');
4244
const favoritesEnabled = useUserPreference('sidebarShowFavorites');
45+
const sidebarDrafts = useFeaturePreview('sidebarDrafts');
4346
const sidebarOrder = useUserPreference<typeof order>('sidebarSectionsOrder') ?? order;
4447
const isDiscussionEnabled = useSetting('Discussion_enabled');
4548
const sidebarShowUnread = useUserPreference('sidebarShowUnread');
@@ -58,6 +61,7 @@ export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] })
5861
useMemo(() => {
5962
const isCollapsed = (groupTitle: string) => collapsedGroups?.includes(groupTitle);
6063

64+
const drafts = new Set();
6165
const incomingCall = new Set();
6266
const favorite = new Set();
6367
const team = new Set();
@@ -82,6 +86,10 @@ export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] })
8286
return unread.add(room);
8387
}
8488

89+
if (sidebarDrafts && room.draft) {
90+
return drafts.add(room);
91+
}
92+
8593
if (favoritesEnabled && room.f) {
8694
return favorite.add(room);
8795
}
@@ -122,6 +130,8 @@ export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] })
122130

123131
sidebarShowUnread && unread.size && groups.set('Unread', unread);
124132

133+
sidebarDrafts && drafts.size && groups.set('Drafts', drafts);
134+
125135
favoritesEnabled && favorite.size && groups.set('Favorites', favorite);
126136

127137
sidebarGroupByType && team.size && groups.set('Teams', team);
@@ -193,6 +203,7 @@ export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] })
193203
rooms,
194204
showOmnichannel,
195205
inquiries.enabled,
206+
sidebarDrafts,
196207
queue,
197208
sidebarShowUnread,
198209
favoritesEnabled,

apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ import VideoMessageRecorder from '../../../composer/VideoMessageRecorder';
3737
import { useFileUpload } from '../../body/hooks/useFileUpload';
3838
import { useChat } from '../../contexts/ChatContext';
3939
import { useComposerPopupOptions } from '../../contexts/ComposerPopupContext';
40-
import { useRoom } from '../../contexts/RoomContext';
40+
import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
4141
import ComposerBoxPopup from '../ComposerBoxPopup';
4242
import ComposerBoxPopupPreview from '../ComposerBoxPopupPreview';
4343
import ComposerUserActionIndicator from '../ComposerUserActionIndicator';
4444
import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow';
4545
import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup';
4646
import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview';
4747
import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs';
48+
import { useDraft } from './hooks/useDraft';
4849
import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus';
4950
import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder';
5051

@@ -127,20 +128,24 @@ const MessageBox = ({
127128
const textareaRef = useRef(null);
128129
const messageComposerRef = useRef<HTMLElement>(null);
129130

130-
const storageID = `messagebox_${room._id}${tmid ? `-${tmid}` : ''}`;
131+
const subscription = useRoomSubscription();
132+
const { initialValue, persistLocal, flushDraft } = useDraft(room._id, tmid ? undefined : subscription?.draft, tmid);
131133

132134
const callbackRef = useCallback(
133135
(node: HTMLTextAreaElement) => {
134136
if (node === null && chat.composer) {
137+
flushDraft();
135138
return chat.setComposerAPI();
136139
}
137140

138141
if (chat.composer) {
139142
return;
140143
}
141-
chat.setComposerAPI(createComposerAPI(node, storageID, quoteChainLimit, messageComposerRef, { rid: room._id, tmid }));
144+
chat.setComposerAPI(
145+
createComposerAPI(node, persistLocal, initialValue, quoteChainLimit, messageComposerRef, { rid: room._id, tmid }),
146+
);
142147
},
143-
[chat, storageID, quoteChainLimit, room._id, tmid],
148+
[chat, flushDraft, initialValue, persistLocal, quoteChainLimit, room._id, tmid],
144149
);
145150

146151
const isTouchDevice = useMediaQuery('(pointer: coarse)');
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
2+
import { useEndpoint } from '@rocket.chat/ui-contexts';
3+
import { useCallback, useRef } from 'react';
4+
5+
export const useDraft = (rid: string, serverDraft?: string, tmid?: string) => {
6+
const storageKey = `messagebox_${rid}${tmid ? `-${tmid}` : ''}`;
7+
const [localDraft, setLocalDraft] = useLocalStorage<string>(storageKey, '');
8+
const saveDraft = useEndpoint('POST', '/v1/rooms.saveDraft');
9+
const initialValueRef = useRef(serverDraft || localDraft);
10+
const draftRef = useRef<string | null>(null);
11+
12+
const persistLocal = useCallback(
13+
(value: string) => {
14+
draftRef.current = value;
15+
setLocalDraft(value);
16+
},
17+
[setLocalDraft],
18+
);
19+
20+
const flushDraft = useCallback(() => {
21+
if (draftRef.current === null || tmid) {
22+
return;
23+
}
24+
25+
void saveDraft({ rid, draft: draftRef.current });
26+
draftRef.current = null;
27+
}, [saveDraft, rid, tmid]);
28+
29+
return {
30+
initialValue: initialValueRef.current,
31+
persistLocal,
32+
flushDraft,
33+
};
34+
};

apps/meteor/lib/publishFields.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const subscriptionFields = {
3939
E2EKey: 1,
4040
E2ESuggestedKey: 1,
4141
oldRoomKeys: 1,
42+
draft: 1,
4243
tunread: 1,
4344
tunreadGroup: 1,
4445
tunreadUser: 1,
3.83 KB
Loading

apps/meteor/server/settings/accounts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ export const createAccountSettings = () =>
750750
'Open_Livechats',
751751
'On_Hold_Chats',
752752
'Unread',
753+
'Drafts',
753754
'Favorites',
754755
'Teams',
755756
'Discussions',

packages/core-typings/src/ISubscription.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface ISubscription extends IRocketChatRecord {
6565

6666
department?: unknown;
6767

68+
draft?: string;
69+
6870
desktopPrefOrigin?: 'subscription' | 'user';
6971
mobilePrefOrigin?: 'subscription' | 'user';
7072
emailPrefOrigin?: 'subscription' | 'user';

0 commit comments

Comments
 (0)