import React, { ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Avatar, Box, color, config, Header, Icon, IconButton, Icons, Input, Line, MenuItem, Modal, Overlay, OverlayBackdrop, OverlayCenter, Scroll, Spinner, Text, } from 'folds'; import { MatrixEvent, Room } from 'matrix-js-sdk'; import { useAtomValue } from 'jotai'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { stopPropagation } from '../../../utils/keyboard'; import { useModalStyle } from '../../../hooks/useModalStyle'; import { mDirectAtom } from '../../../state/mDirectList'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; import { getEditedEvent, trimReplyFromBody, trimReplyFromFormattedBody } from '../../../utils/room'; type RoomRowProps = { room: Room; dm: boolean; useAuthentication: boolean; onClick: () => void; sending: boolean; }; function RoomRow({ room, dm, useAuthentication, onClick, sending }: RoomRowProps) { const mx = useMatrixClient(); const avatarMxc = room.getMxcAvatarUrl(); const avatarUrl = avatarMxc ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined) : undefined; return ( ( )} /> } > {room.name} {dm && ( Direct Message )} ); } type Props = { mEvent: MatrixEvent; onClose: () => void; }; export function ForwardMessageDialog({ mEvent, onClose }: Props) { const mx = useMatrixClient(); const modalStyle = useModalStyle(400); const directs = useAtomValue(mDirectAtom); const useAuthentication = useMediaAuthentication(); const searchInputRef = useRef(null); const [query, setQuery] = useState(''); const [sending, setSending] = useState(false); const [sentTo, setSentTo] = useState(null); const [error, setError] = useState(null); const allRooms = useMemo( () => mx .getRooms() .filter((r) => r.getMyMembership() === 'join' && !r.isSpaceRoom()) .sort((a, b) => (b.getLastActiveTimestamp() ?? 0) - (a.getLastActiveTimestamp() ?? 0)), [mx], ); const filtered = useMemo(() => { if (!query) return allRooms; const q = query.toLowerCase(); return allRooms.filter((r) => r.name.toLowerCase().includes(q)); }, [allRooms, query]); /** * Build the content to forward: * - undecryptable events are refused (would forward `m.bad.encrypted` junk) * - edited messages forward the LATEST edit (`m.new_content`), not the * original pre-edit body * - reply fallbacks (`> <@user> …` quote + `` block) are stripped * along with the `m.relates_to` reply/thread relation, so the forwarded * message stands alone in the target room */ const buildForwardContent = useCallback((): Record | undefined => { if (mEvent.isDecryptionFailure()) return undefined; let content = { ...mEvent.getContent() }; const eventId = mEvent.getId(); const room = mx.getRoom(mEvent.getRoomId()); if (eventId && room) { const editedEvent = getEditedEvent(eventId, mEvent, room.getUnfilteredTimelineSet()); const newContent = editedEvent?.getContent()['m.new_content']; if (newContent && typeof newContent === 'object') { content = { ...(newContent as Record) }; } } delete content['m.relates_to']; if (typeof content.body === 'string') { content.body = trimReplyFromBody(content.body); } if (typeof content.formatted_body === 'string') { content.formatted_body = trimReplyFromFormattedBody(content.formatted_body); } return content; }, [mx, mEvent]); const forward = useCallback( async (room: Room) => { if (sending) return; const fwdContent = buildForwardContent(); if (!fwdContent) { setError('This message could not be decrypted, so it cannot be forwarded.'); return; } setSending(true); setError(null); try { // threadId-aware overload (P3-8): explicit null = send to the main timeline. // eslint-disable-next-line @typescript-eslint/no-explicit-any await mx.sendEvent(room.roomId, null, mEvent.getType() as any, fwdContent); setSentTo(room.name); setTimeout(onClose, 1400); } catch { setSending(false); setError(`Failed to forward to ${room.name}. Try again.`); } }, [mx, mEvent, onClose, sending, buildForwardContent], ); return ( }> searchInputRef.current ?? false, onDeactivate: onClose, clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} >
Forward message
{!sentTo && ( ) => setQuery(e.target.value)} /> {error && ( {error} )} )} {sentTo ? ( ✓ Forwarded to {sentTo} ) : ( {filtered.slice(0, 60).map((room) => ( forward(room)} sending={sending} /> ))} {filtered.length === 0 && ( No rooms found )} {sending && ( )} )}
); }