move notifications to new sdk
This commit is contained in:
parent
c727f76466
commit
8ef2ecb419
3 changed files with 394 additions and 381 deletions
|
|
@ -3,7 +3,7 @@ import { PendingObservableResult } from 'neurone/lib/utils/observable';
|
|||
import { RoomMembership } from 'neurone/lib/api/types';
|
||||
import { useClient } from './useClient';
|
||||
import { useObservableValuePromise } from './useObservableValue';
|
||||
import { JoinRoomMemberObject } from '../../client/storage/collections';
|
||||
import { getJoinRoomMemberId, JoinRoomMemberObject } from '../../client/storage/collections';
|
||||
import { getMxIdLocalPart } from '../utils/matrix';
|
||||
import {
|
||||
roomMemberContentAvatarUrl,
|
||||
|
|
@ -23,14 +23,9 @@ export type RoomMembersAPI = {
|
|||
memberEventsResult: PendingObservableResult<JoinRoomMemberObject[]>;
|
||||
};
|
||||
|
||||
export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
|
||||
const ct = useClient();
|
||||
const memberEventsPromise = useMemo(
|
||||
() => ct.storage.joinRoomMember.observableRead.queryIndex('roomId', roomId),
|
||||
[ct, roomId]
|
||||
);
|
||||
|
||||
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
|
||||
const useMemberEvents = (
|
||||
memberEventsResult: PendingObservableResult<JoinRoomMemberObject[]>
|
||||
): RoomMembersAPI => {
|
||||
const memberEvents = memberEventsResult.value;
|
||||
|
||||
const userIdToMemberEvent = useMemo(() => {
|
||||
|
|
@ -92,6 +87,18 @@ export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
|
|||
return api;
|
||||
};
|
||||
|
||||
export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
|
||||
const ct = useClient();
|
||||
const memberEventsPromise = useMemo(
|
||||
() => ct.storage.joinRoomMember.observableRead.queryIndex('roomId', roomId),
|
||||
[ct, roomId]
|
||||
);
|
||||
|
||||
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
|
||||
|
||||
return useMemberEvents(memberEventsResult);
|
||||
};
|
||||
|
||||
const RoomMembersAPIContext = createContext<RoomMembersAPI | null>(null);
|
||||
export const RoomMembersAPIProvider = RoomMembersAPIContext.Provider;
|
||||
|
||||
|
|
@ -103,3 +110,27 @@ export const useRoomMembersAPI = (): RoomMembersAPI => {
|
|||
|
||||
return api;
|
||||
};
|
||||
|
||||
export const useRoomMembers = (roomId: string, users: string[]) => {
|
||||
const ct = useClient();
|
||||
const memberEventsPromise = useMemo(
|
||||
() =>
|
||||
ct.storage.joinRoomMember.observableRead.getMultiple(
|
||||
users.map((userId) => getJoinRoomMemberId(roomId, userId))
|
||||
),
|
||||
[ct, roomId, users]
|
||||
);
|
||||
|
||||
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
|
||||
|
||||
const memberEvents = useMemo(() => {
|
||||
if (memberEventsResult.value) {
|
||||
return {
|
||||
value: memberEventsResult.value.filter((i) => i !== undefined),
|
||||
};
|
||||
}
|
||||
return memberEventsResult;
|
||||
}, [memberEventsResult]);
|
||||
|
||||
return useMemberEvents(memberEvents);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
|
|
@ -14,75 +22,50 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
INotification,
|
||||
INotificationsResponse,
|
||||
IRoomEvent,
|
||||
JoinRule,
|
||||
Method,
|
||||
RelationType,
|
||||
Room,
|
||||
} from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import {
|
||||
getNotifications,
|
||||
GetNotificationsOK,
|
||||
} from 'neurone/lib/api/matrix/client/v3/notifications';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { NotificationChunk } from 'neurone/lib/api/types';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { InboxNotificationsPathSearchParams } from '../../paths';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
|
||||
import {
|
||||
getEditedEvent,
|
||||
getMemberAvatarMxc,
|
||||
getMemberDisplayName,
|
||||
getRoomAvatarUrl,
|
||||
} from '../../../utils/room';
|
||||
import { ScrollTopContainer } from '../../../components/scroll-top-container';
|
||||
import { useInterval } from '../../../hooks/useInterval';
|
||||
import {
|
||||
AvatarBase,
|
||||
ImageContent,
|
||||
MSticker,
|
||||
MessageNotDecryptedContent,
|
||||
MessageUnsupportedContent,
|
||||
ModernLayout,
|
||||
RedactedContent,
|
||||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../../plugins/react-custom-html-parser';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { Image } from '../../../components/media';
|
||||
import { ImageViewer } from '../../../components/image-viewer';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
import { useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
|
||||
import * as customHtmlCss from '../../../styles/CustomHtml.css';
|
||||
import { useSettings } from '../../../state/hooks/settings';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { markAsRead } from '../../../utils/notifications';
|
||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||
import { VirtualTile } from '../../../components/virtualizer';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { EncryptedContent } from '../../../features/room/message';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
|
||||
import {
|
||||
PowerLevelsContextProvider,
|
||||
usePowerLevels,
|
||||
usePowerLevelsContext,
|
||||
usePowerLevelsState,
|
||||
} from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
PowerLevelTagsContextProvider,
|
||||
usePowerLevelTags,
|
||||
usePowerLevelTagsContext,
|
||||
usePowerLevelTagsState,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
|
|
@ -94,10 +77,33 @@ import {
|
|||
} from '../../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import {
|
||||
RoomMembersAPIProvider,
|
||||
useRoomMembers,
|
||||
useRoomMembersAPI,
|
||||
} from '../../../hooks/useRoomMembers';
|
||||
import { useMessageParserOptions } from '../../../hooks/useMessageParserOptions';
|
||||
import { useRoomHeroes } from '../../../hooks/storage/useRoomHeros';
|
||||
import { GetRoomNameCallback, useGetRoomName } from '../../../hooks/storage/useGetRoomName';
|
||||
import { GetRoomAvatarCallback, useGetRoomAvatar } from '../../../hooks/storage/useGetRoomAvatar';
|
||||
import { mxcToHttp } from '../../../../client/mxcToHttp';
|
||||
import { useRoomUnread } from '../../../state/hooks/unread';
|
||||
import { unreadRoomsAtom } from '../../../state/room/roomToUnread';
|
||||
import { ErrorCode } from '../../../cs-errorcode';
|
||||
import {
|
||||
useObservableValue,
|
||||
useObservableValueOptionally,
|
||||
} from '../../../hooks/useObservableValue';
|
||||
import { Timeline } from '../../../../client/room';
|
||||
import { MessageContent } from '../../../components/MessageContent';
|
||||
import { getLatestEditEvent } from '../../../utils/room';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { rContent } from '../../../../client/reader';
|
||||
|
||||
type RoomNotificationsGroup = {
|
||||
roomId: string;
|
||||
notifications: INotification[];
|
||||
notifications: NotificationChunk[];
|
||||
};
|
||||
type NotificationTimeline = {
|
||||
nextToken?: string;
|
||||
|
|
@ -107,7 +113,7 @@ type LoadTimeline = (from?: string) => Promise<void>;
|
|||
type SilentReloadTimeline = () => Promise<void>;
|
||||
|
||||
const groupNotifications = (
|
||||
notifications: INotification[],
|
||||
notifications: NotificationChunk[],
|
||||
allowRooms: Set<string>
|
||||
): RoomNotificationsGroup[] => {
|
||||
const groups: RoomNotificationsGroup[] = [];
|
||||
|
|
@ -132,7 +138,7 @@ const useNotificationTimeline = (
|
|||
paginationLimit: number,
|
||||
onlyHighlight?: boolean
|
||||
): [NotificationTimeline, LoadTimeline, SilentReloadTimeline] => {
|
||||
const mx = useMatrixClient();
|
||||
const ct = useClient();
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
|
||||
|
||||
|
|
@ -141,15 +147,21 @@ const useNotificationTimeline = (
|
|||
});
|
||||
|
||||
const fetchNotifications = useCallback(
|
||||
(from?: string, limit?: number, only?: 'highlight') => {
|
||||
const queryParams = { from, limit, only };
|
||||
return mx.http.authedRequest<INotificationsResponse>(
|
||||
Method.Get,
|
||||
'/notifications',
|
||||
queryParams
|
||||
async (from?: string, limit?: number, only?: 'highlight') => {
|
||||
const resp = await ct.requestAuthJSON(
|
||||
getNotifications({ searchParams: { from, limit, only } })
|
||||
);
|
||||
const [err, result] = await parseMatrixResp<GetNotificationsOK>(resp);
|
||||
if (err) {
|
||||
if (err.data.errcode === ErrorCode.M_UNRECOGNIZED) {
|
||||
throw new Error('This feature is not supported by your homeserver!');
|
||||
}
|
||||
throw new Error(err.data.error ?? err.data.errcode);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[mx]
|
||||
[ct]
|
||||
);
|
||||
|
||||
const loadTimeline: LoadTimeline = useCallback(
|
||||
|
|
@ -178,8 +190,12 @@ const useNotificationTimeline = (
|
|||
);
|
||||
|
||||
/**
|
||||
* Reload timeline silently i.e without setting to default
|
||||
* before fetching notifications from start
|
||||
* Reload timeline silently i.e without resetting to default.
|
||||
* Fetches the first page and diffs it against current state:
|
||||
* - if nothing new, keeps the existing state reference untouched
|
||||
* - if there are new notifications, only prepends them to the top
|
||||
* (merging into the first existing group when it's the same room)
|
||||
* instead of re-fetching/rebuilding the whole timeline.
|
||||
*/
|
||||
const silentReloadTimeline: SilentReloadTimeline = useCallback(async () => {
|
||||
const data = await fetchNotifications(
|
||||
|
|
@ -187,221 +203,256 @@ const useNotificationTimeline = (
|
|||
paginationLimit,
|
||||
onlyHighlight ? 'highlight' : undefined
|
||||
);
|
||||
const groups = groupNotifications(data.notifications, allJoinedRooms);
|
||||
setNotificationTimeline({
|
||||
nextToken: data.next_token,
|
||||
groups,
|
||||
|
||||
setNotificationTimeline((currentTimeline) => {
|
||||
// Nothing loaded yet, just seed the state normally.
|
||||
if (currentTimeline.groups.length === 0) {
|
||||
return {
|
||||
nextToken: data.next_token,
|
||||
groups: groupNotifications(data.notifications, allJoinedRooms),
|
||||
};
|
||||
}
|
||||
|
||||
const existingIds = new Set<string>();
|
||||
currentTimeline.groups.forEach((group) => {
|
||||
group.notifications.forEach((n) => existingIds.add(n.event.event_id));
|
||||
});
|
||||
|
||||
const newNotifications = data.notifications.filter((n) => !existingIds.has(n.event.event_id));
|
||||
|
||||
// Nothing changed — return the same reference so consumers relying
|
||||
// on identity (e.g. memoized lists) don't needlessly re-render.
|
||||
if (newNotifications.length === 0) {
|
||||
return currentTimeline;
|
||||
}
|
||||
|
||||
const newGroups = groupNotifications(newNotifications, allJoinedRooms);
|
||||
|
||||
const lastNewGroup = newGroups[newGroups.length - 1];
|
||||
const firstExistingGroup = currentTimeline.groups[0];
|
||||
|
||||
// Merge into the existing first group if it's the same room, so we
|
||||
// don't end up with two adjacent groups for the same room.
|
||||
if (lastNewGroup && firstExistingGroup?.roomId === lastNewGroup.roomId) {
|
||||
const mergedGroup: RoomNotificationsGroup = {
|
||||
roomId: firstExistingGroup.roomId,
|
||||
notifications: [...lastNewGroup.notifications, ...firstExistingGroup.notifications],
|
||||
};
|
||||
return {
|
||||
nextToken: currentTimeline.nextToken,
|
||||
groups: [...newGroups.slice(0, -1), mergedGroup, ...currentTimeline.groups.slice(1)],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
nextToken: currentTimeline.nextToken,
|
||||
groups: [...newGroups, ...currentTimeline.groups],
|
||||
};
|
||||
});
|
||||
}, [paginationLimit, onlyHighlight, fetchNotifications, allJoinedRooms]);
|
||||
|
||||
return [notificationTimeline, loadTimeline, silentReloadTimeline];
|
||||
};
|
||||
|
||||
type RoomNotificationsGroupProps = {
|
||||
room: Room;
|
||||
notifications: INotification[];
|
||||
type RoomNotificationsProps = {
|
||||
roomId: string;
|
||||
timeline?: Timeline;
|
||||
notifications: NotificationChunk[];
|
||||
mediaAutoLoad?: boolean;
|
||||
urlPreview?: boolean;
|
||||
hideActivity: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
hour24Clock: boolean;
|
||||
dateFormatString: string;
|
||||
getRoomName: GetRoomNameCallback;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
};
|
||||
function RoomNotificationsGroupComp({
|
||||
room,
|
||||
function RoomNotifications({
|
||||
roomId,
|
||||
timeline,
|
||||
notifications,
|
||||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
hideActivity,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: RoomNotificationsGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
getRoomName,
|
||||
onOpen,
|
||||
}: RoomNotificationsProps) {
|
||||
const ct = useClient();
|
||||
const membersAPI = useRoomMembersAPI();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
// const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
const redacted = useObservableValueOptionally(timeline?.relations.redacted);
|
||||
const edits = useObservableValueOptionally(timeline?.relations.edits.eventToRelations);
|
||||
|
||||
const creators = useRoomCreators(roomId);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const getMemberPowerTag = useGetMemberPowerTag(creators, powerLevels);
|
||||
const creatorsTag = useRoomCreatorsTag();
|
||||
const powerLevelTags = usePowerLevelTags(room, powerLevels);
|
||||
const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
|
||||
const powerLevelTags = usePowerLevelTagsContext();
|
||||
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||
),
|
||||
}),
|
||||
[mx, room, mentionClickHandler]
|
||||
);
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
|
||||
);
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
|
||||
{
|
||||
[MessageEvent.RoomMessage]: (event, displayName, getContent) => {
|
||||
if (event.unsigned?.redacted_because) {
|
||||
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderMessageContent
|
||||
displayName={displayName}
|
||||
msgType={event.content.msgtype ?? ''}
|
||||
ts={event.origin_server_ts}
|
||||
getContent={getContent}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment
|
||||
/>
|
||||
);
|
||||
},
|
||||
[MessageEvent.RoomMessageEncrypted]: (evt, displayName) => {
|
||||
const evtTimeline = room.getTimelineForEvent(evt.event_id);
|
||||
|
||||
const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === evt.event_id);
|
||||
|
||||
if (!mEvent || !evtTimeline) {
|
||||
return (
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T400" priority="300">
|
||||
<code className={customHtmlCss.Code}>{evt.type}</code>
|
||||
{' event'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
if (mEvent.isRedacted()) return <RedactedContent />;
|
||||
if (mEvent.getType() === MessageEvent.Sticker)
|
||||
return (
|
||||
<MSticker
|
||||
content={mEvent.getContent()}
|
||||
renderImageContent={(props) => (
|
||||
<ImageContent
|
||||
{...props}
|
||||
autoPlay={mediaAutoLoad}
|
||||
renderImage={(p) => <Image {...p} loading="lazy" />}
|
||||
renderViewer={(p) => <ImageViewer {...p} />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
if (mEvent.getType() === MessageEvent.RoomMessage) {
|
||||
const editedEvent = getEditedEvent(
|
||||
evt.event_id,
|
||||
mEvent,
|
||||
evtTimeline.getTimelineSet()
|
||||
);
|
||||
const getContent = (() =>
|
||||
editedEvent?.getContent()['m.new_content'] ??
|
||||
mEvent.getContent()) as GetContentCallback;
|
||||
|
||||
return (
|
||||
<RenderMessageContent
|
||||
displayName={displayName}
|
||||
msgType={mEvent.getContent().msgtype ?? ''}
|
||||
ts={mEvent.getTs()}
|
||||
edited={!!editedEvent}
|
||||
getContent={getContent}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
|
||||
return (
|
||||
<Text>
|
||||
<MessageNotDecryptedContent />
|
||||
</Text>
|
||||
);
|
||||
return (
|
||||
<Text>
|
||||
<MessageUnsupportedContent />
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</EncryptedContent>
|
||||
);
|
||||
},
|
||||
[MessageEvent.Sticker]: (event, displayName, getContent) => {
|
||||
if (event.unsigned?.redacted_because) {
|
||||
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
|
||||
}
|
||||
return (
|
||||
<MSticker
|
||||
content={getContent()}
|
||||
renderImageContent={(props) => (
|
||||
<ImageContent
|
||||
{...props}
|
||||
autoPlay={mediaAutoLoad}
|
||||
renderImage={(p) => <Image {...p} loading="lazy" />}
|
||||
renderViewer={(p) => <ImageViewer {...p} />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[StateEvent.RoomTombstone]: (event) => {
|
||||
const { content } = event;
|
||||
return (
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T400" priority="300">
|
||||
Room Tombstone. {content.body}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
(event) => {
|
||||
if (event.unsigned?.redacted_because) {
|
||||
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
|
||||
}
|
||||
return (
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T400" priority="300">
|
||||
<code className={customHtmlCss.Code}>{event.type}</code>
|
||||
{' event'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const { linkifyOpts, htmlReactParserOptions } = useMessageParserOptions(
|
||||
roomId,
|
||||
getRoomName,
|
||||
membersAPI.getName
|
||||
);
|
||||
|
||||
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!eventId) return;
|
||||
onOpen(room.roomId, eventId);
|
||||
onOpen(roomId, eventId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
{notifications.map((notification) => {
|
||||
const { event } = notification;
|
||||
|
||||
const displayName = membersAPI.getName(event.sender);
|
||||
|
||||
const senderAvatarMxc = membersAPI.getAvatar(event.sender);
|
||||
|
||||
const memberPowerTag = getMemberPowerTag(event.sender);
|
||||
const tagColor = memberPowerTag?.color
|
||||
? accessibleTagColors?.get(memberPowerTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = memberPowerTag?.icon
|
||||
? getPowerTagIconSrc(ct, useAuthentication, memberPowerTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
|
||||
|
||||
const editEvents = edits?.get(event.event_id);
|
||||
const editedEvent = editEvents && getLatestEditEvent(event.sender, editEvents, redacted);
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={notification.event.event_id}
|
||||
style={{ padding: `${config.space.S300} ${config.space.S400}` }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
>
|
||||
<ModernLayout
|
||||
before={
|
||||
<AvatarBase>
|
||||
<Avatar size="300">
|
||||
<UserAvatar
|
||||
userId={event.sender}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcToHttp(
|
||||
ct.session.baseUrl,
|
||||
senderAvatarMxc,
|
||||
useAuthentication
|
||||
).thumbnail({ width: 96, height: 96, method: 'crop' })
|
||||
: undefined
|
||||
}
|
||||
alt={displayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
</AvatarBase>
|
||||
}
|
||||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time
|
||||
ts={event.origin_server_ts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
<Chip
|
||||
data-event-id={event.event_id}
|
||||
onClick={handleOpenClick}
|
||||
variant="Secondary"
|
||||
radii="400"
|
||||
>
|
||||
<Text size="T200">Open</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
{redacted?.has(event.event_id) ? (
|
||||
<RedactedContent />
|
||||
) : (
|
||||
<MessageContent
|
||||
event={event}
|
||||
editedEvent={editedEvent}
|
||||
roomId={roomId}
|
||||
decryptionManager={ct.decryption}
|
||||
>
|
||||
{(contentEvent) => (
|
||||
<RenderMessageContent
|
||||
displayName={membersAPI.getName(event.sender)}
|
||||
eventType={contentEvent.type}
|
||||
ts={event.origin_server_ts}
|
||||
edited={!!editedEvent}
|
||||
content={rContent.newContent(contentEvent.content) ?? contentEvent.content}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
</MessageContent>
|
||||
)}
|
||||
</ModernLayout>
|
||||
</SequenceCard>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type RoomNotificationsGroupProps = {
|
||||
roomId: string;
|
||||
notifications: NotificationChunk[];
|
||||
getRoomName: GetRoomNameCallback;
|
||||
getRoomAvatar: GetRoomAvatarCallback;
|
||||
hideActivity: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
function RoomNotificationsGroupComp({
|
||||
roomId,
|
||||
notifications,
|
||||
getRoomName,
|
||||
getRoomAvatar,
|
||||
hideActivity,
|
||||
children,
|
||||
}: RoomNotificationsGroupProps) {
|
||||
const ct = useClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const unread = useRoomUnread(roomId, unreadRoomsAtom);
|
||||
|
||||
const users = useMemo(
|
||||
() => Array.from(new Set(notifications.map((n) => n.event.sender))),
|
||||
[notifications]
|
||||
);
|
||||
|
||||
const avatarMxc = getRoomAvatar(roomId);
|
||||
|
||||
const powerLevelStateResult = usePowerLevelsState(roomId);
|
||||
const powerLevels = usePowerLevels(powerLevelStateResult.value);
|
||||
|
||||
const powerLevelTagsStateResult = usePowerLevelTagsState(roomId);
|
||||
const powerLevelTags = usePowerLevelTags(powerLevels, powerLevelTagsStateResult.value);
|
||||
|
||||
const membersAPI = useRoomMembers(roomId, users);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId, hideActivity);
|
||||
// markAsRead(mx, room.roomId, hideActivity);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -410,21 +461,29 @@ function RoomNotificationsGroupComp({
|
|||
<Box gap="200" grow="Yes">
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
|
||||
alt={room.name}
|
||||
roomId={roomId}
|
||||
src={
|
||||
avatarMxc
|
||||
? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).thumbnail({
|
||||
width: 48,
|
||||
height: 48,
|
||||
method: 'crop',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
alt={getRoomName(roomId)}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
size="50"
|
||||
roomType={room.getType()}
|
||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||
roomType={ct.roomToType.read.get(roomId)}
|
||||
joinRule={ct.roomToJoinRule.read.get(roomId)}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H4" truncate>
|
||||
{room.name}
|
||||
{getRoomName(roomId) ?? 'Unknown'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
|
|
@ -440,108 +499,14 @@ function RoomNotificationsGroupComp({
|
|||
)}
|
||||
</Box>
|
||||
</Header>
|
||||
<Box direction="Column" gap="100">
|
||||
{notifications.map((notification) => {
|
||||
const { event } = notification;
|
||||
|
||||
const displayName =
|
||||
getMemberDisplayName(room, event.sender) ??
|
||||
getMxIdLocalPart(event.sender) ??
|
||||
event.sender;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
||||
const getContent = (() => event.content) as GetContentCallback;
|
||||
|
||||
const relation = event.content['m.relates_to'];
|
||||
const replyEventId = relation?.['m.in_reply_to']?.event_id;
|
||||
const threadRootId =
|
||||
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
|
||||
|
||||
const memberPowerTag = getMemberPowerTag(event.sender);
|
||||
const tagColor = memberPowerTag?.color
|
||||
? accessibleTagColors?.get(memberPowerTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = memberPowerTag?.icon
|
||||
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={notification.event.event_id}
|
||||
style={{ padding: config.space.S400 }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
>
|
||||
<ModernLayout
|
||||
before={
|
||||
<AvatarBase>
|
||||
<Avatar size="300">
|
||||
<UserAvatar
|
||||
userId={event.sender}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcUrlToHttp(
|
||||
mx,
|
||||
senderAvatarMxc,
|
||||
useAuthentication,
|
||||
48,
|
||||
48,
|
||||
'crop'
|
||||
) ?? undefined
|
||||
: undefined
|
||||
}
|
||||
alt={displayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
</AvatarBase>
|
||||
}
|
||||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time
|
||||
ts={event.origin_server_ts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
<Chip
|
||||
data-event-id={event.event_id}
|
||||
onClick={handleOpenClick}
|
||||
variant="Secondary"
|
||||
radii="400"
|
||||
>
|
||||
<Text size="T200">Open</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
{replyEventId && (
|
||||
<Reply
|
||||
room={room}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getMemberPowerTag={getMemberPowerTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
|
||||
</ModernLayout>
|
||||
</SequenceCard>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<RoomMembersAPIProvider value={membersAPI}>
|
||||
<PowerLevelsContextProvider value={powerLevels}>
|
||||
<PowerLevelTagsContextProvider value={powerLevelTags}>
|
||||
{children}
|
||||
</PowerLevelTagsContextProvider>
|
||||
</PowerLevelsContextProvider>
|
||||
</RoomMembersAPIProvider>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -559,13 +524,16 @@ const useNotificationsSearchParams = (
|
|||
const DEFAULT_REFRESH_MS = 7000;
|
||||
|
||||
export function Notifications() {
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const [mediaAutoLoad] = useSetting('mediaAutoLoad');
|
||||
const [urlPreview] = useSetting('urlPreview');
|
||||
const [legacyUsernameColor] = useSetting('legacyUsernameColor');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
const ct = useClient();
|
||||
|
||||
const {
|
||||
hideActivity,
|
||||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
legacyUsernameColor,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
} = useSettings();
|
||||
const screenSize = useScreenSizeContext();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
|
|
@ -579,11 +547,7 @@ export function Notifications() {
|
|||
const onlyHighlight = notificationsSearchParams.only === 'highlight';
|
||||
const setOnlyHighlighted = (highlight: boolean) => {
|
||||
if (highlight) {
|
||||
setSearchParams(
|
||||
new URLSearchParams({
|
||||
only: 'highlight',
|
||||
})
|
||||
);
|
||||
setSearchParams(new URLSearchParams({ only: 'highlight' }));
|
||||
return;
|
||||
}
|
||||
setSearchParams();
|
||||
|
|
@ -631,6 +595,16 @@ export function Notifications() {
|
|||
}
|
||||
}, [timelineState, notificationTimeline, lastVItemIndex, loadTimeline]);
|
||||
|
||||
const notificationRooms = useMemo(
|
||||
() => Array.from(new Set(notificationTimeline.groups.map((g) => g.roomId))),
|
||||
[notificationTimeline]
|
||||
);
|
||||
|
||||
const roomToTimeline = useObservableValue(ct.roomToTimeline);
|
||||
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(notificationRooms);
|
||||
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
|
||||
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader balance>
|
||||
|
|
@ -710,8 +684,6 @@ export function Notifications() {
|
|||
{vItems.map((vItem) => {
|
||||
const group = notificationTimeline.groups[vItem.index];
|
||||
if (!group) return null;
|
||||
const groupRoom = mx.getRoom(group.roomId);
|
||||
if (!groupRoom) return null;
|
||||
|
||||
return (
|
||||
<VirtualTile
|
||||
|
|
@ -721,18 +693,25 @@ export function Notifications() {
|
|||
key={vItem.index}
|
||||
>
|
||||
<RoomNotificationsGroupComp
|
||||
room={groupRoom}
|
||||
roomId={group.roomId}
|
||||
notifications={group.notifications}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
getRoomName={getRoomName}
|
||||
getRoomAvatar={getRoomAvatar}
|
||||
hideActivity={hideActivity}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={
|
||||
legacyUsernameColor || mDirects.has(groupRoom.roomId)
|
||||
}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
>
|
||||
<RoomNotifications
|
||||
roomId={group.roomId}
|
||||
timeline={roomToTimeline.get(group.roomId)}
|
||||
notifications={group.notifications}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={legacyUsernameColor || mDirects.has(group.roomId)}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
getRoomName={getRoomName}
|
||||
/>
|
||||
</RoomNotificationsGroupComp>
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
|
|
@ -777,8 +756,11 @@ export function Notifications() {
|
|||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<Text size="L400">{(timelineState.error as Error).name}</Text>
|
||||
<Text size="T300">{(timelineState.error as Error).message}</Text>
|
||||
<Text size="L400">
|
||||
{timelineState.error instanceof Error
|
||||
? timelineState.error.message
|
||||
: 'Unknown Error'}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -463,11 +463,11 @@ export const getReactionGroups = (
|
|||
export const getLatestEditEvent = (
|
||||
sender: string,
|
||||
events: MessageEventWithoutRoomId[],
|
||||
redacted: Set<string>
|
||||
redacted: Set<string> | undefined
|
||||
): MessageEventWithoutRoomId | undefined => {
|
||||
let event: MessageEventWithoutRoomId | undefined;
|
||||
events.forEach((e) => {
|
||||
if (redacted.has(e.event_id) || sender !== e.sender) return;
|
||||
if (redacted?.has(e.event_id) || sender !== e.sender) return;
|
||||
if (!event) {
|
||||
event = e;
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue