/* Copyright 2024 New Vector Ltd. Copyright 2021 Robin Townsend SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { useEffect, useMemo, useState } from "react"; import classnames from "classnames"; import { IContent, MatrixEvent, Room, RoomMember, EventType, MatrixClient, ContentHelpers, ILocationContent, LocationAssetType, M_TIMESTAMP, M_BEACON, TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; // eslint-disable-next-line no-restricted-imports import OverflowHorizontalSvg from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import { useSettingValue } from "../../../hooks/useSettings"; import { Layout } from "../../../settings/enums/Layout"; import BaseDialog from "./BaseDialog"; import { avatarUrlForUser } from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { isLocationEvent } from "../../../utils/EventUtils"; import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; import { RoomContextDetails } from "../rooms/RoomContextDetails"; import { filterBoolean } from "../../../utils/arrays"; import { IState, RovingTabIndexContext, RovingTabIndexProvider, Type, useRovingTabIndex, } from "../../../accessibility/RovingTabIndex"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; const AVATAR_SIZE = 30; interface IProps { matrixClient: MatrixClient; // The event to forward event: MatrixEvent; // We need a permalink creator for the source room to pass through to EventTile // in case the event is a reply (even though the user can't get at the link) permalinkCreator: RoomPermalinkCreator; onFinished(): void; } interface IEntryProps { room: Room; type: K; content: TimelineEvents[K]; matrixClient: MatrixClient; onFinished(success: boolean): void; } enum SendState { CanSend, Sending, Sent, Failed, } const Entry: React.FC> = ({ room, type, content, matrixClient: cli, onFinished }) => { const [sendState, setSendState] = useState(SendState.CanSend); const [onFocus, isActive, ref] = useRovingTabIndex(); const jumpToRoom = (ev: ButtonEvent): void => { dis.dispatch({ action: Action.ViewRoom, room_id: room.roomId, metricsTrigger: "WebForwardShortcut", metricsViaKeyboard: ev.type !== "click", }); onFinished(true); }; const send = async (): Promise => { setSendState(SendState.Sending); try { await cli.sendEvent(room.roomId, type, content); setSendState(SendState.Sent); } catch { setSendState(SendState.Failed); } }; let className; let disabled = false; let title; let icon; if (sendState === SendState.CanSend) { className = "mx_ForwardList_canSend"; if (!room.maySendMessage()) { disabled = true; title = _t("forward|no_perms_title"); } } else if (sendState === SendState.Sending) { className = "mx_ForwardList_sending"; disabled = true; title = _t("forward|sending"); icon =
; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("forward|sent"); icon =
; } else { className = "mx_ForwardList_sendFailed"; disabled = true; title = _t("timeline|send_state_failed"); icon = ; } const id = `mx_ForwardDialog_entry_${room.roomId}`; return (
{room.name}
{_t("forward|send_label")}
{icon}
); }; const transformEvent = (event: MatrixEvent): { type: string; content: IContent } => { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars "m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event // We're taking a shallow copy here to avoid https://github.com/vector-im/element-web/issues/10924 ...content } = event.getContent(); // beacon pulses get transformed into static locations on forward const type = M_BEACON.matches(event.getType()) ? EventType.RoomMessage : event.getType(); // self location shares should have their description removed // and become 'pin' share type if ( (isLocationEvent(event) && isSelfLocation(content as ILocationContent)) || // beacon pulses get transformed into static locations on forward M_BEACON.matches(event.getType()) ) { const timestamp = M_TIMESTAMP.findIn(content); const geoUri = locationEventGeoUri(event); return { type, content: { ...content, ...ContentHelpers.makeLocationContent( undefined, // text geoUri, timestamp || Date.now(), undefined, // description LocationAssetType.Pin, ), }, }; } return { type, content }; }; const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => { const userId = cli.getSafeUserId(); const [profileInfo, setProfileInfo] = useState({}); useEffect(() => { cli.getProfileInfo(userId).then((info) => setProfileInfo(info)); }, [cli, userId]); const { type, content } = transformEvent(event); // For the message preview we fake the sender as ourselves const mockEvent = new MatrixEvent({ type: "m.room.message", sender: userId, content, unsigned: { age: 97, }, event_id: "$9999999999999999999999999999999999999999999", room_id: event.getRoomId(), origin_server_ts: event.getTs(), }); mockEvent.sender = { name: profileInfo.displayname || userId, rawDisplayName: profileInfo.displayname, userId, getAvatarUrl: (..._) => { return avatarUrlForUser({ avatarUrl: profileInfo.avatar_url }, AVATAR_SIZE, AVATAR_SIZE, "crop"); }, getMxcAvatarUrl: () => profileInfo.avatar_url, } as RoomMember; const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); const previewLayout = useSettingValue("layout"); const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors"); let rooms = useMemo( () => sortRooms( cli .getVisibleRooms(msc3946DynamicRoomPredecessors) .filter((room) => room.getMyMembership() === KnownMembership.Join && !room.isSpaceRoom()), ), [cli, msc3946DynamicRoomPredecessors], ); if (lcQuery) { rooms = new QueryMatcher(rooms, { keys: ["name"], funcs: [(r) => filterBoolean([r.getCanonicalAlias(), ...r.getAltAliases()])], shouldMatchWordsOnly: false, }).match(lcQuery); } const [truncateAt, setTruncateAt] = useState(20); function overflowTile(overflowCount: number, totalCount: number): JSX.Element { const text = _t("common|and_n_others", { count: overflowCount }); return ( } name={text} showPresence={false} onClick={() => setTruncateAt(totalCount)} /> ); } const onKeyDown = (ev: React.KeyboardEvent, state: IState): void => { let handled = true; const action = getKeyBindingsManager().getAccessibilityAction(ev); switch (action) { case KeyBindingAction.Enter: { state.activeNode?.querySelector(".mx_ForwardList_sendButton")?.click(); break; } default: handled = false; } if (handled) { ev.preventDefault(); ev.stopPropagation(); } }; return (

{_t("forward|message_preview_heading")}


{({ onKeyDownHandler }) => (
{(context) => ( { setQuery(query); setTimeout(() => { const node = context.state.nodes[0]; if (node) { context.dispatch({ type: Type.SetFocus, payload: { node }, }); node?.scrollIntoView?.({ block: "nearest", }); } }); }} autoFocus={true} onKeyDown={onKeyDownHandler} aria-activedescendant={context.state.activeNode?.id} aria-owns="mx_ForwardDialog_resultsList" /> )} {rooms.length > 0 ? (
rooms .slice(start, end) .map((room) => ( )) } getChildCount={() => rooms.length} />
) : ( {_t("common|no_results")} )}
)}
); }; export default ForwardDialog;