Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 138 additions & 1 deletion src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
BlockType,
} from '$components/editor';
import { plainToEditorInput } from '$components/editor/input';
import { htmlToMarkdown } from '$plugins/markdown';
import { EmojiBoard, EmojiBoardTab } from '$components/emoji-board';
import { UseStateProvider } from '$components/UseStateProvider';
import type { TUploadContent } from '$utils/matrix';
Expand All @@ -80,6 +81,7 @@ import {
roomIdToReplyDraftAtomFamily,
roomIdToUploadItemsAtomFamily,
roomUploadAtomFamily,
roomIdToEditDraftAtomFamily,
} from '$state/room/roomInputDrafts';
import { UploadCardRenderer } from '$components/upload-card';
import type { UploadBoardImperativeHandlers } from '$components/upload-board';
Expand All @@ -91,7 +93,7 @@ import { safeFile } from '$utils/mimeTypes';
import { fulfilledPromiseSettledResult } from '$utils/common';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { getMentionContent, isThreadRelationEvent, reactionOrEditEvent } from '$utils/room';
import { getMentionContent, isThreadRelationEvent, reactionOrEditEvent, getEditedEvent } from '$utils/room';
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '$hooks/useCommands';
import { mobileOrTablet } from '$utils/user-agent';
import { useElementSizeObserver } from '$hooks/useElementSizeObserver';
Expand Down Expand Up @@ -294,6 +296,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(

const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(draftKey));
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(draftKey));
const [editDraft, setEditDraft] = useAtom(roomIdToEditDraftAtomFamily(draftKey));

const [uploadBoard, setUploadBoard] = useState(true);
const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(draftKey));
Expand Down Expand Up @@ -458,6 +461,45 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}
}, [replyDraft?.eventId, editor]);

const prevEditEventId = useRef(editDraft?.eventId);
useEffect(() => {
if (editDraft?.eventId === prevEditEventId.current) return;
prevEditEventId.current = editDraft?.eventId;

if (!editDraft) {
// Edit was cancelled — editor was already reset by the cancel handler
return;
}

const editEvent = room.findEventById(editDraft.eventId);
if (!editEvent) return;

const evtId = editEvent.getId();
const evtTimeline = evtId ? room.getTimelineForEvent(evtId) : undefined;
const editedVersion =
evtTimeline && evtId
? getEditedEvent(evtId, editEvent, evtTimeline.getTimelineSet())
: undefined;
const content = editedVersion?.getContent()['m.new_content'] ?? editEvent.getContent();
const body = typeof content.body === 'string' ? content.body : '';
const formattedBody =
typeof content.formatted_body === 'string' ? content.formatted_body : undefined;

const initialValue = plainToEditorInput(formattedBody ? htmlToMarkdown(formattedBody) : body);

resetEditor(editor);
resetEditorHistory(editor);
Transforms.insertFragment(editor, initialValue);
requestAnimationFrame(() => {
try {
ReactEditor.focus(editor);
moveCursor(editor);
} catch {
// ignore focus errors
}
});
}, [editDraft, editor, room]);

const handleFileMetadata = useCallback(
(fileItem: TUploadItem, metadata: TUploadMetadata) => {
setSelectedFiles({
Expand Down Expand Up @@ -827,6 +869,53 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(

if (plainText === '') return;

// Discord-style edit: when an editDraft is active, send an m.replace event
// instead of a new message and clear the edit state.
if (editDraft) {
const editEvent = room.findEventById(editDraft.eventId);
if (editEvent) {
const oldContent = editEvent.getContent();
const msgtype = (oldContent.msgtype as string) ?? MsgType.Text;

const newContent: IContent = { msgtype, body: plainText };
if (!customHtmlEqualsPlainText(customHtml, plainText)) {
newContent.format = 'org.matrix.custom.html';
newContent.formatted_body = customHtml;
}
const mentionData = getMentions(mx, roomId, editor);
newContent['m.mentions'] = getMentionContent(
Array.from(mentionData.users),
mentionData.room
);

const sendContent: IContent = {
...oldContent,
'm.relates_to': {
event_id: editDraft.eventId,
rel_type: RelationType.Replace,
},
body: `* ${plainText}`,
'm.new_content': newContent,
'm.mentions': newContent['m.mentions'],
};
if (newContent.format) {
sendContent.format = newContent.format;
sendContent.formatted_body = `* ${newContent.formatted_body as string}`;
}

resetEditor(editor);
resetEditorHistory(editor);
setInputKey((prev) => prev + 1);
setEditDraft(undefined);
sendTypingStatus(false);

mx.sendMessage(roomId, sendContent as RoomMessageEventContent).catch((error: unknown) => {
log.error('failed to send edit', { roomId }, error);
});
}
return;
}

// PluralKit-style proxy wrappers (per-message profile proxies) must be stripped
// *before* building `content`, otherwise we end up sending the wrapper verbatim.
let proxiedPerMessageProfile:
Expand Down Expand Up @@ -1079,6 +1168,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
isEncrypted,
setEditingScheduledDelayId,
setScheduledTime,
editDraft,
setEditDraft,
setServerMaxDelayMs,
]);

Expand Down Expand Up @@ -1145,6 +1236,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
setAutocompleteQuery(undefined);
return;
}
if (editDraft) {
setEditDraft(undefined);
resetEditor(editor);
resetEditorHistory(editor);
return;
}
setReplyDraft(undefined);
}
},
Expand All @@ -1158,6 +1255,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
showAudioRecorder,
editor,
onEditLastMessage,
editDraft,
setEditDraft,
]
);

