From 4d4e7f3c9c026cceb139c963afdf8aa6b8e124fc Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Wed, 11 Dec 2024 18:29:20 +0530 Subject: [PATCH] Support 3pid invites --- .../viewmodels/MemberListViewModel.tsx | 71 +++++++++++++----- .../viewmodels/MemberTileViewModel.tsx | 34 ++++++++- src/components/views/rooms/MemberListView.tsx | 10 ++- src/components/views/rooms/MemberTileView.tsx | 75 ++++++++++++++----- src/models/rooms/ThreePIDInvite.ts | 4 +- 5 files changed, 149 insertions(+), 45 deletions(-) diff --git a/src/components/viewmodels/MemberListViewModel.tsx b/src/components/viewmodels/MemberListViewModel.tsx index 6af987e080..644f160bf1 100644 --- a/src/components/viewmodels/MemberListViewModel.tsx +++ b/src/components/viewmodels/MemberListViewModel.tsx @@ -43,8 +43,39 @@ import PosthogTrackers from "../../PosthogTrackers"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import { inviteToRoom } from "../../utils/room/inviteToRoom"; import { canInviteTo } from "../../utils/room/canInviteTo"; +import { isValid3pidInvite } from "../../RoomInvite"; +import { ThreePIDInvite } from "../../models/rooms/ThreePIDInvite"; +import { XOR } from "../../@types/common"; -function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember { +type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>; + +function getPending3PidInvites(room: Room, searchQuery?: string): Member[] { + // include 3pid invites (m.room.third_party_invite) state events. + // The HS may have already converted these into m.room.member invites so + // we shouldn't add them if the 3pid invite state key (token) is in the + // member invite (content.third_party_invite.signed.token) + const inviteEvents = room.currentState.getStateEvents("m.room.third_party_invite").filter(function (e) { + if (!isValid3pidInvite(e)) return false; + if (searchQuery && !(e.getContent().display_name as string)?.includes(searchQuery)) return false; + + // discard all invites which have a m.room.member event since we've + // already added them. + const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()!); + if (memberEvent) return false; + return true; + }); + const invites: Member[] = inviteEvents.map((e) => { + return { + threePidInvite: { + displayName: e.getContent().display_name, + event: e, + }, + }; + }); + return invites; +} + +function sdkRoomMemberToRoomMember(member: SDKRoomMember): Member { const displayUserId = UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { roomId: member.roomId, @@ -61,22 +92,24 @@ function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember { } return { - roomId: member.roomId, - userId: member.userId, - displayUserId: displayUserId, - name: member.name, - rawDisplayName: member.rawDisplayName, - disambiguate: member.disambiguate, - avatarThumbnailUrl: avatarThumbnailUrl, - powerLevel: member.powerLevel, - lastModifiedTime: member.getLastModifiedTime(), - presenceState, - isInvite: member.membership === KnownMembership.Invite, + member: { + roomId: member.roomId, + userId: member.userId, + displayUserId: displayUserId, + name: member.name, + rawDisplayName: member.rawDisplayName, + disambiguate: member.disambiguate, + avatarThumbnailUrl: avatarThumbnailUrl, + powerLevel: member.powerLevel, + lastModifiedTime: member.getLastModifiedTime(), + presenceState, + isInvite: member.membership === KnownMembership.Invite, + }, }; } export interface MemberListViewState { - members: RoomMember[]; + members: Member[]; memberCount: number; search: (searchQuery: string) => void; isPresenceEnabled: boolean; @@ -85,7 +118,6 @@ export interface MemberListViewState { canInvite: boolean; onInviteButtonClick: (ev: ButtonEvent) => void; } - export function useMemberListViewModel(roomId: string): MemberListViewState { const cli = useMatrixClientContext(); const room = useMemo(() => cli.getRoom(roomId), [roomId, cli]); @@ -93,7 +125,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { throw new Error(`Room with id ${roomId} does not exist!`); } const sdkContext = useContext(SDKContext); - const [members, setMembers] = useState([]); + const [members, setMembers] = useState([]); const [memberCount, setMemberCount] = useState(0); const searchQuery = useRef(""); const [isLoading, setIsLoading] = useState(true); @@ -108,13 +140,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { ); const joined = joinedSdk.map(sdkRoomMemberToRoomMember); const invited = invitedSdk.map(sdkRoomMemberToRoomMember); - setMembers([...invited, ...joined]); - if (!searchQuery.current) setMemberCount(joined.length); + const threePidInvited = getPending3PidInvites(room, searchQuery.current); + const newMembers = [...invited, ...threePidInvited, ...joined]; + setMembers(newMembers); + if (!searchQuery.current) setMemberCount(newMembers.length); }, 500, { leading: true, trailing: true }, ), - [roomId, sdkContext.memberListStore], + //todo: can we remove room here? + [roomId, sdkContext.memberListStore, room], ); const search = useCallback( diff --git a/src/components/viewmodels/MemberTileViewModel.tsx b/src/components/viewmodels/MemberTileViewModel.tsx index edf18eee2e..23c29391a1 100644 --- a/src/components/viewmodels/MemberTileViewModel.tsx +++ b/src/components/viewmodels/MemberTileViewModel.tsx @@ -30,17 +30,22 @@ import { RoomMember } from "../../models/rooms/RoomMember"; import { E2EState } from "../views/rooms/E2EIcon"; import { _t, _td, TranslationKey } from "../../languageHandler"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; +import { ThreePIDInvite } from "../../models/rooms/ThreePIDInvite"; -interface IProps { +interface MemberTileViewModelProps { member: RoomMember; showPresence?: boolean; } -export interface MemberTileViewState extends IProps { +interface ThreePidTileViewModelProps { + threePidInvite: ThreePIDInvite; +} + +export interface MemberTileViewState extends MemberTileViewModelProps { e2eStatus?: E2EState; name: string; onClick: () => void; - title: string; + title?: string; userLabel?: string; } @@ -54,7 +59,28 @@ const PowerLabel: Record = { [PowerStatus.Moderator]: _td("power_level|mod"), }; -export default function useMemberTileViewModel(props: IProps): MemberTileViewState { +export interface ThreePidTileViewState { + name: string; + onClick: () => void; +} + +export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState { + const invite = props.threePidInvite; + const name = invite.displayName; + const onClick = (): void => { + dis.dispatch({ + action: Action.View3pidInvite, + event: invite.event, + }); + }; + + return { + name, + onClick, + }; +} + +export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState { const [e2eStatus, setE2eStatus] = useState(); useEffect(() => { diff --git a/src/components/views/rooms/MemberListView.tsx b/src/components/views/rooms/MemberListView.tsx index b609894cec..b03da21388 100644 --- a/src/components/views/rooms/MemberListView.tsx +++ b/src/components/views/rooms/MemberListView.tsx @@ -21,7 +21,7 @@ import { AutoSizer } from "react-virtualized"; import { Flex } from "../../utils/Flex"; import { useMemberListViewModel } from "../../viewmodels/MemberListViewModel"; -import MemberTileNext from "./MemberTileView"; +import { RoomMemberTileView, ThreePidInviteTileView } from "./MemberTileView"; import MemberListHeaderView from "./MemberListHeaderView"; import BaseCard from "../right_panel/BaseCard"; import { _t } from "../../../languageHandler"; @@ -35,10 +35,14 @@ const MemberListView: React.FC = (props: IProps) => { const vm = useMemberListViewModel(props.roomId); const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { - const member = vm.members[index]; + const item = vm.members[index]; return (
- + {item.member ? ( + + ) : ( + + )}
); }; diff --git a/src/components/views/rooms/MemberTileView.tsx b/src/components/views/rooms/MemberTileView.tsx index 040a342c32..d534a302c4 100644 --- a/src/components/views/rooms/MemberTileView.tsx +++ b/src/components/views/rooms/MemberTileView.tsx @@ -20,17 +20,59 @@ import React from "react"; import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import { RoomMember } from "../../../models/rooms/RoomMember"; import MemberAvatarNext from "../avatars/MemberAvatarView"; -import useMemberTileViewModel from "../../viewmodels/MemberTileViewModel"; +import { useThreePidTileViewModel, useMemberTileViewModel } from "../../viewmodels/MemberTileViewModel"; import E2EIcon from "./E2EIconView"; import AvatarPresenceIconView from "./PresenceIconView"; import AccessibleButton from "../elements/AccessibleButton"; +import { ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite"; +import BaseAvatar from "../avatars/BaseAvatar"; interface IProps { member: RoomMember; showPresence?: boolean; } -export default function MemberTileView(props: IProps): JSX.Element { +interface ThreePidProps { + threePidInvite: ThreePIDInvite; +} + +interface TileProps { + avatarJsx: JSX.Element; + nameJsx: JSX.Element | string; + onClick: () => void; + title?: string; + presenceJsx?: JSX.Element; + userLabelJsx?: JSX.Element; + e2eIconJsx?: JSX.Element; +} + +function MemberTile(props: TileProps): JSX.Element { + return ( + // The wrapping div is required to make the magic mouse listener work, for some reason. +
+ +
+
+ {props.avatarJsx} {props.presenceJsx} +
+
{props.nameJsx}
+
+
+ {props.userLabelJsx} + {props.e2eIconJsx} +
+
+
+ ); +} + +export function ThreePidInviteTileView(props: ThreePidProps): JSX.Element { + const vm = useThreePidTileViewModel(props); + const av =