Support 3pid invites
This commit is contained in:
@@ -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<RoomMember[]>([]);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [memberCount, setMemberCount] = useState<number>(0);
|
||||
const searchQuery = useRef("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(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(
|
||||
|
||||
@@ -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, TranslationKey> = {
|
||||
[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<E2EState | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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<IProps> = (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 (
|
||||
<div key={key} style={style}>
|
||||
<MemberTileNext member={member} showPresence={false} />
|
||||
{item.member ? (
|
||||
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
|
||||
) : (
|
||||
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
<div>
|
||||
<AccessibleButton className="mx_MemberTileView" title={props.title} onClick={props.onClick}>
|
||||
<div className="mx_MemberTileView_left">
|
||||
<div className="mx_MemberTileView_avatar">
|
||||
{props.avatarJsx} {props.presenceJsx}
|
||||
</div>
|
||||
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
|
||||
</div>
|
||||
<div className="mx_MemberTileView_right">
|
||||
{props.userLabelJsx}
|
||||
{props.e2eIconJsx}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThreePidInviteTileView(props: ThreePidProps): JSX.Element {
|
||||
const vm = useThreePidTileViewModel(props);
|
||||
const av = <BaseAvatar name={vm.name} size="36px" aria-hidden="true" />;
|
||||
return <MemberTile nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />;
|
||||
}
|
||||
|
||||
export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||
const vm = useMemberTileViewModel(props);
|
||||
const member = vm.member;
|
||||
const av = <MemberAvatarNext member={member} size="32px" aria-hidden="true" />;
|
||||
@@ -39,7 +81,10 @@ export default function MemberTileView(props: IProps): JSX.Element {
|
||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
||||
|
||||
const presenceState = member.presenceState;
|
||||
const presenceJSX = vm.showPresence && presenceState && <AvatarPresenceIconView presenceState={presenceState} />;
|
||||
let presenceJSX: JSX.Element | undefined;
|
||||
if (vm.showPresence && presenceState) {
|
||||
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
|
||||
}
|
||||
|
||||
let userLabelJSX;
|
||||
if (vm.userLabel) {
|
||||
@@ -51,21 +96,15 @@ export default function MemberTileView(props: IProps): JSX.Element {
|
||||
e2eIcon = <E2EIcon isUser={true} status={vm.e2eStatus} />;
|
||||
}
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
<div>
|
||||
<AccessibleButton className="mx_MemberTileView" title={vm.title} onClick={vm.onClick}>
|
||||
<div className="mx_MemberTileView_left">
|
||||
<div className="mx_MemberTileView_avatar">
|
||||
{av} {presenceJSX}
|
||||
</div>
|
||||
<div className="mx_MemberTileView_name">{nameJSX}</div>
|
||||
</div>
|
||||
<div className="mx_MemberTileView_right">
|
||||
{userLabelJSX}
|
||||
{e2eIcon}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<MemberTile
|
||||
title={vm.title}
|
||||
onClick={vm.onClick}
|
||||
avatarJsx={av}
|
||||
presenceJsx={presenceJSX}
|
||||
nameJsx={nameJSX}
|
||||
userLabelJsx={userLabelJSX}
|
||||
e2eIconJsx={e2eIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export type ThreePIDInvite = {
|
||||
eventId: string;
|
||||
stateKey: string;
|
||||
displayName: string;
|
||||
event: MatrixEvent;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user