From 2db0ac0148ba3b21b5b2331f144c6246a8aab296 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 15:40:22 +0100 Subject: [PATCH 1/6] extracted sendFeedback utility for user notifications in rooms --- src/app/hooks/useCommands.ts | 104 ++++++++-------------------- src/app/utils/sendFeedbackToUser.ts | 12 ++++ 2 files changed, 42 insertions(+), 74 deletions(-) create mode 100644 src/app/utils/sendFeedbackToUser.ts diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index 0c2008c01..436386926 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -35,6 +35,7 @@ import { parsePronounsInput } from '$utils/pronouns'; import { useRoomNavigate } from './useRoomNavigate'; import { enrichWidgetUrl } from './useRoomWidgets'; import { useUserProfile } from './useUserProfile'; +import { sendFeedback } from '$utils/sendFeedbackToUser'; export const SHRUG = '¯\\_(ツ)_/¯'; export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻'; @@ -641,17 +642,6 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const input = payload.trim().toLowerCase(); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~sable-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - try { if (input === 'reset' || input === 'clear') { await mx.sendStateEvent( @@ -660,7 +650,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { {}, userId ); - sendFeedback('Room color has been reset.'); + sendFeedback('Room color has been reset.', room, userId); return; } @@ -671,14 +661,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { { color: input }, userId ); - sendFeedback(`Room color set to ${input}.`); + sendFeedback(`Room color set to ${input}.`, room, userId); } else { - sendFeedback('Invalid format. Use #RRGGBB.'); + sendFeedback('Invalid format. Use #RRGGBB.', room, userId); } } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { sendFeedback( - 'Permission Denied. An admin must enable "Room Colors" in Settings > Cosmetics in app.sable.moe or another supported client.' + 'Permission Denied. An admin must enable "Room Colors" in Settings > Cosmetics in app.sable.moe or another supported client.', + room, + userId ); } } @@ -692,17 +684,6 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const input = payload.trim().toLowerCase(); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~sable-g-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - const parents = room .getLiveTimeline() .getState(EventTimeline.FORWARDS) @@ -719,7 +700,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { {}, userId ); - sendFeedback('Global space color reset.'); + sendFeedback('Global space color reset.', room, userId); return; } @@ -730,14 +711,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { { color: input }, userId ); - sendFeedback(`Global space color set to ${input}.`); + sendFeedback(`Global space color set to ${input}.`, room, userId); } else { - sendFeedback('Invalid format. Use #RRGGBB.'); + sendFeedback('Invalid format. Use #RRGGBB.', room, userId); } } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { sendFeedback( - 'Permission Denied. An admin must enable "Space-Wide Colors" in Settings > Cosmetics in app.sable.moe or another supported client.' + 'Permission Denied. An admin must enable "Space-Wide Colors" in Settings > Cosmetics in app.sable.moe or another supported client.', + room, + userId ); } } @@ -753,21 +736,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { .slice(0, 32); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~font-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - try { if (input.toLowerCase() === 'reset' || input === '') { await mx.sendStateEvent(room.roomId, StateEvent.RoomCosmeticsFont as any, {}, userId); - sendFeedback('Room font reset.'); + sendFeedback('Room font reset.', room, userId); return; } @@ -777,11 +749,13 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { { font: input }, userId ); - sendFeedback(`Room font set to "${input}".`); + sendFeedback(`Room font set to "${input}".`, room, userId); } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { sendFeedback( - 'Permission Denied. An admin must enable "Room Fonts" in Settings > Cosmetics in app.sable.moe or another supported client.' + 'Permission Denied. An admin must enable "Room Fonts" in Settings > Cosmetics in app.sable.moe or another supported client.', + room, + userId ); } } @@ -797,17 +771,6 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { .slice(0, 32); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~sfont-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - const parents = room .getLiveTimeline() .getState(EventTimeline.FORWARDS) @@ -824,7 +787,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { {}, userId ); - sendFeedback('Space font reset.'); + sendFeedback('Space font reset.', room, userId); return; } @@ -834,11 +797,13 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { { font: input }, userId ); - sendFeedback(`Space font set to "${input}".`); + sendFeedback(`Space font set to "${input}".`, room, userId); } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { sendFeedback( - 'Permission Denied. An admin must enable "Space-Wide Fonts" in Settings > Cosmetics in app.sable.moe or another supported client.' + 'Permission Denied. An admin must enable "Space-Wide Fonts" in Settings > Cosmetics in app.sable.moe or another supported client.', + room, + userId ); } } @@ -850,23 +815,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~nullptr-widget-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - const parts = payload.trim().split(/\s+/); const url = parts[0]; const name = parts.slice(1).join(' ') || 'Widget'; if (!url) { - sendFeedback('Usage: /addwidget [name]'); + sendFeedback('Usage: /addwidget [name]', room, userId); return; } @@ -874,7 +828,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { try { parsedUrl = new URL(url); } catch { - sendFeedback('Invalid URL. Please provide a valid widget URL.'); + sendFeedback('Invalid URL. Please provide a valid widget URL.', room, userId); return; } @@ -892,14 +846,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { } as any, widgetId ); - sendFeedback(`Widget "${name}" added.`); + sendFeedback(`Widget "${name}" added.`, room, userId); } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { sendFeedback( - 'Permission denied. You need permission to manage widgets in this room.' + 'Permission denied. You need permission to manage widgets in this room.', + room, + userId ); } else { - sendFeedback(`Failed to add widget: ${e.message || 'Unknown error'}`); + sendFeedback(`Failed to add widget: ${e.message || 'Unknown error'}`, room, userId); } } }, diff --git a/src/app/utils/sendFeedbackToUser.ts b/src/app/utils/sendFeedbackToUser.ts new file mode 100644 index 000000000..c1fa7ab42 --- /dev/null +++ b/src/app/utils/sendFeedbackToUser.ts @@ -0,0 +1,12 @@ +import { MatrixEvent, Room } from 'matrix-js-sdk'; + +export function sendFeedback(msg: string, room: Room, userId: string) { + const localNotice = new MatrixEvent({ + type: 'm.room.message', + content: { msgtype: 'm.notice', body: msg }, + event_id: `~sable-feedback-${Date.now()}`, + room_id: room.roomId, + sender: userId, + }); + room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); +} From c92c8181e8e10cb32f77c40aa0b935fc16daa25a Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 15:45:39 +0100 Subject: [PATCH 2/6] refactor: streamline sendFeedback utility and add ShareE2EEHistory command --- src/app/hooks/useCommands.ts | 201 +++++++++++++---------------------- 1 file changed, 71 insertions(+), 130 deletions(-) diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index 436386926..c8ee9fb28 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -10,7 +10,6 @@ import { Visibility, RoomServerAclEventContent, MsgType, - MatrixEvent, } from '$types/matrix-sdk'; import { useMemo } from 'react'; import { Membership, StateEvent } from '$types/matrix/room'; @@ -32,10 +31,10 @@ import { settingsAtom } from '$state/settings'; import { useOpenBugReportModal } from '$state/hooks/bugReportModal'; import { createRoomEncryptionState } from '$components/create-room'; import { parsePronounsInput } from '$utils/pronouns'; +import { sendFeedback } from '$utils/sendFeedbackToUser'; import { useRoomNavigate } from './useRoomNavigate'; import { enrichWidgetUrl } from './useRoomWidgets'; import { useUserProfile } from './useUserProfile'; -import { sendFeedback } from '$utils/sendFeedbackToUser'; export const SHRUG = '¯\\_(ツ)_/¯'; export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻'; @@ -252,6 +251,8 @@ export enum Command { Headpat = 'headpat', // Meta Report = 'bugreport', + // Experimental + ShareE2EEHistory = 'sharehistory', } export type CommandContent = { @@ -869,17 +870,6 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const rawInput = match ? match[1].trim() : payload.trim(); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~pronoun-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - try { if (['reset', 'clear', ''].includes(rawInput.toLowerCase())) { await mx.sendStateEvent( @@ -888,7 +878,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { {}, userId ); - sendFeedback('Room pronouns have been reset.'); + sendFeedback('Room pronouns have been reset.', room, userId); return; } @@ -905,10 +895,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { .map((p) => (p.language ? `for ${p.language} "${p.summary}" was set` : p.summary)) .join(', '); - sendFeedback(`Room pronouns set: ${feedbackString}`); + sendFeedback(`Room pronouns set: ${feedbackString}`, room, userId); } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { - sendFeedback('Permission Denied. Could not update room pronouns.'); + sendFeedback('Permission Denied. Could not update room pronouns.', room, userId); } } }, @@ -922,17 +912,6 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const rawInput = match ? match[1].trim() : payload.trim(); const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~gpronoun-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - const parents = room .getLiveTimeline() .getState(EventTimeline.FORWARDS) @@ -949,7 +928,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { {}, userId ); - sendFeedback('Global space pronouns reset.'); + sendFeedback('Global space pronouns reset.', room, userId); return; } @@ -966,10 +945,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { .map((p) => (p.language ? `for ${p.language} "${p.summary}" was set` : p.summary)) .join(', '); - sendFeedback(`Global space pronouns set: ${feedbackString}`); + sendFeedback(`Global space pronouns set: ${feedbackString}`, room, userId); } catch (e: any) { if (e.errcode === 'M_FORBIDDEN') { - sendFeedback('Permission Denied. Could not update space pronouns.'); + sendFeedback('Permission Denied. Could not update space pronouns.', room, userId); } } }, @@ -995,25 +974,15 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { '[Dev only] Send raw message event. Example: /rawmsg {"msgtype":"m.text", "body":"hello"}', exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~rawmsg-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; if (!developerTools) { - sendFeedback('Command available in Developer Mode only.'); + sendFeedback('Command available in Developer Mode only.', room, userId); return; } try { const content = JSON.parse(payload); await mx.sendMessage(room.roomId, content); } catch (e: any) { - sendFeedback(`Invalid JSON: ${e.message}`); + sendFeedback(`Invalid JSON: ${e.message}`, room, userId); } }, }, @@ -1022,19 +991,9 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: '[Dev only] Send any raw event. Usage: /raw [-s stateKey]', exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~rawevent-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; if (!developerTools) { - sendFeedback('Command available in Developer Mode only.'); + sendFeedback('Command available in Developer Mode only.', room, userId); return; } @@ -1046,7 +1005,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const jsonString = mainPayload.trim().substring(eventType.length).trim(); if (!eventType || !jsonString) { - sendFeedback('Usage: /rawevent [-s stateKey]'); + sendFeedback('Usage: /rawevent [-s stateKey]', room, userId); return; } @@ -1055,13 +1014,17 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { if (typeof stateKey === 'string') { await mx.sendStateEvent(room.roomId, eventType as any, content, stateKey); - sendFeedback(`State event "${eventType}" sent with state key "${stateKey}".`); + sendFeedback( + `State event "${eventType}" sent with state key "${stateKey}".`, + room, + userId + ); } else { await mx.sendEvent(room.roomId, eventType as any, content); - sendFeedback(`Event "${eventType}" sent.`); + sendFeedback(`Event "${eventType}" sent.`, room, userId); } } catch (e: any) { - sendFeedback(`Error: ${e.message}`); + sendFeedback(`Error: ${e.message}`, room, userId); } }, }, @@ -1070,26 +1033,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: '[Dev only] Merge global account data. Usage: /rawacc ', exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~rawacc-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - (room as any).addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; if (!developerTools) { - sendFeedback('Command available in Developer Mode only.'); + sendFeedback('Command available in Developer Mode only.', room, userId); return; } const trimmed = payload.trim(); const firstSpaceIndex = trimmed.indexOf(' '); if (firstSpaceIndex === -1) { - sendFeedback('Usage: /rawacc '); + sendFeedback('Usage: /rawacc ', room, userId); return; } @@ -1105,9 +1058,9 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const mergedContent = { ...existingContent, ...newContent }; await mx.setAccountData(type as any, mergedContent); - sendFeedback(`Account data "${type}" merged successfully.`); + sendFeedback(`Account data "${type}" merged successfully.`, room, userId); } catch (e: any) { - sendFeedback(`Error: ${e.message}`); + sendFeedback(`Error: ${e.message}`, room, userId); } }, }, @@ -1116,38 +1069,28 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: '[Dev Only] Remove a key from account data. Usage: /delacc ', exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~removeacc-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; const parts = payload.trim().split(/\s+/); if (parts.length < 2) { - sendFeedback('Usage: /delacc '); + sendFeedback('Usage: /delacc ', room, userId); return; } const [type, key] = parts; try { const existingEvent = mx.getAccountData(type as any); if (!existingEvent) { - sendFeedback(`No account data found for type "${type}".`); + sendFeedback(`No account data found for type "${type}".`, room, userId); return; } const content = { ...existingEvent.getContent() }; if (!(key in content)) { - sendFeedback(`Key "${key}" not found in "${type}".`); + sendFeedback(`Key "${key}" not found in "${type}".`, room, userId); return; } delete content[key]; await mx.setAccountData(type as any, content as any); - sendFeedback(`Key "${key}" removed from "${type}".`); + sendFeedback(`Key "${key}" removed from "${type}".`, room, userId); } catch (e: any) { - sendFeedback(`Error: ${e.message}`); + sendFeedback(`Error: ${e.message}`, room, userId); } }, }, @@ -1156,23 +1099,13 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: '[Dev Only] Set an extended profile property. Usage: /setext ', exe: async (payload) => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~setext-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; if (!developerTools) { - sendFeedback('Command available in Developer Mode only.'); + sendFeedback('Command available in Developer Mode only.', room, userId); return; } const parts = payload.trim().split(/\s+/); if (parts.length < 2) { - sendFeedback('Usage: /setext '); + sendFeedback('Usage: /setext ', room, userId); return; } const key = parts[0]; @@ -1184,12 +1117,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { try { if (typeof mx.setExtendedProfileProperty === 'function') { await mx.setExtendedProfileProperty(key, finalValue); - sendFeedback(`Extended profile property "${key}" set to: ${finalValue}`); + sendFeedback( + `Extended profile property "${key}" set to: ${finalValue}`, + room, + userId + ); } else { - sendFeedback('Error: setExtendedProfileProperty is not supported.'); + sendFeedback('Error: setExtendedProfileProperty is not supported.', room, userId); } } catch (e: any) { - sendFeedback(`Failed to set extended profile: ${e.message}`); + sendFeedback(`Failed to set extended profile: ${e.message}`, room, userId); } }, }, @@ -1200,36 +1137,25 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const userId = mx.getSafeUserId(); const key = payload.trim(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~removeext-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; - if (!developerTools) { - sendFeedback('Command available in Developer Mode only.'); + sendFeedback('Command available in Developer Mode only.', room, userId); return; } if (!key) { - sendFeedback('Usage: /delext '); + sendFeedback('Usage: /delext ', room, userId); return; } try { if (typeof mx.deleteExtendedProfileProperty === 'function') { await mx.deleteExtendedProfileProperty(key); - sendFeedback(`Extended profile property "${key}" removed.`); + sendFeedback(`Extended profile property "${key}" removed.`, room, userId); } else { - sendFeedback('Error: setExtendedProfileProperty is not supported.'); + sendFeedback('Error: setExtendedProfileProperty is not supported.', room, userId); } } catch (e: any) { - sendFeedback(`Failed to remove property: ${e.message}`); + sendFeedback(`Failed to remove property: ${e.message}`, room, userId); } }, }, @@ -1238,27 +1164,42 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: 'Force discard the current outbound E2EE session in this room.', exe: async () => { const userId = mx.getSafeUserId(); - const sendFeedback = (msg: string) => { - const localNotice = new MatrixEvent({ - type: 'm.room.message', - content: { msgtype: 'm.notice', body: msg }, - event_id: `~discard-${Date.now()}`, - room_id: room.roomId, - sender: userId, - }); - room.addLiveEvents([localNotice], { duplicateStrategy: 'ignore' } as any); - }; try { const crypto = mx.getCrypto(); if (!crypto) { - sendFeedback('Encryption is not enabled on this client.'); + sendFeedback('Encryption is not enabled on this client.', room, userId); return; } await crypto.forceDiscardSession(room.roomId); - sendFeedback('Outbound encryption session discarded.'); + sendFeedback('Outbound encryption session discarded.', room, userId); + } catch (e: any) { + sendFeedback(`Failed to discard session: ${e.message}`, room, userId); + } + }, + }, + // Sharing E2EE History of a room with a user + [Command.ShareE2EEHistory]: { + name: Command.ShareE2EEHistory, + description: + 'Share E2EE history of this room with a user. Example: /sharee2eehistory @user:example.org', + exe: async (payload) => { + const targetUserId = payload.trim(); + const { roomId } = room; + if (!targetUserId) { + sendFeedback('Usage: /sharee2eehistory @user:example.org', room, mx.getSafeUserId()); + return; + } + const crypto = mx.getCrypto(); + if (!crypto) { + sendFeedback('Encryption is not enabled on this client.', room, mx.getSafeUserId()); + return; + } + try { + await crypto.shareRoomHistoryWithUser(roomId, targetUserId); + sendFeedback(`E2EE history shared with ${targetUserId}.`, room, mx.getSafeUserId()); } catch (e: any) { - sendFeedback(`Failed to discard session: ${e.message}`); + sendFeedback(`Failed to share E2EE history: ${e.message}`, room, mx.getSafeUserId()); } }, }, From d2a05f280ead8cac0bf4839d9cdbbb38034bd179 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 15:48:55 +0100 Subject: [PATCH 3/6] feat: update ShareE2EEHistory command description to clarify limitations --- src/app/hooks/useCommands.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index c8ee9fb28..ba048e1f6 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -1182,7 +1182,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { [Command.ShareE2EEHistory]: { name: Command.ShareE2EEHistory, description: - 'Share E2EE history of this room with a user. Example: /sharee2eehistory @user:example.org', + 'Share E2EE history (MSC4268) of this room with a user. Example: /sharee2eehistory @user:example.org', exe: async (payload) => { const targetUserId = payload.trim(); const { roomId } = room; @@ -1197,7 +1197,11 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { } try { await crypto.shareRoomHistoryWithUser(roomId, targetUserId); - sendFeedback(`E2EE history shared with ${targetUserId}.`, room, mx.getSafeUserId()); + sendFeedback( + `E2EE history shared with ${targetUserId}. (Their client needs to support MSC4268)`, + room, + mx.getSafeUserId() + ); } catch (e: any) { sendFeedback(`Failed to share E2EE history: ${e.message}`, room, mx.getSafeUserId()); } From 2c808046aba499ac71cf7f40682d9feedb306575 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 15:59:47 +0100 Subject: [PATCH 4/6] refactor: convert shareRoomHistoryWithUser to promise-based handling for improved error management --- src/app/hooks/useCommands.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index ba048e1f6..ef09847be 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -1195,16 +1195,18 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { sendFeedback('Encryption is not enabled on this client.', room, mx.getSafeUserId()); return; } - try { - await crypto.shareRoomHistoryWithUser(roomId, targetUserId); - sendFeedback( - `E2EE history shared with ${targetUserId}. (Their client needs to support MSC4268)`, - room, - mx.getSafeUserId() - ); - } catch (e: any) { - sendFeedback(`Failed to share E2EE history: ${e.message}`, room, mx.getSafeUserId()); - } + crypto + .shareRoomHistoryWithUser(roomId, targetUserId) + .then(() => { + sendFeedback( + `E2EE history shared with ${targetUserId}. (Their client needs to support MSC4268)`, + room, + mx.getSafeUserId() + ); + }) + .catch((e) => { + sendFeedback(`Failed to share E2EE history: ${e.message}`, room, mx.getSafeUserId()); + }); }, }, // Cute Events From 210d34030a15cc09ae52754acafb1e7bfab2a91a Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 16:07:18 +0100 Subject: [PATCH 5/6] add changeset for my pr relating the addition of `/sharehistory` --- .changeset/add_command_for_msc4268_e2ee_history_sharing.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add_command_for_msc4268_e2ee_history_sharing.md diff --git a/.changeset/add_command_for_msc4268_e2ee_history_sharing.md b/.changeset/add_command_for_msc4268_e2ee_history_sharing.md new file mode 100644 index 000000000..5ef3a5f50 --- /dev/null +++ b/.changeset/add_command_for_msc4268_e2ee_history_sharing.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +added a `/sharehistory` command to [share encrypted history with a user](https://github.com/matrix-org/matrix-spec-proposals/blob/rav/proposal/encrypted_history_sharing/proposals/4268-encrypted-history-sharing.md) From 8802476670f8e32374162f3a1a1323ad077f26aa Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 16 Mar 2026 20:21:53 +0100 Subject: [PATCH 6/6] added settings toggle --- .../settings/experimental/Experimental.tsx | 2 + .../experimental/MSC4268HistoryShare.tsx | 42 +++++++++++++++++++ src/app/hooks/useCommands.ts | 20 ++++++++- src/app/state/settings.ts | 3 ++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/app/features/settings/experimental/MSC4268HistoryShare.tsx diff --git a/src/app/features/settings/experimental/Experimental.tsx b/src/app/features/settings/experimental/Experimental.tsx index 1760bc9c4..5516afbfd 100644 --- a/src/app/features/settings/experimental/Experimental.tsx +++ b/src/app/features/settings/experimental/Experimental.tsx @@ -4,6 +4,7 @@ import { InfoCard } from '$components/info-card'; import { LanguageSpecificPronouns } from '../cosmetics/LanguageSpecificPronouns'; import { Sync } from '../general'; import { BandwidthSavingEmojis } from './BandwithSavingEmojis'; +import { MSC4268HistoryShare } from './MSC4268HistoryShare'; type ExperimentalProps = { requestClose: () => void; @@ -43,6 +44,7 @@ export function Experimental({ requestClose }: ExperimentalProps) {
+ diff --git a/src/app/features/settings/experimental/MSC4268HistoryShare.tsx b/src/app/features/settings/experimental/MSC4268HistoryShare.tsx new file mode 100644 index 000000000..d93a41031 --- /dev/null +++ b/src/app/features/settings/experimental/MSC4268HistoryShare.tsx @@ -0,0 +1,42 @@ +import { SequenceCard } from '$components/sequence-card'; +import { SettingTile } from '$components/setting-tile'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { Box, Switch, Text } from 'folds'; +import { SequenceCardStyle } from '../styles.css'; + +export function MSC4268HistoryShare() { + const [enabledMSC4268Command, setEnabledMSC4268Command] = useSetting( + settingsAtom, + 'enableMSC4268CMD' + ); + + return ( + + Enable Sharing of Encrypted History + + + } + /> + + + ); +} diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index ef09847be..b74daf70b 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -266,6 +266,7 @@ export type CommandRecord = Record; export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const { navigateRoom } = useRoomNavigate(); const [developerTools] = useSetting(settingsAtom, 'developerTools'); + const [enableMSC4268CMD] = useSetting(settingsAtom, 'enableMSC4268CMD'); const profile = useUserProfile(mx.getSafeUserId()); const openBugReport = useOpenBugReportModal(); @@ -1186,6 +1187,14 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { exe: async (payload) => { const targetUserId = payload.trim(); const { roomId } = room; + if (!enableMSC4268CMD) { + sendFeedback( + 'This command is disabled. Enable it under experimental settings to use it.', + room, + mx.getSafeUserId() + ); + return; + } if (!targetUserId) { sendFeedback('Usage: /sharee2eehistory @user:example.org', room, mx.getSafeUserId()); return; @@ -1296,7 +1305,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { }, }, }), - [mx, navigateRoom, room, profile.displayName, profile.avatarUrl, developerTools, openBugReport] + [ + mx, + navigateRoom, + room, + profile.displayName, + profile.avatarUrl, + developerTools, + enableMSC4268CMD, + openBugReport, + ] ); return commands; diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index a5e373a1e..ee09b3e21 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -64,6 +64,7 @@ export interface Settings { dateFormatString: string; developerTools: boolean; + enableMSC4268CMD: boolean; // Cosmetics! jumboEmojiSize: JumboEmojiSize; @@ -129,6 +130,8 @@ const defaultSettings: Settings = { legacyUsernameColor: false, allowPipVideos: false, + enableMSC4268CMD: false, + // Push notifications (SW/Sygnal): default on for mobile, opt-in on desktop. // In-app pill banner: default on for mobile (primary foreground alert), opt-in on desktop. // System (OS) notifications: desktop-only; hidden and disabled on mobile.