From b3b5c11b1b894cdaa1e7e355bdb58c126db864e4 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Sat, 23 Mar 2024 22:14:08 +0530
Subject: [PATCH] add invitations
---
src/app/organisms/room/RoomView.jsx | 1 +
src/app/pages/App.tsx | 4 +-
src/app/pages/client/inbox/Invites.tsx | 266 +++++++++++++++++++++++++
src/app/pages/client/inbox/index.ts | 1 +
src/app/state/room-list/utils.ts | 4 +-
src/app/utils/matrix.ts | 78 ++++++++
6 files changed, 351 insertions(+), 3 deletions(-)
create mode 100644 src/app/pages/client/inbox/Invites.tsx
diff --git a/src/app/organisms/room/RoomView.jsx b/src/app/organisms/room/RoomView.jsx
index 9d97cb60..1c040313 100644
--- a/src/app/organisms/room/RoomView.jsx
+++ b/src/app/organisms/room/RoomView.jsx
@@ -35,6 +35,7 @@ function RoomView({ room, eventId }) {
const canMessage = myUserId
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
: false;
+ // FIXME: decide can message on membership based also
useEffect(() => {
const settingsToggle = (isVisible) => {
diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx
index 8406ef48..d3360370 100644
--- a/src/app/pages/App.tsx
+++ b/src/app/pages/App.tsx
@@ -44,7 +44,7 @@ import { RoomViewer } from '../organisms/room/Room';
import { Direct } from './client/direct';
import { SpaceViewer } from './client/space';
import { Explore, ExploreRedirect, FeaturedRooms, PublicRooms } from './client/explore';
-import { Notifications, Inbox, InboxRedirect } from './client/inbox';
+import { Notifications, Inbox, InboxRedirect, Invites } from './client/inbox';
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
const queryClient = new QueryClient();
@@ -108,7 +108,7 @@ const createRouter = (clientConfig: ClientConfig) => {
}>
} />
} />
- invites
} />
+ } />
diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx
new file mode 100644
index 00000000..c8760117
--- /dev/null
+++ b/src/app/pages/client/inbox/Invites.tsx
@@ -0,0 +1,266 @@
+import React, { useCallback, useRef, useState } from 'react';
+import {
+ Avatar,
+ Box,
+ Button,
+ Icon,
+ Icons,
+ Overlay,
+ OverlayBackdrop,
+ OverlayCenter,
+ Scroll,
+ Spinner,
+ Text,
+ color,
+ config,
+} from 'folds';
+import { useAtomValue } from 'jotai';
+import FocusTrap from 'focus-trap-react';
+import { MatrixError, Room } from 'matrix-js-sdk';
+import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
+import { useDirectInvites, useRoomInvites, useSpaceInvites } from '../../../state/hooks/inviteList';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { allInvitesAtom } from '../../../state/room-list/inviteList';
+import { mDirectAtom } from '../../../state/mDirectList';
+import { SequenceCard } from '../../../components/sequence-card';
+import { getMemberDisplayName, getRoomAvatarUrl, isDirectInvite } from '../../../utils/room';
+import { nameInitials } from '../../../utils/common';
+import { RoomAvatar } from '../../../components/room-avatar';
+import { addRoomIdToMDirect, getMxIdLocalPart, guessDmRoomUserId } from '../../../utils/matrix';
+import { Time } from '../../../components/message';
+import { StateEvent } from '../../../../types/matrix/room';
+import { useStateEvent } from '../../../hooks/useStateEvent';
+import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
+import { onEnterOrSpace } from '../../../utils/keyboard';
+import { RoomTopicViewer } from '../../../components/room-topic-viewer';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+
+const COMPACT_CARD_WIDTH = 548;
+
+type InviteCardProps = {
+ room: Room;
+ userId: string;
+ compact?: boolean;
+ onNavigate: (roomId: string) => void;
+};
+function InviteCard({ room, userId, compact, onNavigate }: InviteCardProps) {
+ const mx = useMatrixClient();
+ const roomName = room.name || room.getCanonicalAlias() || room.roomId;
+ const member = room.getMember(userId);
+ const memberEvent = member?.events.member;
+ const memberTs = memberEvent?.getTs() ?? 0;
+ const senderId = memberEvent?.getSender();
+ const senderName = senderId
+ ? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
+ : undefined;
+
+ const topicEvent = useStateEvent(room, StateEvent.RoomTopic);
+ const topic = (topicEvent?.getContent().topic as string) || undefined;
+
+ const [viewTopic, setViewTopic] = useState(false);
+ const closeTopic = () => setViewTopic(false);
+ const openTopic = () => setViewTopic(true);
+
+ const [joinState, join] = useAsyncCallback(
+ useCallback(async () => {
+ const dmUserId = isDirectInvite(room, userId) ? guessDmRoomUserId(room, userId) : undefined;
+
+ await mx.joinRoom(room.roomId);
+ if (dmUserId) {
+ await addRoomIdToMDirect(mx, room.roomId, dmUserId);
+ }
+ onNavigate(room.roomId);
+ }, [mx, room, userId, onNavigate])
+ );
+ const [leaveState, leave] = useAsyncCallback, MatrixError, []>(
+ useCallback(() => mx.leave(room.roomId), [mx, room])
+ );
+
+ const joining =
+ joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
+ const leaving =
+ leaveState.status === AsyncStatus.Loading || leaveState.status === AsyncStatus.Success;
+
+ return (
+
+
+
+
+ Invited by {senderName}
+
+
+
+
+
+
+
+
+ (
+
+ {nameInitials(roomName)}
+
+ )}
+ />
+
+
+
+
+
+ {roomName}
+
+ {topic && (
+
+ {topic}
+
+ )}
+ }>
+
+
+
+
+
+
+
+ {joinState.status === AsyncStatus.Error && (
+
+ {joinState.error.message}
+
+ )}
+ {leaveState.status === AsyncStatus.Error && (
+
+ {leaveState.error.message}
+
+ )}
+
+
+ : undefined}
+ >
+ Decline
+
+ : undefined}
+ >
+ Accept
+
+
+
+
+
+ );
+}
+
+export function Invites() {
+ const mx = useMatrixClient();
+ const userId = mx.getUserId()!;
+ const mDirects = useAtomValue(mDirectAtom);
+ const directInvites = useDirectInvites(mx, allInvitesAtom, mDirects);
+ const spaceInvites = useSpaceInvites(mx, allInvitesAtom);
+ const roomInvites = useRoomInvites(mx, allInvitesAtom, mDirects);
+ const containerRef = useRef(null);
+ const [compact, setCompact] = useState(document.body.clientWidth <= COMPACT_CARD_WIDTH);
+ useElementSizeObserver(
+ useCallback(() => containerRef.current, []),
+ useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), [])
+ );
+
+ const { navigateRoom, navigateSpace } = useRoomNavigate();
+
+ const renderInvite = (roomId: string, handleNavigate: (rId: string) => void) => {
+ const room = mx.getRoom(roomId);
+ if (!room) return null;
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ Invitations
+
+
+
+
+
+
+
+
+ {directInvites.length > 0 && (
+
+ Direct Messages
+
+ {directInvites.map((roomId) => renderInvite(roomId, navigateRoom))}
+
+
+ )}
+ {spaceInvites.length > 0 && (
+
+ Spaces
+
+ {spaceInvites.map((roomId) => renderInvite(roomId, navigateSpace))}
+
+
+ )}
+ {roomInvites.length > 0 && (
+
+ Rooms
+
+ {roomInvites.map((roomId) => renderInvite(roomId, navigateRoom))}
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/pages/client/inbox/index.ts b/src/app/pages/client/inbox/index.ts
index 98ed96bd..c8036b47 100644
--- a/src/app/pages/client/inbox/index.ts
+++ b/src/app/pages/client/inbox/index.ts
@@ -1,2 +1,3 @@
export * from './Inbox';
export * from './Notifications';
+export * from './Invites';
diff --git a/src/app/state/room-list/utils.ts b/src/app/state/room-list/utils.ts
index 0e2c4a9f..1ca7e7de 100644
--- a/src/app/state/room-list/utils.ts
+++ b/src/app/state/room-list/utils.ts
@@ -38,7 +38,9 @@ export const useBindRoomsWithMembershipsAtom = (
};
const handleMembershipChange = (room: Room) => {
- if (!satisfyMembership(room)) {
+ if (satisfyMembership(room)) {
+ setRoomsAtom({ type: 'PUT', roomId: room.roomId });
+ } else {
setRoomsAtom({ type: 'DELETE', roomId: room.roomId });
}
};
diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts
index f733de44..55e16a95 100644
--- a/src/app/utils/matrix.ts
+++ b/src/app/utils/matrix.ts
@@ -8,10 +8,12 @@ import {
MatrixError,
MatrixEvent,
Room,
+ RoomMember,
UploadProgress,
UploadResponse,
} from 'matrix-js-sdk';
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
+import { AccountDataEvent } from '../../types/matrix/accountData';
export const matchMxId = (id: string): RegExpMatchArray | null =>
id.match(/^([@!$+#])(\S+):(\S+)$/);
@@ -172,3 +174,79 @@ export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined
return dmLikeRooms.find((room) => room.getMember(userId));
};
+
+export const guessDmRoomUserId = (room: Room, myUserId: string): string => {
+ const getOldestMember = (members: RoomMember[]): RoomMember | undefined => {
+ let oldestMemberTs: number | undefined;
+ let oldestMember: RoomMember | undefined;
+
+ const pickOldestMember = (member: RoomMember) => {
+ if (member.userId === myUserId) return;
+
+ if (
+ oldestMemberTs === undefined ||
+ (member.events.member && member.events.member.getTs() < oldestMemberTs)
+ ) {
+ oldestMember = member;
+ oldestMemberTs = member.events.member?.getTs();
+ }
+ };
+
+ members.forEach(pickOldestMember);
+
+ return oldestMember;
+ };
+
+ // Pick the joined user who's been here longest (and isn't us),
+ const member = getOldestMember(room.getJoinedMembers());
+ if (member) return member.userId;
+
+ // if there are no joined members other than us, use the oldest member
+ const member1 = getOldestMember(room.currentState.getMembers());
+ return member1?.userId ?? myUserId;
+};
+
+export const addRoomIdToMDirect = async (
+ mx: MatrixClient,
+ roomId: string,
+ userId: string
+): Promise => {
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct);
+ const userIdToRoomIds: Record = mDirectsEvent?.getContent() ?? {};
+
+ // remove it from the lists of any others users
+ // (it can only be a DM room for one person)
+ Object.keys(userIdToRoomIds).forEach((targetUserId) => {
+ const roomIds = userIdToRoomIds[targetUserId];
+
+ if (targetUserId !== userId) {
+ const indexOfRoomId = roomIds.indexOf(roomId);
+ if (indexOfRoomId > -1) {
+ roomIds.splice(indexOfRoomId, 1);
+ }
+ }
+ });
+
+ const roomIds = userIdToRoomIds[userId] || [];
+ if (roomIds.indexOf(roomId) === -1) {
+ roomIds.push(roomId);
+ }
+ userIdToRoomIds[userId] = roomIds;
+
+ await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds);
+};
+
+export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string): Promise => {
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct);
+ const userIdToRoomIds: Record = mDirectsEvent?.getContent() ?? {};
+
+ Object.keys(userIdToRoomIds).forEach((targetUserId) => {
+ const roomIds = userIdToRoomIds[targetUserId];
+ const indexOfRoomId = roomIds.indexOf(roomId);
+ if (indexOfRoomId > -1) {
+ roomIds.splice(indexOfRoomId, 1);
+ }
+ });
+
+ await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds);
+};