Expand Down Expand Up @@ -1429,6 +1528,44 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</Box>
</div>
)}
{editDraft && (
<div>
<Box
alignItems="Center"
gap="300"
style={{
padding: `${config.space.S200} ${config.space.S300} 0`,
}}
>
<IconButton
onClick={() => {
setEditDraft(undefined);
resetEditor(editor);
resetEditorHistory(editor);
}}
variant="SurfaceVariant"
size="300"
radii="300"
aria-label="Cancel edit"
title="Cancel edit"
>
<Icon src={Icons.Cross} size="50" />
</IconButton>
<Box
direction="Row"
gap="200"
alignItems="Center"
grow="Yes"
style={{ minWidth: 0 }}
>
<Icon size="100" src={Icons.Pencil} />
<Text size="T300" truncate>
Editing message
</Text>
</Box>
</Box>
</div>
)}
{sendError && (
<div>
<Box
Expand Down
27 changes: 24 additions & 3 deletions src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ import { useRoomAbbreviationsContext } from '$hooks/useRoomAbbreviations';
import { buildAbbrReplaceTextNode } from '$components/message/RenderBody';
import { profilesCacheAtom } from '$state/userRoomProfile';
import { roomToParentsAtom } from '$state/room/roomToParents';
import { roomIdToReplyDraftAtomFamily } from '$state/room/roomInputDrafts';
import {
roomIdToReplyDraftAtomFamily,
roomIdToEditDraftAtomFamily,
} from '$state/room/roomInputDrafts';
import { roomIdToOpenThreadAtomFamily } from '$state/room/roomToOpenThread';
import {
getRoomUnreadInfo,
Expand Down Expand Up @@ -136,6 +139,19 @@ export function RoomTimeline({
const { editId, handleEdit } = useMessageEdit(editor, { onReset: onEditorReset, alive });
const { navigateRoom } = useRoomNavigate();

const [editInInput] = useSetting(settingsAtom, 'editInInput');
const setEditDraft = useSetAtom(roomIdToEditDraftAtomFamily(room.roomId));
const handleEditCallback = useCallback(
(id?: string) => {
if (editInInput) {
setEditDraft(id ? { eventId: id } : undefined);
return;
}
handleEdit(id);
},
[editInInput, handleEdit, setEditDraft]
);

const [hideReads] = useSetting(settingsAtom, 'hideReads');
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
Expand Down Expand Up @@ -616,7 +632,12 @@ export function RoomTimeline({
hideNickAvatarEvents,
showHiddenEvents,
},
state: { focusItem: timelineSync.focusItem, editId, activeReplyId, openThreadId },
state: {
focusItem: timelineSync.focusItem,
editId: editInInput ? undefined : editId,
activeReplyId,
openThreadId,
},
permissions: {
canRedact: permissions.action('redact', mx.getSafeUserId()),
canDeleteOwn: permissions.event('m.room.redaction', mx.getSafeUserId()),
Expand All @@ -628,7 +649,7 @@ export function RoomTimeline({
onUsernameClick: actions.handleUsernameClick,
onReplyClick: actions.handleReplyClick,
onReactionToggle: actions.handleReactionToggle,
onEditId: actions.handleEdit,
onEditId: handleEditCallback,
onResend: actions.handleResend,
onDeleteFailedSend: actions.handleDeleteFailedSend,
setOpenThread: actions.setOpenThread,
Expand Down
26 changes: 23 additions & 3 deletions src/app/features/room/ThreadDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ import { useRoomCreators } from '$hooks/useRoomCreators';
import { useImagePackRooms } from '$hooks/useImagePackRooms';
import { useOpenUserRoomProfile } from '$state/hooks/userRoomProfile';
import type { IReplyDraft } from '$state/room/roomInputDrafts';
import { roomIdToReplyDraftAtomFamily } from '$state/room/roomInputDrafts';
import {
roomIdToReplyDraftAtomFamily,
roomIdToEditDraftAtomFamily,
} from '$state/room/roomInputDrafts';
import { roomToParentsAtom } from '$state/room/roomToParents';
import { useIgnoredUsers } from '$hooks/useIgnoredUsers';
import { useGetMemberPowerTag } from '$hooks/useMemberPowerTag';
Expand Down Expand Up @@ -124,6 +127,18 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra
const serverFetchAttemptedRef = useRef<string | null>(null);
const autoFillInProgressRef = useRef(false);
const { editId, handleEdit } = useMessageEdit(editor);
const [editInInput] = useSetting(settingsAtom, 'editInInput');
const setEditDraft = useSetAtom(roomIdToEditDraftAtomFamily(threadRootId));
const handleEditCallback = useCallback(
(id?: string) => {
if (editInInput) {
setEditDraft(id ? { eventId: id } : undefined);
return;
}
handleEdit(id);
},
[editInInput, handleEdit, setEditDraft]
);
const nicknames = useAtomValue(nicknamesAtom);
const pushProcessor = useMemo(() => new PushProcessor(mx), [mx]);
const useAuthentication = useMediaAuthentication();
Expand Down Expand Up @@ -711,7 +726,12 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra
showHiddenEvents,
hideThreadChip: true,
},
state: { focusItem, editId, activeReplyId, openThreadId: threadRootId },
state: {
focusItem,
editId: editInInput ? undefined : editId,
activeReplyId,
openThreadId: threadRootId,
},
permissions: {
canRedact,
canDeleteOwn,
Expand All @@ -723,7 +743,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra
onUsernameClick: handleUsernameClick,
onReplyClick: handleReplyClick,
onReactionToggle: handleReactionToggle,
onEditId: handleEdit,
onEditId: handleEditCallback,
onResend: handleResend,
onDeleteFailedSend: handleDeleteFailedSend,
setOpenThread: () => {},
Expand Down
31 changes: 31 additions & 0 deletions src/app/features/settings/experimental/EditInInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 EditInInput() {
const [editInInput, setEditInInput] = useSetting(settingsAtom, 'editInInput');

return (
<Box direction="Column" gap="100">
<Text size="L400">Discord-Style Message Editing</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
focusId="experimental-edit-in-input"
title="Edit messages in the composer"
description="When editing a message, load its content into the main text input instead of editing inline in the timeline. Cancel with Escape or the × button."
after={
<Switch
variant="Primary"
value={editInInput}
onChange={setEditInInput}
title={editInInput ? 'Disable edit in composer' : 'Enable edit in composer'}
/>
}
/>
</SequenceCard>
</Box>
);
}
2 changes: 2 additions & 0 deletions src/app/features/settings/experimental/Experimental.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Sync } from '../general';
import { SettingsSectionPage } from '../SettingsSectionPage';
import { BandwidthSavingEmojis } from './BandwithSavingEmojis';
import { MSC4268HistoryShare } from './MSC4268HistoryShare';
import { EditInInput } from './EditInInput';

function PersonaToggle() {
const [showPersonaSetting, setShowPersonaSetting] = useSetting(
Expand Down Expand Up @@ -59,6 +60,7 @@ export function Experimental({ requestBack, requestClose }: Readonly<Experimenta
<br />
<Box direction="Column" gap="700">
<Sync />
<EditInInput />
<MSC4268HistoryShare />
<BandwidthSavingEmojis />
<PersonaToggle />
Expand Down
9 changes: 9 additions & 0 deletions src/app/state/room/roomInputDrafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,12 @@ export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
export const roomIdToReplyDraftAtomFamily = atomFamily<string, TReplyDraftAtom>(() =>
createReplyDraftAtom()
);

export type IEditDraft = {
eventId: string;
};
const createEditDraftAtom = () => atom<IEditDraft | undefined>(undefined);
export type TEditDraftAtom = ReturnType<typeof createEditDraftAtom>;
export const roomIdToEditDraftAtomFamily = atomFamily<string, TEditDraftAtom>(() =>
createEditDraftAtom()
);
6 changes: 6 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export interface Settings {
vcmsgSidebarWidth: number;
widgetSidebarWidth: number;

// experimental
editInInput: boolean;

// furry stuff
renderAnimals: boolean;

Expand Down Expand Up @@ -301,6 +304,9 @@ export const defaultSettings: Settings = {
threadRootHeight: 220,
vcmsgSidebarWidth: 399,
widgetSidebarWidth: 420,

// experimental
editInInput: false,
// furry stuff
renderAnimals: true,

Expand Down
Loading