From 865447e88b4bd64b49c04951e96db09137785588 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:15:14 +0530 Subject: [PATCH 01/14] add pinned room events hook --- src/app/hooks/useRoomPinnedEvents.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/app/hooks/useRoomPinnedEvents.ts diff --git a/src/app/hooks/useRoomPinnedEvents.ts b/src/app/hooks/useRoomPinnedEvents.ts new file mode 100644 index 000000000..9ab1d6bda --- /dev/null +++ b/src/app/hooks/useRoomPinnedEvents.ts @@ -0,0 +1,15 @@ +import { useMemo } from 'react'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { Room } from 'matrix-js-sdk'; +import { StateEvent } from '../../types/matrix/room'; +import { useStateEvent } from './useStateEvent'; + +export const useRoomPinnedEvents = (room: Room): string[] => { + const pinEvent = useStateEvent(room, StateEvent.RoomPinnedEvents); + const events = useMemo(() => { + const content = pinEvent?.getContent(); + return content?.pinned ?? []; + }, [pinEvent]); + + return events; +}; From 7022f7e0db92c2f74b58b6b2f789c12f8e71674b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:15:34 +0530 Subject: [PATCH 02/14] room pinned message - WIP --- .../room-pin-menu/RoomPinMenu.css.ts | 6 ++ .../components/room-pin-menu/RoomPinMenu.tsx | 43 ++++++++++++ src/app/components/room-pin-menu/index.ts | 1 + src/app/features/room/RoomViewHeader.tsx | 69 ++++++++++++++++++- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/app/components/room-pin-menu/RoomPinMenu.css.ts create mode 100644 src/app/components/room-pin-menu/RoomPinMenu.tsx create mode 100644 src/app/components/room-pin-menu/index.ts diff --git a/src/app/components/room-pin-menu/RoomPinMenu.css.ts b/src/app/components/room-pin-menu/RoomPinMenu.css.ts new file mode 100644 index 000000000..d57fdf8a5 --- /dev/null +++ b/src/app/components/room-pin-menu/RoomPinMenu.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; +import { config } from 'folds'; + +export const PinMenuHeader = style({ + paddingLeft: config.space.S300, +}); diff --git a/src/app/components/room-pin-menu/RoomPinMenu.tsx b/src/app/components/room-pin-menu/RoomPinMenu.tsx new file mode 100644 index 000000000..7639f1639 --- /dev/null +++ b/src/app/components/room-pin-menu/RoomPinMenu.tsx @@ -0,0 +1,43 @@ +import React, { forwardRef } from 'react'; +import { Room } from 'matrix-js-sdk'; +import { Box, Header, Menu, Scroll, Text, toRem } from 'folds'; +import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; +import * as css from './RoomPinMenu.css'; + +type RoomPinMenuProps = { + room: Room; + requestClose: () => void; +}; +export const RoomPinMenu = forwardRef( + ({ room, requestClose }, ref) => { + const pinnedEvents = useRoomPinnedEvents(room); + const pinnedData = pinnedEvents.map((eventId) => room.findEventById(eventId) ?? eventId); + + return ( + +
+ + Pinned Messages + + + + +
+ + + {pinnedData.map((data) => { + if (typeof data === 'string') return

{data}

; + + return

{data.getContent().body}

; + })} +
+
+
+ ); + } +); diff --git a/src/app/components/room-pin-menu/index.ts b/src/app/components/room-pin-menu/index.ts new file mode 100644 index 000000000..65ddaeea1 --- /dev/null +++ b/src/app/components/room-pin-menu/index.ts @@ -0,0 +1 @@ +export * from './RoomPinMenu'; diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index ae80deb6e..7900ad29a 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -19,6 +19,7 @@ import { Line, PopOut, RectCords, + Badge, } from 'folds'; import { useNavigate } from 'react-router-dom'; import { JoinRule, Room } from 'matrix-js-sdk'; @@ -54,6 +55,8 @@ import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getViaServers } from '../../plugins/via-servers'; import { BackRouteHandler } from '../../components/BackRouteHandler'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; +import { RoomPinMenu } from '../../components/room-pin-menu'; type RoomMenuProps = { room: Room; @@ -180,14 +183,18 @@ export function RoomViewHeader() { const room = useRoom(); const space = useSpaceOptionally(); const [menuAnchor, setMenuAnchor] = useState(); + const [pinMenuAnchor, setPinMenuAnchor] = useState(); const mDirects = useAtomValue(mDirectAtom); + const pinnedEvents = useRoomPinnedEvents(room); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const ecryptedRoom = !!encryptionEvent; const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const name = useRoomName(room); const topic = useRoomTopic(room); - const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; + const avatarUrl = avatarMxc + ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); @@ -205,6 +212,10 @@ export function RoomViewHeader() { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; + const handleOpenPinMenu: MouseEventHandler = (evt) => { + setPinMenuAnchor(evt.currentTarget.getBoundingClientRect()); + }; + return ( @@ -297,6 +308,62 @@ export function RoomViewHeader() { )} )} + {pinnedEvents.length > 0 && ( + + Pinned Messages + + } + > + {(triggerRef) => ( + + + + {pinnedEvents.length} + + + + + )} + + )} + setPinMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setPinMenuAnchor(undefined)} /> + + } + /> {screenSize === ScreenSize.Desktop && ( Date: Sun, 8 Dec 2024 10:01:00 +0530 Subject: [PATCH 03/14] add room event hook --- src/app/hooks/useRoomEvent.ts | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/app/hooks/useRoomEvent.ts diff --git a/src/app/hooks/useRoomEvent.ts b/src/app/hooks/useRoomEvent.ts new file mode 100644 index 000000000..e7a9e3739 --- /dev/null +++ b/src/app/hooks/useRoomEvent.ts @@ -0,0 +1,50 @@ +import { MatrixEvent, Room } from 'matrix-js-sdk'; +import { useCallback, useEffect } from 'react'; +import to from 'await-to-js'; +import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; +import { useMatrixClient } from './useMatrixClient'; +import { AsyncStatus, useAsyncCallback } from './useAsyncCallback'; + +const useFetchEvent = (room: Room, eventId: string) => { + const mx = useMatrixClient(); + + const fetchEventCallback = useCallback(async () => { + const evt = await mx.fetchRoomEvent(room.roomId, eventId); + const mEvent = new MatrixEvent(evt); + + if (mEvent.isEncrypted() && mx.getCrypto()) { + await to(mEvent.attemptDecryption(mx.getCrypto() as CryptoBackend)); + } + + return mEvent; + }, [mx, room.roomId, eventId]); + + return useAsyncCallback(fetchEventCallback); +}; + +/** + * + * @param room + * @param eventId + * @returns `MatrixEvent`, `undefined` means loading, `null` means failure + */ +export const useRoomEvent = (room: Room, eventId: string) => { + const event = room.findEventById(eventId); + + const [fetchState, fetchEvent] = useFetchEvent(room, eventId); + + useEffect(() => { + if (!event) { + fetchEvent(); + } + }, [event, fetchEvent]); + + if (event) return event; + + if (fetchState.status === AsyncStatus.Idle || fetchState.status === AsyncStatus.Loading) + return undefined; + + if (fetchState.status === AsyncStatus.Success) return fetchState.data; + + return null; +}; From 26540fa1ba1253ea7a1b586ad5b480b5c0c6599a Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 8 Dec 2024 10:01:17 +0530 Subject: [PATCH 04/14] fetch pinned messages before displaying --- .../room-pin-menu/RoomPinMenu.css.ts | 6 -- .../components/room-pin-menu/RoomPinMenu.tsx | 43 ------------ src/app/features/room/RoomViewHeader.tsx | 2 +- .../room/room-pin-menu/RoomPinMenu.css.ts | 19 ++++++ .../room/room-pin-menu/RoomPinMenu.tsx | 68 +++++++++++++++++++ .../room}/room-pin-menu/index.ts | 0 6 files changed, 88 insertions(+), 50 deletions(-) delete mode 100644 src/app/components/room-pin-menu/RoomPinMenu.css.ts delete mode 100644 src/app/components/room-pin-menu/RoomPinMenu.tsx create mode 100644 src/app/features/room/room-pin-menu/RoomPinMenu.css.ts create mode 100644 src/app/features/room/room-pin-menu/RoomPinMenu.tsx rename src/app/{components => features/room}/room-pin-menu/index.ts (100%) diff --git a/src/app/components/room-pin-menu/RoomPinMenu.css.ts b/src/app/components/room-pin-menu/RoomPinMenu.css.ts deleted file mode 100644 index d57fdf8a5..000000000 --- a/src/app/components/room-pin-menu/RoomPinMenu.css.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { config } from 'folds'; - -export const PinMenuHeader = style({ - paddingLeft: config.space.S300, -}); diff --git a/src/app/components/room-pin-menu/RoomPinMenu.tsx b/src/app/components/room-pin-menu/RoomPinMenu.tsx deleted file mode 100644 index 7639f1639..000000000 --- a/src/app/components/room-pin-menu/RoomPinMenu.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { forwardRef } from 'react'; -import { Room } from 'matrix-js-sdk'; -import { Box, Header, Menu, Scroll, Text, toRem } from 'folds'; -import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; -import * as css from './RoomPinMenu.css'; - -type RoomPinMenuProps = { - room: Room; - requestClose: () => void; -}; -export const RoomPinMenu = forwardRef( - ({ room, requestClose }, ref) => { - const pinnedEvents = useRoomPinnedEvents(room); - const pinnedData = pinnedEvents.map((eventId) => room.findEventById(eventId) ?? eventId); - - return ( - -
- - Pinned Messages - - - - -
- - - {pinnedData.map((data) => { - if (typeof data === 'string') return

{data}

; - - return

{data.getContent().body}

; - })} -
-
-
- ); - } -); diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 7900ad29a..d4c63f68f 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -56,7 +56,7 @@ import { getViaServers } from '../../plugins/via-servers'; import { BackRouteHandler } from '../../components/BackRouteHandler'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; -import { RoomPinMenu } from '../../components/room-pin-menu'; +import { RoomPinMenu } from './room-pin-menu'; type RoomMenuProps = { room: Room; diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts new file mode 100644 index 000000000..f3dfa4123 --- /dev/null +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts @@ -0,0 +1,19 @@ +import { style } from '@vanilla-extract/css'; +import { config, toRem } from 'folds'; + +export const PinMenu = style({ + display: 'flex', + maxWidth: toRem(548), + width: '100vw', + maxHeight: '90vh', +}); + +export const PinMenuHeader = style({ + paddingLeft: config.space.S400, + paddingRight: config.space.S200, +}); + +export const PinMenuContent = style({ + paddingLeft: config.space.S200, + paddingBottom: config.space.S200, +}); diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx new file mode 100644 index 000000000..781c271fd --- /dev/null +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -0,0 +1,68 @@ +import React, { forwardRef } from 'react'; +import { Room } from 'matrix-js-sdk'; +import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text } from 'folds'; +import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; +import * as css from './RoomPinMenu.css'; +import { SequenceCard } from '../../../components/sequence-card'; +import { useRoomEvent } from '../../../hooks/useRoomEvent'; + +type PinnedMessageProps = { + room: Room; + eventId: string; +}; +function PinnedMessage({ room, eventId }: PinnedMessageProps) { + const pinnedEvent = useRoomEvent(room, eventId); + + if (pinnedEvent === undefined) return Loading...; + if (pinnedEvent === null) return Failed to load!; + + return ( + + {pinnedEvent.getSender()} + {pinnedEvent.getContent().body} + + ); +} + +type RoomPinMenuProps = { + room: Room; + requestClose: () => void; +}; +export const RoomPinMenu = forwardRef( + ({ room, requestClose }, ref) => { + const pinnedEvents = useRoomPinnedEvents(room); + + return ( + + +
+ + Pinned Messages + + + + + + +
+ + + + {pinnedEvents.map((eventId) => ( + + + + ))} + + + +
+
+ ); + } +); diff --git a/src/app/components/room-pin-menu/index.ts b/src/app/features/room/room-pin-menu/index.ts similarity index 100% rename from src/app/components/room-pin-menu/index.ts rename to src/app/features/room/room-pin-menu/index.ts From 5b88924e82b2b402ee08acc8dc4e3fd4053f8a73 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 8 Dec 2024 20:34:23 +0530 Subject: [PATCH 05/14] use react-query in room event hook --- src/app/hooks/useRoomEvent.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/hooks/useRoomEvent.ts b/src/app/hooks/useRoomEvent.ts index e7a9e3739..b01a89f71 100644 --- a/src/app/hooks/useRoomEvent.ts +++ b/src/app/hooks/useRoomEvent.ts @@ -1,9 +1,9 @@ import { MatrixEvent, Room } from 'matrix-js-sdk'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useMemo } from 'react'; import to from 'await-to-js'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; +import { useQuery } from '@tanstack/react-query'; import { useMatrixClient } from './useMatrixClient'; -import { AsyncStatus, useAsyncCallback } from './useAsyncCallback'; const useFetchEvent = (room: Room, eventId: string) => { const mx = useMatrixClient(); @@ -19,7 +19,7 @@ const useFetchEvent = (room: Room, eventId: string) => { return mEvent; }, [mx, room.roomId, eventId]); - return useAsyncCallback(fetchEventCallback); + return fetchEventCallback; }; /** @@ -29,22 +29,19 @@ const useFetchEvent = (room: Room, eventId: string) => { * @returns `MatrixEvent`, `undefined` means loading, `null` means failure */ export const useRoomEvent = (room: Room, eventId: string) => { - const event = room.findEventById(eventId); + const event = useMemo(() => room.findEventById(eventId), [room, eventId]); - const [fetchState, fetchEvent] = useFetchEvent(room, eventId); + const fetchEvent = useFetchEvent(room, eventId); - useEffect(() => { - if (!event) { - fetchEvent(); - } - }, [event, fetchEvent]); + const { data, error } = useQuery({ + enabled: event === undefined, + queryKey: [room.roomId, eventId], + queryFn: fetchEvent, + }); if (event) return event; + if (data) return data; + if (error) return null; - if (fetchState.status === AsyncStatus.Idle || fetchState.status === AsyncStatus.Loading) - return undefined; - - if (fetchState.status === AsyncStatus.Success) return fetchState.data; - - return null; + return undefined; }; From c13a1cdcd4af3f0442976784fa8f5feaab89d509 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:50:58 +0530 Subject: [PATCH 06/14] disable staleTime and gc to 1 hour in room event hook --- src/app/hooks/useRoomEvent.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/hooks/useRoomEvent.ts b/src/app/hooks/useRoomEvent.ts index b01a89f71..3ca2449fa 100644 --- a/src/app/hooks/useRoomEvent.ts +++ b/src/app/hooks/useRoomEvent.ts @@ -28,8 +28,15 @@ const useFetchEvent = (room: Room, eventId: string) => { * @param eventId * @returns `MatrixEvent`, `undefined` means loading, `null` means failure */ -export const useRoomEvent = (room: Room, eventId: string) => { - const event = useMemo(() => room.findEventById(eventId), [room, eventId]); +export const useRoomEvent = ( + room: Room, + eventId: string, + getLocally?: () => MatrixEvent | undefined +) => { + const event = useMemo(() => { + if (getLocally) return getLocally(); + return room.findEventById(eventId); + }, [room, eventId, getLocally]); const fetchEvent = useFetchEvent(room, eventId); @@ -37,6 +44,8 @@ export const useRoomEvent = (room: Room, eventId: string) => { enabled: event === undefined, queryKey: [room.roomId, eventId], queryFn: fetchEvent, + staleTime: Infinity, + gcTime: 60 * 60 * 1000, // 1hour }); if (event) return event; From c8674ea57b917a6fe1fb0c910754687bc70ac418 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:51:19 +0530 Subject: [PATCH 07/14] use room event hook in reply component --- src/app/components/message/Reply.tsx | 128 ++++++++----------- src/app/features/room/RoomTimeline.tsx | 2 - src/app/pages/client/inbox/Notifications.tsx | 10 +- 3 files changed, 62 insertions(+), 78 deletions(-) diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 82a9d9198..7687074e4 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -1,8 +1,6 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds'; -import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk'; -import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; -import React, { MouseEventHandler, ReactNode, useEffect, useMemo, useState } from 'react'; -import to from 'await-to-js'; +import { EventTimelineSet, Room } from 'matrix-js-sdk'; +import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import classNames from 'classnames'; import colorMXID from '../../../util/colorMXID'; import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room'; @@ -12,6 +10,7 @@ import { randomNumberBetween } from '../../utils/common'; import * as css from './Reply.css'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; +import { useRoomEvent } from '../../hooks/useRoomEvent'; type ReplyLayoutProps = { userColor?: string; @@ -46,7 +45,6 @@ export const ThreadIndicator = as<'div'>(({ ...props }, ref) => ( )); type ReplyProps = { - mx: MatrixClient; room: Room; timelineSet?: EventTimelineSet | undefined; replyEventId: string; @@ -54,78 +52,60 @@ type ReplyProps = { onClick?: MouseEventHandler | undefined; }; -export const Reply = as<'div', ReplyProps>((_, ref) => { - const { mx, room, timelineSet, replyEventId, threadRootId, onClick, ...props } = _; - const [replyEvent, setReplyEvent] = useState( - timelineSet?.findEventById(replyEventId) - ); - const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); +export const Reply = as<'div', ReplyProps>( + ({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => { + const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); + const getFromLocalTimeline = useCallback( + () => timelineSet?.findEventById(replyEventId), + [timelineSet, replyEventId] + ); + const replyEvent = useRoomEvent(room, replyEventId, getFromLocalTimeline); - const { body } = replyEvent?.getContent() ?? {}; - const sender = replyEvent?.getSender(); + const { body } = replyEvent?.getContent() ?? {}; + const sender = replyEvent?.getSender(); - const fallbackBody = replyEvent?.isRedacted() ? ( - - ) : ( - - ); + const fallbackBody = replyEvent?.isRedacted() ? ( + + ) : ( + + ); - useEffect(() => { - let disposed = false; - const loadEvent = async () => { - const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, replyEventId)); - const mEvent = new MatrixEvent(evt); - if (disposed) return; - if (err) { - setReplyEvent(null); - return; - } - if (mEvent.isEncrypted() && mx.getCrypto()) { - await to(mEvent.attemptDecryption(mx.getCrypto() as CryptoBackend)); - } - setReplyEvent(mEvent); - }; - if (replyEvent === undefined) loadEvent(); - return () => { - disposed = true; - }; - }, [replyEvent, mx, room, replyEventId]); + const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; + const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; - const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; - const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; - - return ( - - {threadRootId && ( - - )} - + {threadRootId && ( + + )} + + {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} + + ) + } + data-event-id={replyEventId} + onClick={onClick} + > + {replyEvent !== undefined ? ( - {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} + {badEncryption ? : bodyJSX} - ) - } - data-event-id={replyEventId} - onClick={onClick} - > - {replyEvent !== undefined ? ( - - {badEncryption ? : bodyJSX} - - ) : ( - - )} - - - ); -}); + ) : ( + + )} + +
+ ); + } +); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index a2738fcbf..4bff0c7cd 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -993,7 +993,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli reply={ replyEventId && ( {replyEventId && ( Date: Thu, 12 Dec 2024 19:51:47 +0530 Subject: [PATCH 08/14] render pinned messages --- .../room/room-pin-menu/RoomPinMenu.css.ts | 1 - .../room/room-pin-menu/RoomPinMenu.tsx | 363 ++++++++++++++++-- 2 files changed, 341 insertions(+), 23 deletions(-) diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts index f3dfa4123..9b0269b5d 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts @@ -15,5 +15,4 @@ export const PinMenuHeader = style({ export const PinMenuContent = style({ paddingLeft: config.space.S200, - paddingBottom: config.space.S200, }); diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index 781c271fd..e6cee99c1 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -1,26 +1,155 @@ -import React, { forwardRef } from 'react'; -import { Room } from 'matrix-js-sdk'; -import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text } from 'folds'; +/* eslint-disable react/destructuring-assignment */ +import React, { forwardRef, MouseEventHandler, useMemo, useRef } from 'react'; +import { MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; +import { + Avatar, + Box, + Chip, + config, + Header, + Icon, + IconButton, + Icons, + Menu, + Scroll, + Text, + toRem, +} from 'folds'; +import { Opts as LinkifyOpts } from 'linkifyjs'; +import { HTMLReactParserOptions } from 'html-react-parser'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; import * as css from './RoomPinMenu.css'; import { SequenceCard } from '../../../components/sequence-card'; import { useRoomEvent } from '../../../hooks/useRoomEvent'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { + AvatarBase, + ImageContent, + MessageNotDecryptedContent, + MessageUnsupportedContent, + ModernLayout, + MSticker, + RedactedContent, + Reply, + Time, + Username, +} from '../../../components/message'; +import { UserAvatar } from '../../../components/user-avatar'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { getEditedEvent, getMemberAvatarMxc, getMemberDisplayName } from '../../../utils/room'; +import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room'; +import colorMXID from '../../../../util/colorMXID'; +import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler'; +import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler'; +import { + factoryRenderLinkifyWithMention, + getReactCustomHtmlParser, + LINKIFY_OPTS, + makeMentionCustomProps, + renderMatrixMention, +} from '../../../plugins/react-custom-html-parser'; +import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer'; +import { RenderMessageContent } from '../../../components/RenderMessageContent'; +import { useSetting } from '../../../state/hooks/settings'; +import { settingsAtom } from '../../../state/settings'; +import * as customHtmlCss from '../../../styles/CustomHtml.css'; +import { EncryptedContent } from '../message'; +import { Image } from '../../../components/media'; +import { ImageViewer } from '../../../components/image-viewer'; +import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; +import { VirtualTile } from '../../../components/virtualizer'; + +function PinnedMessageLoading() { + return ; +} + +function PinnedMessageError() { + return Failed to load!; +} type PinnedMessageProps = { room: Room; eventId: string; + renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>; + onOpen: (roomId: string, eventId: string) => void; }; -function PinnedMessage({ room, eventId }: PinnedMessageProps) { +function PinnedMessage({ room, eventId, renderContent, onOpen }: PinnedMessageProps) { const pinnedEvent = useRoomEvent(room, eventId); + const useAuthentication = useMediaAuthentication(); + const mx = useMatrixClient(); + + if (pinnedEvent === undefined) return ; + if (pinnedEvent === null) return ; + + const sender = pinnedEvent.getSender()!; + + const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; + const senderAvatarMxc = getMemberAvatarMxc(room, sender); + const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; - if (pinnedEvent === undefined) return Loading...; - if (pinnedEvent === null) return Failed to load!; + const relation = pinnedEvent.getContent()['m.relates_to']; + const replyEventId = relation?.['m.in_reply_to']?.event_id; + const threadRootId = relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; + + const handleOpenClick: MouseEventHandler = (evt) => { + evt.stopPropagation(); + const evtId = evt.currentTarget.getAttribute('data-event-id'); + if (!evtId) return; + onOpen(room.roomId, evtId); + }; return ( - - {pinnedEvent.getSender()} - {pinnedEvent.getContent().body} - + + + } + /> + + + } + > + + + + + {displayName} + + + + + + Open + + + + {replyEventId && ( + + )} + {renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)} + ); } @@ -30,7 +159,174 @@ type RoomPinMenuProps = { }; export const RoomPinMenu = forwardRef( ({ room, requestClose }, ref) => { + const mx = useMatrixClient(); const pinnedEvents = useRoomPinnedEvents(room); + const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]); + const useAuthentication = useMediaAuthentication(); + const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); + const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); + const { navigateRoom } = useRoomNavigate(); + const scrollRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: sortedPinnedEvent.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 80, + overscan: 4, + }); + + const mentionClickHandler = useMentionClickHandler(room.roomId); + const spoilerClickHandler = useSpoilerClickHandler(); + + const linkifyOpts = useMemo( + () => ({ + ...LINKIFY_OPTS, + render: factoryRenderLinkifyWithMention((href) => + renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler)) + ), + }), + [mx, room, mentionClickHandler] + ); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, room.roomId, { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + handleMentionClick: mentionClickHandler, + }), + [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication] + ); + + const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>( + { + [MessageEvent.RoomMessage]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + + return ( + + ); + }, + [MessageEvent.RoomMessageEncrypted]: (event, displayName) => { + const eventId = event.getId()!; + const evtTimeline = room.getTimelineForEvent(eventId); + + const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId); + + if (!mEvent || !evtTimeline) { + return ( + + + {event.getType()} + {' event'} + + + ); + } + + return ( + + {() => { + if (mEvent.isRedacted()) return ; + if (mEvent.getType() === MessageEvent.Sticker) + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + if (mEvent.getType() === MessageEvent.RoomMessage) { + const editedEvent = getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet()); + const getContent = (() => + editedEvent?.getContent()['m.new_content'] ?? + mEvent.getContent()) as GetContentCallback; + + return ( + + ); + } + if (mEvent.getType() === MessageEvent.RoomMessageEncrypted) + return ( + + + + ); + return ( + + + + ); + }} + + ); + }, + [MessageEvent.Sticker]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + }, + }, + undefined, + (event) => { + if (event.isRedacted()) { + return ; + } + return ( + + + {event.getType()} + {' event'} + + + ); + } + ); return ( @@ -46,18 +342,41 @@ export const RoomPinMenu = forwardRef( - - - {pinnedEvents.map((eventId) => ( - - - - ))} + + +
+ {virtualizer.getVirtualItems().map((vItem) => { + const eventId = sortedPinnedEvent[vItem.index]; + if (!eventId) return null; + + return ( + + + + + + ); + })} +
From 01aafb76f3a0b921d2771b961c81a851d1dd0ce7 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:26:20 +0530 Subject: [PATCH 09/14] add option to pin/unpin messages --- src/app/features/room/RoomTimeline.tsx | 7 +- src/app/features/room/message/Message.tsx | 141 ++++++++++++------ .../room/room-pin-menu/RoomPinMenu.tsx | 69 ++++++++- 3 files changed, 164 insertions(+), 53 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 4bff0c7cd..4bcda585e 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -433,10 +433,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId)); const powerLevels = usePowerLevelsContext(); - const { canDoAction, canSendEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } = + usePowerLevelsAPI(powerLevels); const myPowerLevel = getPowerLevel(mx.getUserId() ?? ''); const canRedact = canDoAction('redact', myPowerLevel); const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel); const [editId, setEditId] = useState(); const roomToParents = useAtomValue(roomToParentsAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); @@ -983,6 +985,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli edit={editId === mEventId} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canSendReaction={canSendReaction} + canPinEvent={canPinEvent} imagePackRooms={imagePackRooms} relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} @@ -1054,6 +1057,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli edit={editId === mEventId} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canSendReaction={canSendReaction} + canPinEvent={canPinEvent} imagePackRooms={imagePackRooms} relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} @@ -1161,6 +1165,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli highlight={highlighted} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canSendReaction={canSendReaction} + canPinEvent={canPinEvent} imagePackRooms={imagePackRooms} relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index e9a2d7973..21b186422 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -35,6 +35,7 @@ import { useHover, useFocusWithin } from 'react-aria'; import { MatrixEvent, Room } from 'matrix-js-sdk'; import { Relations } from 'matrix-js-sdk/lib/models/relations'; import classNames from 'classnames'; +import { EventType, RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { AvatarBase, BubbleLayout, @@ -51,7 +52,12 @@ import { getMemberAvatarMxc, getMemberDisplayName, } from '../../../utils/room'; -import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias, mxcUrlToHttp } from '../../../utils/matrix'; +import { + getCanonicalAliasOrRoomId, + getMxIdLocalPart, + isRoomAlias, + mxcUrlToHttp, +} from '../../../utils/matrix'; import { MessageLayout, MessageSpacing } from '../../../state/settings'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; @@ -68,6 +74,8 @@ import { stopPropagation } from '../../../utils/keyboard'; import { getMatrixToRoomEvent } from '../../../plugins/matrix-to'; import { getViaServers } from '../../../plugins/via-servers'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; +import { StateEvent } from '../../../../types/matrix/room'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; @@ -235,9 +243,9 @@ export const MessageSourceCodeItem = as< const getContent = (evt: MatrixEvent) => evt.isEncrypted() ? { - [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(), - [`<== ORIGINAL_EVENT ==>`]: evt.event, - } + [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(), + [`<== ORIGINAL_EVENT ==>`]: evt.event, + } : evt.event; const getText = (): string => { @@ -340,6 +348,46 @@ export const MessageCopyLinkItem = as< ); }); +export const MessagePinItem = as< + 'button', + { + room: Room; + mEvent: MatrixEvent; + onClose?: () => void; + } +>(({ room, mEvent, onClose, ...props }, ref) => { + const mx = useMatrixClient(); + const pinnedEvents = useRoomPinnedEvents(room); + const isPinned = pinnedEvents.includes(mEvent.getId() ?? ''); + + const handlePin = () => { + const eventId = mEvent.getId(); + const pinContent: RoomPinnedEventsEventContent = { + pinned: Array.from(pinnedEvents).filter((id) => id !== eventId), + }; + if (!isPinned && eventId) { + pinContent.pinned.push(eventId); + } + mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, pinContent); + onClose?.(); + }; + + return ( + } + radii="300" + onClick={handlePin} + {...props} + ref={ref} + > + + {isPinned ? 'Unpin Message' : 'Pin Message'} + + + ); +}); + export const MessageDeleteItem = as< 'button', { @@ -611,6 +659,7 @@ export type MessageProps = { edit?: boolean; canDelete?: boolean; canSendReaction?: boolean; + canPinEvent?: boolean; imagePackRooms?: Room[]; relations?: Relations; messageLayout: MessageLayout; @@ -634,6 +683,7 @@ export const Message = as<'div', MessageProps>( edit, canDelete, canSendReaction, + canPinEvent, imagePackRooms, relations, messageLayout, @@ -949,29 +999,32 @@ export const Message = as<'div', MessageProps>( /> + {canPinEvent && ( + + )}
{((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && ( - <> - - - {!mEvent.isRedacted() && canDelete && ( - - )} - {mEvent.getSender() !== mx.getUserId() && ( - - )} - - - )} + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )}
} @@ -1095,26 +1148,26 @@ export const Event = as<'div', EventProps>(
{((!mEvent.isRedacted() && canDelete && !stateEvent) || (mEvent.getSender() !== mx.getUserId() && !stateEvent)) && ( - <> - - - {!mEvent.isRedacted() && canDelete && ( - - )} - {mEvent.getSender() !== mx.getUserId() && ( - - )} - - - )} + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )} } diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index e6cee99c1..be2f69531 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/destructuring-assignment */ -import React, { forwardRef, MouseEventHandler, useMemo, useRef } from 'react'; +import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react'; import { MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; import { Avatar, @@ -12,6 +12,7 @@ import { Icons, Menu, Scroll, + Spinner, Text, toRem, } from 'folds'; @@ -38,8 +39,13 @@ import { import { UserAvatar } from '../../../components/user-avatar'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { getEditedEvent, getMemberAvatarMxc, getMemberDisplayName } from '../../../utils/room'; -import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room'; +import { + getEditedEvent, + getMemberAvatarMxc, + getMemberDisplayName, + getStateEvent, +} from '../../../utils/room'; +import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room'; import colorMXID from '../../../../util/colorMXID'; import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler'; import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler'; @@ -60,6 +66,9 @@ import { Image } from '../../../components/media'; import { ImageViewer } from '../../../components/image-viewer'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { VirtualTile } from '../../../components/virtualizer'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; function PinnedMessageLoading() { return ; @@ -74,17 +83,29 @@ type PinnedMessageProps = { eventId: string; renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>; onOpen: (roomId: string, eventId: string) => void; + canPinEvent: boolean; }; -function PinnedMessage({ room, eventId, renderContent, onOpen }: PinnedMessageProps) { +function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: PinnedMessageProps) { const pinnedEvent = useRoomEvent(room, eventId); const useAuthentication = useMediaAuthentication(); const mx = useMatrixClient(); + const [unpinState, unpin] = useAsyncCallback( + useCallback(() => { + const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents); + const content = pinEvent?.getContent() ?? { pinned: [] }; + const newContent: RoomPinnedEventsEventContent = { + pinned: content.pinned.filter((id) => id !== eventId), + }; + + return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, newContent); + }, [room, eventId, mx]) + ); + if (pinnedEvent === undefined) return ; if (pinnedEvent === null) return ; const sender = pinnedEvent.getSender()!; - const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; const senderAvatarMxc = getMemberAvatarMxc(room, sender); const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; @@ -100,6 +121,11 @@ function PinnedMessage({ room, eventId, renderContent, onOpen }: PinnedMessagePr onOpen(room.roomId, evtId); }; + const handleUnpinClick: MouseEventHandler = (evt) => { + evt.stopPropagation(); + unpin(); + }; + return ( Open + {canPinEvent && ( + + {unpinState.status === AsyncStatus.Loading ? ( + + ) : ( + + )} + + )} {replyEventId && ( @@ -160,6 +202,11 @@ type RoomPinMenuProps = { export const RoomPinMenu = forwardRef( ({ room, requestClose }, ref) => { const mx = useMatrixClient(); + const userId = mx.getUserId()!; + const powerLevels = usePowerLevelsContext(); + const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, getPowerLevel(userId)); + const pinnedEvents = useRoomPinnedEvents(room); const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]); const useAuthentication = useMediaAuthentication(); @@ -328,6 +375,11 @@ export const RoomPinMenu = forwardRef( } ); + const handleOpen = (roomId: string, eventId: string) => { + navigateRoom(roomId, eventId); + requestClose(); + }; + return ( @@ -362,7 +414,7 @@ export const RoomPinMenu = forwardRef( key={vItem.index} > @@ -370,7 +422,8 @@ export const RoomPinMenu = forwardRef( room={room} eventId={eventId} renderContent={renderMatrixEvent} - onOpen={navigateRoom} + onOpen={handleOpen} + canPinEvent={canPinEvent} /> From 2026eda7ba3e21a1d39d2d2416a2e4cceea96953 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:04:52 +0530 Subject: [PATCH 10/14] remove message base from message placeholders and add variant --- .../placeholder/CompactPlaceholder.tsx | 24 ++++--- .../placeholder/DefaultPlaceholder.tsx | 41 ++++++++---- .../placeholder/LinePlaceholder.css.ts | 43 ++++++++++--- .../message/placeholder/LinePlaceholder.tsx | 13 +++- src/app/features/room/RoomTimeline.tsx | 64 ++++++++++++++----- 5 files changed, 135 insertions(+), 50 deletions(-) diff --git a/src/app/components/message/placeholder/CompactPlaceholder.tsx b/src/app/components/message/placeholder/CompactPlaceholder.tsx index a6be083ef..ffe62c51a 100644 --- a/src/app/components/message/placeholder/CompactPlaceholder.tsx +++ b/src/app/components/message/placeholder/CompactPlaceholder.tsx @@ -1,22 +1,28 @@ import React from 'react'; -import { as, toRem } from 'folds'; +import { as, ContainerColor, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; -import { CompactLayout, MessageBase } from '../layout'; +import { CompactLayout } from '../layout'; -export const CompactPlaceholder = as<'div'>(({ ...props }, ref) => ( - +export const CompactPlaceholder = as<'div', { variant?: ContainerColor }>( + ({ variant, ...props }, ref) => ( - - + + } > - + - -)); + ) +); diff --git a/src/app/components/message/placeholder/DefaultPlaceholder.tsx b/src/app/components/message/placeholder/DefaultPlaceholder.tsx index 5f0b57fae..2693d0d59 100644 --- a/src/app/components/message/placeholder/DefaultPlaceholder.tsx +++ b/src/app/components/message/placeholder/DefaultPlaceholder.tsx @@ -1,25 +1,42 @@ import React, { CSSProperties } from 'react'; -import { Avatar, Box, as, color, toRem } from 'folds'; +import { Avatar, Box, ContainerColor, as, color, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; -import { MessageBase, ModernLayout } from '../layout'; +import { ModernLayout } from '../layout'; const contentMargin: CSSProperties = { marginTop: toRem(3) }; -const avatarBg: CSSProperties = { backgroundColor: color.SurfaceVariant.Container }; -export const DefaultPlaceholder = as<'div'>(({ ...props }, ref) => ( - - }> +export const DefaultPlaceholder = as<'div', { variant?: ContainerColor }>( + ({ variant, ...props }, ref) => ( + + } + > - - + + - - + + - -)); + ) +); diff --git a/src/app/components/message/placeholder/LinePlaceholder.css.ts b/src/app/components/message/placeholder/LinePlaceholder.css.ts index 0baedf6e1..34ad76a3e 100644 --- a/src/app/components/message/placeholder/LinePlaceholder.css.ts +++ b/src/app/components/message/placeholder/LinePlaceholder.css.ts @@ -1,12 +1,35 @@ -import { style } from '@vanilla-extract/css'; -import { DefaultReset, color, config, toRem } from 'folds'; +import { ComplexStyleRule } from '@vanilla-extract/css'; +import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; +import { ContainerColor, DefaultReset, color, config, toRem } from 'folds'; -export const LinePlaceholder = style([ - DefaultReset, - { - width: '100%', - height: toRem(16), - borderRadius: config.radii.R300, - backgroundColor: color.SurfaceVariant.Container, +const getVariant = (variant: ContainerColor): ComplexStyleRule => ({ + backgroundColor: color[variant].Container, +}); + +export const LinePlaceholder = recipe({ + base: [ + DefaultReset, + { + width: '100%', + height: toRem(16), + borderRadius: config.radii.R300, + }, + ], + variants: { + variant: { + Background: getVariant('Background'), + Surface: getVariant('Surface'), + SurfaceVariant: getVariant('SurfaceVariant'), + Primary: getVariant('Primary'), + Secondary: getVariant('Secondary'), + Success: getVariant('Success'), + Warning: getVariant('Warning'), + Critical: getVariant('Critical'), + }, + }, + defaultVariants: { + variant: 'SurfaceVariant', }, -]); +}); + +export type LinePlaceholderVariants = RecipeVariants; diff --git a/src/app/components/message/placeholder/LinePlaceholder.tsx b/src/app/components/message/placeholder/LinePlaceholder.tsx index a5e7bd75a..58fc52c0e 100644 --- a/src/app/components/message/placeholder/LinePlaceholder.tsx +++ b/src/app/components/message/placeholder/LinePlaceholder.tsx @@ -3,6 +3,13 @@ import { Box, as } from 'folds'; import classNames from 'classnames'; import * as css from './LinePlaceholder.css'; -export const LinePlaceholder = as<'div'>(({ className, ...props }, ref) => ( - -)); +export const LinePlaceholder = as<'div', css.LinePlaceholderVariants>( + ({ className, variant, ...props }, ref) => ( + + ) +); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 4bcda585e..88dd5256a 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -1554,17 +1554,33 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {(canPaginateBack || !rangeAtStart) && (messageLayout === 1 ? ( <> - - - - - + + + + + + + + + + + + + + + ) : ( <> - - - + + + + + + + + + ))} @@ -1573,17 +1589,33 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {(!liveTimelineLinked || !rangeAtEnd) && (messageLayout === 1 ? ( <> - - - - - + + + + + + + + + + + + + + + ) : ( <> - - - + + + + + + + + + ))} From 3e6c5e945ce6f58f21606029d495733daeb1544c Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:05:15 +0530 Subject: [PATCH 11/14] display message placeholder while loading pinned messages --- src/app/features/room/room-pin-menu/RoomPinMenu.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index be2f69531..9dc59b0e0 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/destructuring-assignment */ import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react'; import { MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { Avatar, Box, @@ -14,7 +15,6 @@ import { Scroll, Spinner, Text, - toRem, } from 'folds'; import { Opts as LinkifyOpts } from 'linkifyjs'; import { HTMLReactParserOptions } from 'html-react-parser'; @@ -26,6 +26,7 @@ import { useRoomEvent } from '../../../hooks/useRoomEvent'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { AvatarBase, + DefaultPlaceholder, ImageContent, MessageNotDecryptedContent, MessageUnsupportedContent, @@ -67,13 +68,8 @@ import { ImageViewer } from '../../../components/image-viewer'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { VirtualTile } from '../../../components/virtualizer'; import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels'; -import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -function PinnedMessageLoading() { - return ; -} - function PinnedMessageError() { return Failed to load!; } @@ -102,7 +98,7 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi }, [room, eventId, mx]) ); - if (pinnedEvent === undefined) return ; + if (pinnedEvent === undefined) return ; if (pinnedEvent === null) return ; const sender = pinnedEvent.getSender()!; From 59e5f49eb166a4e776ece381b6f3a3bdcf67d03c Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:14:39 +0530 Subject: [PATCH 12/14] render pinned event error --- .../room/room-pin-menu/RoomPinMenu.tsx | 91 +++++++++---------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index 9dc59b0e0..55bad659d 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -6,6 +6,7 @@ import { Avatar, Box, Chip, + color, config, Header, Icon, @@ -70,10 +71,6 @@ import { VirtualTile } from '../../../components/virtualizer'; import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -function PinnedMessageError() { - return Failed to load!; -} - type PinnedMessageProps = { room: Room; eventId: string; @@ -98,18 +95,6 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi }, [room, eventId, mx]) ); - if (pinnedEvent === undefined) return ; - if (pinnedEvent === null) return ; - - const sender = pinnedEvent.getSender()!; - const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; - const senderAvatarMxc = getMemberAvatarMxc(room, sender); - const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; - - const relation = pinnedEvent.getContent()['m.relates_to']; - const replyEventId = relation?.['m.in_reply_to']?.event_id; - const threadRootId = relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; - const handleOpenClick: MouseEventHandler = (evt) => { evt.stopPropagation(); const evtId = evt.currentTarget.getAttribute('data-event-id'); @@ -122,6 +107,45 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi unpin(); }; + const renderOptions = () => ( + + + Open + + {canPinEvent && ( + + {unpinState.status === AsyncStatus.Loading ? ( + + ) : ( + + )} + + )} + + ); + + if (pinnedEvent === undefined) return ; + if (pinnedEvent === null) + return ( + + + Failed to load message! + + {renderOptions()} + + ); + + const sender = pinnedEvent.getSender()!; + const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; + const senderAvatarMxc = getMemberAvatarMxc(room, sender); + const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; return ( - - - Open - - {canPinEvent && ( - - {unpinState.status === AsyncStatus.Loading ? ( - - ) : ( - - )} - - )} - + {renderOptions()} - {replyEventId && ( + {pinnedEvent.replyEventId && ( )} @@ -214,7 +213,7 @@ export const RoomPinMenu = forwardRef( const virtualizer = useVirtualizer({ count: sortedPinnedEvent.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 80, + estimateSize: () => 75, overscan: 4, }); From 2199681ec3cc134c6e021a31c487ff0b90c66d3b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:36:04 +0530 Subject: [PATCH 13/14] show no pinned message placeholder --- src/app/features/room/RoomViewHeader.tsx | 44 ++++----- .../room/room-pin-menu/RoomPinMenu.tsx | 97 +++++++++++++------ 2 files changed, 87 insertions(+), 54 deletions(-) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index d4c63f68f..7ee1d3029 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -308,23 +308,23 @@ export function RoomViewHeader() { )} )} - {pinnedEvents.length > 0 && ( - - Pinned Messages - - } - > - {(triggerRef) => ( - + + Pinned Messages + + } + > + {(triggerRef) => ( + + {pinnedEvents.length > 0 && ( - - - )} - - )} + )} + + + )} + ( -
- {virtualizer.getVirtualItems().map((vItem) => { - const eventId = sortedPinnedEvent[vItem.index]; - if (!eventId) return null; + {sortedPinnedEvent.length > 0 ? ( +
+ {virtualizer.getVirtualItems().map((vItem) => { + const eventId = sortedPinnedEvent[vItem.index]; + if (!eventId) return null; - return ( - - - - - - ); - })} -
+ + + + + ); + })} +
+ ) : ( + + + + + No Pinned Messages + + + Users with sufficient power level can pin a messages from its context menu. + + + + )}
From f4d7e36579d7ec2d4e44dcd240ac3a8e030b1e2a Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:55:10 +0530 Subject: [PATCH 14/14] fix message placeholder flickering --- .../placeholder/CompactPlaceholder.tsx | 41 +++++++------ .../placeholder/DefaultPlaceholder.tsx | 59 +++++++++---------- src/app/features/room/RoomTimeline.tsx | 32 +++++----- 3 files changed, 64 insertions(+), 68 deletions(-) diff --git a/src/app/components/message/placeholder/CompactPlaceholder.tsx b/src/app/components/message/placeholder/CompactPlaceholder.tsx index ffe62c51a..e6168ae3e 100644 --- a/src/app/components/message/placeholder/CompactPlaceholder.tsx +++ b/src/app/components/message/placeholder/CompactPlaceholder.tsx @@ -1,28 +1,27 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { as, ContainerColor, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; import { CompactLayout } from '../layout'; export const CompactPlaceholder = as<'div', { variant?: ContainerColor }>( - ({ variant, ...props }, ref) => ( - - - - - } - > - - - ) + ({ variant, ...props }, ref) => { + const nameSize = useMemo(() => randomNumberBetween(40, 100), []); + const msgSize = useMemo(() => randomNumberBetween(120, 500), []); + + return ( + + + + + } + > + + + ); + } ); diff --git a/src/app/components/message/placeholder/DefaultPlaceholder.tsx b/src/app/components/message/placeholder/DefaultPlaceholder.tsx index 2693d0d59..725ac4b9f 100644 --- a/src/app/components/message/placeholder/DefaultPlaceholder.tsx +++ b/src/app/components/message/placeholder/DefaultPlaceholder.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties } from 'react'; +import React, { CSSProperties, useMemo } from 'react'; import { Avatar, Box, ContainerColor, as, color, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; @@ -7,36 +7,33 @@ import { ModernLayout } from '../layout'; const contentMargin: CSSProperties = { marginTop: toRem(3) }; export const DefaultPlaceholder = as<'div', { variant?: ContainerColor }>( - ({ variant, ...props }, ref) => ( - - } - > - - - - - - - - { + const nameSize = useMemo(() => randomNumberBetween(40, 100), []); + const msgSize = useMemo(() => randomNumberBetween(80, 200), []); + const msg2Size = useMemo(() => randomNumberBetween(80, 200), []); + + return ( + + } + > + + + + + + + + + - - - ) + + ); + } ); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 88dd5256a..63b3d3e2c 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -1555,31 +1555,31 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli (messageLayout === 1 ? ( <> - + - + - + - + - + ) : ( <> - + - + - + ))} @@ -1590,31 +1590,31 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli (messageLayout === 1 ? ( <> - + - + - + - + - + ) : ( <> - + - + - + ))}