mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-11 17:50:29 +03:00
Pinned Messages (#2081)
* add pinned room events hook * room pinned message - WIP * add room event hook * fetch pinned messages before displaying * use react-query in room event hook * disable staleTime and gc to 1 hour in room event hook * use room event hook in reply component * render pinned messages * add option to pin/unpin messages * remove message base from message placeholders and add variant * display message placeholder while loading pinned messages * render pinned event error * show no pinned message placeholder * fix message placeholder flickering
This commit is contained in:
parent
00d5553bcb
commit
35f0e400ad
14 changed files with 940 additions and 192 deletions
|
|
@ -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<MatrixEvent | null | undefined>(
|
||||
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() ? (
|
||||
<MessageDeletedContent />
|
||||
) : (
|
||||
<MessageFailedContent />
|
||||
);
|
||||
const fallbackBody = replyEvent?.isRedacted() ? (
|
||||
<MessageDeletedContent />
|
||||
) : (
|
||||
<MessageFailedContent />
|
||||
);
|
||||
|
||||
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 (
|
||||
<Box direction="Column" {...props} ref={ref}>
|
||||
{threadRootId && (
|
||||
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||
)}
|
||||
<ReplyLayout
|
||||
as="button"
|
||||
userColor={sender ? colorMXID(sender) : undefined}
|
||||
username={
|
||||
sender && (
|
||||
<Text size="T300" truncate>
|
||||
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
data-event-id={replyEventId}
|
||||
onClick={onClick}
|
||||
>
|
||||
{replyEvent !== undefined ? (
|
||||
<Text size="T300" truncate>
|
||||
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
||||
</Text>
|
||||
) : (
|
||||
<LinePlaceholder
|
||||
style={{
|
||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||
maxWidth: toRem(placeholderWidth),
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<Box direction="Column" {...props} ref={ref}>
|
||||
{threadRootId && (
|
||||
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||
)}
|
||||
</ReplyLayout>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
<ReplyLayout
|
||||
as="button"
|
||||
userColor={sender ? colorMXID(sender) : undefined}
|
||||
username={
|
||||
sender && (
|
||||
<Text size="T300" truncate>
|
||||
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
data-event-id={replyEventId}
|
||||
onClick={onClick}
|
||||
>
|
||||
{replyEvent !== undefined ? (
|
||||
<Text size="T300" truncate>
|
||||
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
||||
</Text>
|
||||
) : (
|
||||
<LinePlaceholder
|
||||
style={{
|
||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||
maxWidth: toRem(placeholderWidth),
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ReplyLayout>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
import React from 'react';
|
||||
import { as, toRem } from 'folds';
|
||||
import React, { useMemo } from 'react';
|
||||
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) => (
|
||||
<MessageBase>
|
||||
<CompactLayout
|
||||
{...props}
|
||||
ref={ref}
|
||||
before={
|
||||
<>
|
||||
<LinePlaceholder style={{ maxWidth: toRem(50) }} />
|
||||
<LinePlaceholder style={{ maxWidth: toRem(randomNumberBetween(40, 100)) }} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<LinePlaceholder style={{ maxWidth: toRem(randomNumberBetween(120, 500)) }} />
|
||||
</CompactLayout>
|
||||
</MessageBase>
|
||||
));
|
||||
export const CompactPlaceholder = as<'div', { variant?: ContainerColor }>(
|
||||
({ variant, ...props }, ref) => {
|
||||
const nameSize = useMemo(() => randomNumberBetween(40, 100), []);
|
||||
const msgSize = useMemo(() => randomNumberBetween(120, 500), []);
|
||||
|
||||
return (
|
||||
<CompactLayout
|
||||
{...props}
|
||||
ref={ref}
|
||||
before={
|
||||
<>
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(50) }} />
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(nameSize) }} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(msgSize) }} />
|
||||
</CompactLayout>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,39 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { Avatar, Box, as, color, toRem } from 'folds';
|
||||
import React, { CSSProperties, useMemo } from 'react';
|
||||
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) => (
|
||||
<MessageBase>
|
||||
<ModernLayout {...props} ref={ref} before={<Avatar style={avatarBg} size="300" />}>
|
||||
<Box style={contentMargin} grow="Yes" direction="Column" gap="200">
|
||||
<Box grow="Yes" gap="200" alignItems="Center" justifyContent="SpaceBetween">
|
||||
<LinePlaceholder style={{ maxWidth: toRem(randomNumberBetween(40, 100)) }} />
|
||||
<LinePlaceholder style={{ maxWidth: toRem(50) }} />
|
||||
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 (
|
||||
<ModernLayout
|
||||
{...props}
|
||||
ref={ref}
|
||||
before={
|
||||
<Avatar
|
||||
style={{ backgroundColor: color[variant ?? 'SurfaceVariant'].Container }}
|
||||
size="300"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box style={contentMargin} grow="Yes" direction="Column" gap="200">
|
||||
<Box grow="Yes" gap="200" alignItems="Center" justifyContent="SpaceBetween">
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(nameSize) }} />
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(50) }} />
|
||||
</Box>
|
||||
<Box grow="Yes" gap="200" wrap="Wrap">
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(msgSize) }} />
|
||||
<LinePlaceholder variant={variant} style={{ maxWidth: toRem(msg2Size) }} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box grow="Yes" gap="200" wrap="Wrap">
|
||||
<LinePlaceholder style={{ maxWidth: toRem(randomNumberBetween(80, 200)) }} />
|
||||
<LinePlaceholder style={{ maxWidth: toRem(randomNumberBetween(80, 200)) }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</ModernLayout>
|
||||
</MessageBase>
|
||||
));
|
||||
</ModernLayout>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<typeof LinePlaceholder>;
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<Box className={classNames(css.LinePlaceholder, className)} shrink="No" {...props} ref={ref} />
|
||||
));
|
||||
export const LinePlaceholder = as<'div', css.LinePlaceholderVariants>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
<Box
|
||||
className={classNames(css.LinePlaceholder({ variant }), className)}
|
||||
shrink="No"
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue