From 63ceae52ed22bcb372cb39a313d275a97db541b7 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 26 Aug 2025 15:25:54 +0100 Subject: [PATCH] Add context to invites. --- res/css/_components.pcss | 1 + res/css/views/rooms/_RoomPreviewContext.pcss | 8 + src/components/views/rooms/RoomPreviewBar.tsx | 4 + .../views/rooms/RoomPreviewContext.tsx | 156 ++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 res/css/views/rooms/_RoomPreviewContext.pcss create mode 100644 src/components/views/rooms/RoomPreviewContext.tsx diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 602885546e..504a2c1031 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -323,6 +323,7 @@ @import "./views/rooms/_RoomKnocksBar.pcss"; @import "./views/rooms/_RoomPreviewBar.pcss"; @import "./views/rooms/_RoomPreviewCard.pcss"; +@import "./views/rooms/_RoomPreviewContext.pcss"; @import "./views/rooms/_RoomSearchAuxPanel.pcss"; @import "./views/rooms/_RoomSublist.pcss"; @import "./views/rooms/_RoomTile.pcss"; diff --git a/res/css/views/rooms/_RoomPreviewContext.pcss b/res/css/views/rooms/_RoomPreviewContext.pcss new file mode 100644 index 0000000000..1067eec3ef --- /dev/null +++ b/res/css/views/rooms/_RoomPreviewContext.pcss @@ -0,0 +1,8 @@ +.mx_RoomPreviewContext { + // TODO: FIX + min-width: 400px; + > li { + margin-bottom: 1em; + list-style: none; + } +} \ No newline at end of file diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index b5c7e08154..599c53527a 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -32,6 +32,7 @@ import { ModuleRunner } from "../../../modules/ModuleRunner"; import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg"; import Field from "../elements/Field"; import ModuleApi from "../../../modules/Api.ts"; +import { RoomPreviewContext } from "./RoomPreviewContext.tsx"; const MemberEventHtmlReasonField = "io.element.html_reason"; @@ -317,6 +318,7 @@ class RoomPreviewBar extends React.Component { let title: string | undefined; let subTitle: string | ReactNode[] | undefined; let reasonElement: JSX.Element | undefined; + let inviteContext: JSX.Element | undefined; let primaryActionHandler: (() => void) | undefined; let primaryActionLabel: string | undefined; let secondaryActionHandler: (() => void) | undefined; @@ -557,6 +559,7 @@ class RoomPreviewBar extends React.Component { /> ); } + inviteContext = primaryActionHandler = this.props.onJoinClick; secondaryActionLabel = _t("action|decline"); @@ -736,6 +739,7 @@ class RoomPreviewBar extends React.Component { {subTitleElements} {reasonElement} + {inviteContext}
= ({inviterMember}) => { + const client = useMatrixClientContext(); + const [joinedTo, setJoinedTo] = useState<{title: string, description: string, type: "info"|"success"}|null>(); + const [roomCount, setRoomCount] = useState(); + + useEffect(() => { + if (!inviterMember?.userId) { + return; + } + + (async () => { + let rooms; + try { + rooms = await client._unstable_getSharedRooms(inviterMember.userId); + } catch (ex) { + // Could not fetch rooms. + // TODO: Handle error. + return; + } + const joinedToPrivateSpaces = new Map(); + const joinedToPrivateRooms = new Map(); + const joinedToPublicSpaces = new Map(); + const joinedToPublicRooms = new Map(); + for (const roomId of rooms) { + const room = client.getRoom(roomId); + if (!room) { + continue; + } + if (room.isSpaceRoom()) { + if (PRIVATE_JOIN_RULES.includes(room.getJoinRule())) { + joinedToPrivateSpaces.set(room.name, room.getMembers().length); + } else { + joinedToPublicSpaces.set(room.name, room.getMembers().length); + } + } else { + if (PRIVATE_JOIN_RULES.includes(room.getJoinRule())) { + joinedToPrivateRooms.set(room.name, room.getMembers().length); + } else { + joinedToPublicRooms.set(room.name, room.getMembers().length); + } + } + } + + for (const [roomSet, type] of ([[joinedToPrivateSpaces, "private spaces"], [joinedToPrivateRooms, "private rooms"], [joinedToPublicSpaces, "spaces"], [joinedToPublicRooms, "rooms"]] as [Map, string][])) { + if (roomSet.size === 0) { + continue; + } + const roomNames = [...roomSet].sort(([,memberCountA], [,memberCountB]) => memberCountB - memberCountA).slice(0,3).map(([name]) => name).join(', '); + if (roomNames) { + setJoinedTo({description: `You share ${roomSet.size} ${type}, including ${roomNames}`, title: `You share ${type}`, type: type === "private spaces" ? "success" : "info"}); + } else { + setJoinedTo({description: `You share ${roomSet.size} ${type}`, title: `You share ${type}`, type: type === "private spaces" ? "success" : "info"}); + } + break; + } + setRoomCount(rooms.filter(r => r !== inviterMember.roomId).length); + })(); + + return () => { + setRoomCount(null); + } + },[client, inviterMember]); + + const userBanned = useMemo(() => { + if (!inviterMember?.userId) { + return null; + } + const bannedRooms = client.getRooms().map<[Room, RoomMember|null]>((r) => [r ,r.getMember(inviterMember?.userId)]).filter(([room, member]) => member?.membership === KnownMembership.Ban); + if (bannedRooms.length) { + const exampleNames = bannedRooms.filter(([room]) => room.normalizedName && room.normalizedName !== room.roomId).slice(0,3).map(([room]) => room.normalizedName).join(', '); + if (exampleNames) { + return `User has been banned from ${bannedRooms.length} rooms, including ${exampleNames}`; + } + return `User has been banned from ${bannedRooms.length} rooms`; + } + return null; + }, [client, inviterMember]); + + const userKicked = useMemo(() => { + if (!inviterMember?.userId) { + return null; + } + const kickedRooms = client.getRooms().map<[Room, RoomMember|null]>((r) => [r ,r.getMember(inviterMember?.userId)]).filter(([room, member]) => member?.isKicked()); + if (kickedRooms.length) { + const exampleNames = kickedRooms.filter(([room]) => room.normalizedName && room.normalizedName !== room.roomId).slice(0,3).map(([room]) => room.normalizedName).join(', '); + if (exampleNames) { + return `User has been kicked from ${kickedRooms.length} rooms, including ${exampleNames}`; + } + return `User has been kicked from ${kickedRooms.length} rooms`; + } + return null; + }, [client, inviterMember]); + + const userFirstSeen = useMemo(() => { + if (!inviterMember?.userId) { + return null; + } + const earliestMembershipTs = client + .getRooms() + .map((r) => r.getMember(inviterMember?.userId)) + .filter((member) => member?.membership === KnownMembership.Join) + .map((member) => member?.events.member?.getTs()) + .filter((ts) => ts !== undefined) + .sort((tsA, tsB) => tsA - tsB)[0]; + + + if (earliestMembershipTs) { + const userDuration = Date.now() - earliestMembershipTs; + if (userDuration > LONG_TERM_USER_MS) { + const description = `You first saw activity from this user ${formatDuration(userDuration)} ago.`; + return { text: `This user has been active for a while.`, description, type: "success" } + } else { + const description = `The earliest activity you have seen from this user was ${formatDuration(userDuration)} ago.`; + return { text: `This user may have recently created their account.`, description, type: "critical" }; + } + + } + return null; + }, [client, inviterMember]); + + + if (!inviterMember) { + return null; + } + + return
    + {roomCount === 0 &&
  • + +
  • } + {joinedTo &&
  • + {joinedTo.description} +
  • } + {userBanned &&
  • + {userBanned} +
  • } + {userKicked &&
  • + {userKicked} +
  • } + {userFirstSeen &&
  • + {userFirstSeen.description} +
  • } + {roomCount !== 0 &&
  • + +
  • } +
; +} \ No newline at end of file