import React, { ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Avatar, Box, Button, Checkbox, 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 { buildForwardContent } from './forwardContent'; type RoomRowProps = { room: Room; dm: boolean; useAuthentication: boolean; selected: boolean; onToggle: () => void; sending: boolean; }; function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: RoomRowProps) { const mx = useMatrixClient(); const avatarMxc = room.getMxcAvatarUrl(); const avatarUrl = avatarMxc ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined) : undefined; return ( { evt.stopPropagation(); onToggle(); }} /> } before={ ( )} /> } > {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); // Selection persists across query changes: a room selected then filtered out // of the rendered slice stays selected. const [selectedRoomIds, setSelectedRoomIds] = useState>(new Set()); const toggleRoom = useCallback((roomId: string) => { setSelectedRoomIds((prev) => { const next = new Set(prev); if (next.has(roomId)) { next.delete(roomId); } else { next.add(roomId); } return next; }); }, []); 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]); const sendToSelected = useCallback(async () => { if (sending || selectedRoomIds.size === 0) return; const fwdContent = buildForwardContent(mx, mEvent); if (!fwdContent) { setError('This message could not be decrypted, so it cannot be forwarded.'); return; } setSending(true); setError(null); const ids = [...selectedRoomIds]; const results = await Promise.allSettled( // threadId-aware overload (P3-8): explicit null = send to the main timeline. // eslint-disable-next-line @typescript-eslint/no-explicit-any ids.map((id) => mx.sendEvent(id, null, mEvent.getType() as any, fwdContent)), ); const failedIds: string[] = []; const failedNames: string[] = []; results.forEach((result, i) => { if (result.status === 'rejected') { failedIds.push(ids[i]); failedNames.push(mx.getRoom(ids[i])?.name ?? ids[i]); } }); const total = ids.length; const failed = failedNames.length; const succeeded = total - failed; if (failed === 0) { setSentTo(`Forwarded to ${total} ${total === 1 ? 'room' : 'rooms'}`); setTimeout(onClose, 1400); return; } setSending(false); // Prune to only the failures so a retry doesn't re-send to rooms that // already succeeded (duplicate messages). setSelectedRoomIds(new Set(failedIds)); if (succeeded === 0) { setError('Failed to forward. Try again.'); return; } setError(`Forwarded to ${succeeded}/${total}. Failed: ${failedNames.join(', ')}.`); }, [mx, mEvent, onClose, sending, selectedRoomIds]); return ( }> searchInputRef.current ?? false, onDeactivate: onClose, clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} >
Forward message
{!sentTo && ( ) => setQuery(e.target.value)} /> {error && ( {error} )} )} {sentTo ? ( ✓ {sentTo} ) : ( <> {filtered.slice(0, 60).map((room) => ( toggleRoom(room.roomId)} sending={sending} /> ))} {filtered.length === 0 && ( No rooms found )} {sending && ( )} )}
); }