/* eslint-disable react/destructuring-assignment */ import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react'; import { MatrixEvent, Room } from 'matrix-js-sdk'; import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { Avatar, Box, Chip, color, config, Header, Icon, IconButton, Icons, Menu, Scroll, Spinner, 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, DefaultPlaceholder, ImageContent, MessageNotDecryptedContent, MessageUnsupportedContent, ModernLayout, MSticker, RedactedContent, Reply, Time, Username, UsernameBold, } from '../../../components/message'; import { UserAvatar } from '../../../components/user-avatar'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { getEditedEvent, getMemberAvatarMxc, getMemberDisplayName, getStateEvent, } from '../../../utils/room'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room'; 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'; import { usePowerLevelsContext } from '../../../hooks/usePowerLevels'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { ContainerColor } from '../../../styles/ContainerColor.css'; import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags'; import { useTheme } from '../../../hooks/useTheme'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { useIsDirectRoom } from '../../../hooks/useRoom'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; import { GetMemberPowerTag, getPowerTagIconSrc, useAccessiblePowerTagColors, useGetMemberPowerTag, } from '../../../hooks/useMemberPowerTag'; import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag'; type PinnedMessageProps = { room: Room; eventId: string; renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>; onOpen: (roomId: string, eventId: string) => void; canPinEvent: boolean; getMemberPowerTag: GetMemberPowerTag; accessibleTagColors: Map; legacyUsernameColor: boolean; hour24Clock: boolean; dateFormatString: string; }; function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent, getMemberPowerTag, accessibleTagColors, legacyUsernameColor, hour24Clock, dateFormatString, }: 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 as any, newContent); }, [room, eventId, mx]) ); const handleOpenClick: MouseEventHandler = (evt) => { evt.stopPropagation(); const evtId = evt.currentTarget.getAttribute('data-event-id'); if (!evtId) return; onOpen(room.roomId, evtId); }; const handleUnpinClick: MouseEventHandler = (evt) => { evt.stopPropagation(); 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; const memberPowerTag = getMemberPowerTag(sender); const tagColor = memberPowerTag?.color ? accessibleTagColors?.get(memberPowerTag.color) : undefined; const tagIconSrc = memberPowerTag?.icon ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor; return ( } /> } > {displayName} {tagIconSrc && } {renderOptions()} {pinnedEvent.replyEventId && ( )} {renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)} ); } type RoomPinMenuProps = { room: Room; requestClose: () => void; }; export const RoomPinMenu = forwardRef( ({ room, requestClose }, ref) => { const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, userId); const creatorsTag = useRoomCreatorsTag(); const powerLevelTags = usePowerLevelTags(room, powerLevels); const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); const theme = useTheme(); const accessibleTagColors = useAccessiblePowerTagColors( theme.kind, creatorsTag, powerLevelTags ); 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 direct = useIsDirectRoom(); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const { navigateRoom } = useRoomNavigate(); const scrollRef = useRef(null); const virtualizer = useVirtualizer({ count: sortedPinnedEvent.length, getScrollElement: () => scrollRef.current, estimateSize: () => 75, 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'} ); } ); const handleOpen = (roomId: string, eventId: string) => { navigateRoom(roomId, eventId); requestClose(); }; return (
Pinned Messages
{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. )}
); } );