Replace 'following' banner with WhatsApp-style delivery status checkmarks on own messages.
This commit is contained in:
parent
3652320b0f
commit
6c65dee82e
12 changed files with 180 additions and 296 deletions
|
|
@ -462,10 +462,6 @@
|
||||||
"drop_files": "Drop Files in \"{{name}}\"",
|
"drop_files": "Drop Files in \"{{name}}\"",
|
||||||
"drag_drop_desc": "Drag and drop files here or click for selection dialog",
|
"drag_drop_desc": "Drag and drop files here or click for selection dialog",
|
||||||
|
|
||||||
"is_following": " is following the conversation.",
|
|
||||||
"are_following": " are following the conversation.",
|
|
||||||
"others_following_count": "{{count}} others",
|
|
||||||
|
|
||||||
"pinned_messages": "Pinned Messages",
|
"pinned_messages": "Pinned Messages",
|
||||||
"no_pinned_messages": "No Pinned Messages",
|
"no_pinned_messages": "No Pinned Messages",
|
||||||
"no_pinned_messages_desc": "Users with sufficient permissions can pin messages from the message context menu.",
|
"no_pinned_messages_desc": "Users with sufficient permissions can pin messages from the message context menu.",
|
||||||
|
|
|
||||||
|
|
@ -462,10 +462,6 @@
|
||||||
"drop_files": "Перетащите файлы в \"{{name}}\"",
|
"drop_files": "Перетащите файлы в \"{{name}}\"",
|
||||||
"drag_drop_desc": "Перетащите файлы сюда или нажмите для выбора",
|
"drag_drop_desc": "Перетащите файлы сюда или нажмите для выбора",
|
||||||
|
|
||||||
"is_following": " читает чат.",
|
|
||||||
"are_following": " читают чат.",
|
|
||||||
"others_following_count": "ещё {{count}}",
|
|
||||||
|
|
||||||
"pinned_messages": "Закреплённые сообщения",
|
"pinned_messages": "Закреплённые сообщения",
|
||||||
"no_pinned_messages": "Нет закреплённых сообщений",
|
"no_pinned_messages": "Нет закреплённых сообщений",
|
||||||
"no_pinned_messages_desc": "Пользователи с достаточным уровнем прав могут закреплять сообщения через контекстное меню.",
|
"no_pinned_messages_desc": "Пользователи с достаточным уровнем прав могут закреплять сообщения через контекстное меню.",
|
||||||
|
|
|
||||||
30
src/app/components/message/MessageStatus.tsx
Normal file
30
src/app/components/message/MessageStatus.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Icon, Icons, color } from 'folds';
|
||||||
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
|
import { useMessageStatus, MessageDeliveryStatus } from '../../hooks/useMessageStatus';
|
||||||
|
|
||||||
|
export type MessageStatusProps = {
|
||||||
|
room: Room;
|
||||||
|
mEvent: MatrixEvent;
|
||||||
|
hideReadReceipts?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MessageStatus({ room, mEvent, hideReadReceipts }: MessageStatusProps) {
|
||||||
|
const status = useMessageStatus(room, mEvent);
|
||||||
|
|
||||||
|
if (status === MessageDeliveryStatus.Sending) {
|
||||||
|
return <Icon size="50" src={Icons.Clock} style={{ opacity: 0.5, flexShrink: 0 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === MessageDeliveryStatus.Read && !hideReadReceipts) {
|
||||||
|
return (
|
||||||
|
<Icon size="50" src={Icons.CheckTwice} style={{ color: color.Success.Main, flexShrink: 0 }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === MessageDeliveryStatus.Sent || status === MessageDeliveryStatus.Read) {
|
||||||
|
return <Icon size="50" src={Icons.Check} style={{ opacity: 0.5, flexShrink: 0 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -8,3 +8,4 @@ export * from './Time';
|
||||||
export * from './MsgTypeRenderers';
|
export * from './MsgTypeRenderers';
|
||||||
export * from './FileHeader';
|
export * from './FileHeader';
|
||||||
export * from './RenderBody';
|
export * from './RenderBody';
|
||||||
|
export * from './MessageStatus';
|
||||||
|
|
|
||||||
|
|
@ -625,7 +625,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottomRef.current.count += 1;
|
scrollToBottomRef.current.count += 1;
|
||||||
scrollToBottomRef.current.smooth = true;
|
scrollToBottomRef.current.smooth = mEvt.getSender() !== mx.getUserId();
|
||||||
|
|
||||||
setTimeline((ct) => ({
|
setTimeline((ct) => ({
|
||||||
...ct,
|
...ct,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,9 @@ import { RoomTimeline } from './RoomTimeline';
|
||||||
import { RoomViewTyping } from './RoomViewTyping';
|
import { RoomViewTyping } from './RoomViewTyping';
|
||||||
import { RoomTombstone } from './RoomTombstone';
|
import { RoomTombstone } from './RoomTombstone';
|
||||||
import { RoomInput } from './RoomInput';
|
import { RoomInput } from './RoomInput';
|
||||||
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
|
||||||
import { Page } from '../../components/page';
|
import { Page } from '../../components/page';
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
import { editableActiveElement } from '../../utils/dom';
|
import { editableActiveElement } from '../../utils/dom';
|
||||||
import { settingsAtom } from '../../state/settings';
|
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoom } from '../../hooks/useRoom';
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
|
|
@ -58,8 +55,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
|
||||||
|
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const { roomId } = room;
|
const { roomId } = room;
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
@ -133,7 +128,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
import { recipe } from '@vanilla-extract/recipes';
|
|
||||||
import { DefaultReset, color, config, toRem } from 'folds';
|
|
||||||
|
|
||||||
export const RoomViewFollowingPlaceholder = style([
|
|
||||||
DefaultReset,
|
|
||||||
{
|
|
||||||
height: toRem(28),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const RoomViewFollowing = recipe({
|
|
||||||
base: [
|
|
||||||
DefaultReset,
|
|
||||||
{
|
|
||||||
minHeight: toRem(28),
|
|
||||||
padding: `0 ${config.space.S400}`,
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: color.Surface.Container,
|
|
||||||
color: color.Surface.OnContainer,
|
|
||||||
outline: 'none',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
variants: {
|
|
||||||
clickable: {
|
|
||||||
true: {
|
|
||||||
cursor: 'pointer',
|
|
||||||
selectors: {
|
|
||||||
'&:hover, &:focus-visible': {
|
|
||||||
color: color.Primary.Main,
|
|
||||||
},
|
|
||||||
'&:active': {
|
|
||||||
color: color.Primary.Main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Icon,
|
|
||||||
Icons,
|
|
||||||
Modal,
|
|
||||||
Overlay,
|
|
||||||
OverlayBackdrop,
|
|
||||||
OverlayCenter,
|
|
||||||
Text,
|
|
||||||
as,
|
|
||||||
config,
|
|
||||||
} from 'folds';
|
|
||||||
import { Room } from 'matrix-js-sdk';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { getMemberDisplayName } from '../../utils/room';
|
|
||||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
|
||||||
import * as css from './RoomViewFollowing.css';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
|
||||||
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
|
|
||||||
import { EventReaders } from '../../components/event-readers';
|
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
|
||||||
|
|
||||||
export function RoomViewFollowingPlaceholder() {
|
|
||||||
return <div className={css.RoomViewFollowingPlaceholder} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RoomViewFollowingProps = {
|
|
||||||
room: Room;
|
|
||||||
};
|
|
||||||
export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
|
||||||
({ className, room, ...props }, ref) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const latestEvent = useRoomLatestRenderedEvent(room);
|
|
||||||
const latestEventReaders = useRoomEventReaders(room, latestEvent?.getId());
|
|
||||||
const names = latestEventReaders
|
|
||||||
.filter((readerId) => readerId !== mx.getUserId())
|
|
||||||
.map(
|
|
||||||
(readerId) => getMemberDisplayName(room, readerId) ?? getMxIdLocalPart(readerId) ?? readerId
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventId = latestEvent?.getId();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{eventId && (
|
|
||||||
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
|
||||||
<OverlayCenter>
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
|
||||||
onDeactivate: () => setOpen(false),
|
|
||||||
clickOutsideDeactivates: true,
|
|
||||||
escapeDeactivates: stopPropagation,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Modal variant="Surface" size="300">
|
|
||||||
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
|
||||||
</Modal>
|
|
||||||
</FocusTrap>
|
|
||||||
</OverlayCenter>
|
|
||||||
</Overlay>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
as={names.length > 0 ? 'button' : 'div'}
|
|
||||||
onClick={names.length > 0 ? () => setOpen(true) : undefined}
|
|
||||||
className={classNames(css.RoomViewFollowing({ clickable: names.length > 0 }), className)}
|
|
||||||
alignItems="Center"
|
|
||||||
justifyContent="End"
|
|
||||||
gap="200"
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{names.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.CheckTwice} />
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
{names.length === 1 && (
|
|
||||||
<>
|
|
||||||
<b>{names[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.is_following')}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{names.length === 2 && (
|
|
||||||
<>
|
|
||||||
<b>{names[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.and')}
|
|
||||||
</Text>
|
|
||||||
<b>{names[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.are_following')}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{names.length === 3 && (
|
|
||||||
<>
|
|
||||||
<b>{names[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{names[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.and')}
|
|
||||||
</Text>
|
|
||||||
<b>{names[2]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.are_following')}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{names.length > 3 && (
|
|
||||||
<>
|
|
||||||
<b>{names[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{names[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{names[2]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.and')}
|
|
||||||
</Text>
|
|
||||||
<b>{t('Room.others_following_count', { count: names.length - 3 })}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{t('Room.are_following')}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -42,6 +42,7 @@ import {
|
||||||
BubbleLayout,
|
BubbleLayout,
|
||||||
CompactLayout,
|
CompactLayout,
|
||||||
MessageBase,
|
MessageBase,
|
||||||
|
MessageStatus,
|
||||||
ModernLayout,
|
ModernLayout,
|
||||||
Time,
|
Time,
|
||||||
Username,
|
Username,
|
||||||
|
|
@ -53,10 +54,7 @@ import {
|
||||||
getMemberAvatarMxc,
|
getMemberAvatarMxc,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
} from '../../../utils/room';
|
} from '../../../utils/room';
|
||||||
import {
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
getMxIdLocalPart,
|
|
||||||
mxcUrlToHttp,
|
|
||||||
} from '../../../utils/matrix';
|
|
||||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||||
|
|
@ -467,9 +465,7 @@ export const MessageDeleteItem = as<
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<Text priority="400">
|
<Text priority="400">{t('Room.delete_confirm')}</Text>
|
||||||
{t('Room.delete_confirm')}
|
|
||||||
</Text>
|
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">
|
<Text size="L400">
|
||||||
{t('Room.reason')}{' '}
|
{t('Room.reason')}{' '}
|
||||||
|
|
@ -495,7 +491,9 @@ export const MessageDeleteItem = as<
|
||||||
aria-disabled={deleteState.status === AsyncStatus.Loading}
|
aria-disabled={deleteState.status === AsyncStatus.Loading}
|
||||||
>
|
>
|
||||||
<Text size="B400">
|
<Text size="B400">
|
||||||
{deleteState.status === AsyncStatus.Loading ? t('Room.deleting') : t('Room.delete')}
|
{deleteState.status === AsyncStatus.Loading
|
||||||
|
? t('Room.deleting')
|
||||||
|
: t('Room.delete')}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -598,9 +596,7 @@ export const MessageReportItem = as<
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<Text priority="400">
|
<Text priority="400">{t('Room.report_desc')}</Text>
|
||||||
{t('Room.report_desc')}
|
|
||||||
</Text>
|
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">{t('Room.report_reason')}</Text>
|
<Text size="L400">{t('Room.report_reason')}</Text>
|
||||||
<Input name="reasonInput" variant="Background" required />
|
<Input name="reasonInput" variant="Background" required />
|
||||||
|
|
@ -629,7 +625,9 @@ export const MessageReportItem = as<
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text size="B400">
|
<Text size="B400">
|
||||||
{reportState.status === AsyncStatus.Loading ? t('Room.reporting') : t('Room.report')}
|
{reportState.status === AsyncStatus.Loading
|
||||||
|
? t('Room.reporting')
|
||||||
|
: t('Room.report')}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -733,6 +731,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||||
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
|
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
|
||||||
|
|
||||||
|
const isOwnMessage = senderId === mx.getUserId();
|
||||||
const senderDisplayName =
|
const senderDisplayName =
|
||||||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||||
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
|
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
|
||||||
|
|
@ -746,6 +745,9 @@ export const Message = as<'div', MessageProps>(
|
||||||
|
|
||||||
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
|
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
|
||||||
|
|
||||||
|
const isBubble = messageLayout === MessageLayout.Bubble;
|
||||||
|
const showTimeInHeader = !isBubble || !isOwnMessage;
|
||||||
|
|
||||||
const headerJSX = !collapse && (
|
const headerJSX = !collapse && (
|
||||||
<Box
|
<Box
|
||||||
gap="300"
|
gap="300"
|
||||||
|
|
@ -762,17 +764,14 @@ export const Message = as<'div', MessageProps>(
|
||||||
onContextMenu={onUserClick}
|
onContextMenu={onUserClick}
|
||||||
onClick={onUsernameClick}
|
onClick={onUsernameClick}
|
||||||
>
|
>
|
||||||
<Text
|
<Text as="span" size={isBubble ? 'T300' : 'T400'} truncate>
|
||||||
as="span"
|
|
||||||
size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'}
|
|
||||||
truncate
|
|
||||||
>
|
|
||||||
<UsernameBold>{senderDisplayName}</UsernameBold>
|
<UsernameBold>{senderDisplayName}</UsernameBold>
|
||||||
</Text>
|
</Text>
|
||||||
</Username>
|
</Username>
|
||||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No" gap="100">
|
{showTimeInHeader && (
|
||||||
|
<Box shrink="No" gap="100" alignItems="Center">
|
||||||
{messageLayout === MessageLayout.Modern && hover && (
|
{messageLayout === MessageLayout.Modern && hover && (
|
||||||
<>
|
<>
|
||||||
<Text as="span" size="T200" priority="300">
|
<Text as="span" size="T200" priority="300">
|
||||||
|
|
@ -789,7 +788,11 @@ export const Message = as<'div', MessageProps>(
|
||||||
hour24Clock={hour24Clock}
|
hour24Clock={hour24Clock}
|
||||||
dateFormatString={dateFormatString}
|
dateFormatString={dateFormatString}
|
||||||
/>
|
/>
|
||||||
|
{isOwnMessage && (
|
||||||
|
<MessageStatus room={room} mEvent={mEvent} hideReadReceipts={hideReadReceipts} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -818,6 +821,18 @@ export const Message = as<'div', MessageProps>(
|
||||||
</AvatarBase>
|
</AvatarBase>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const bubbleMetaJSX = isBubble && isOwnMessage && (
|
||||||
|
<span className={css.BubbleMeta}>
|
||||||
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
|
<MessageStatus room={room} mEvent={mEvent} hideReadReceipts={hideReadReceipts} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
const msgContentJSX = (
|
const msgContentJSX = (
|
||||||
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
||||||
{reply}
|
{reply}
|
||||||
|
|
@ -1139,12 +1154,25 @@ export const Message = as<'div', MessageProps>(
|
||||||
{msgContentJSX}
|
{msgContentJSX}
|
||||||
</CompactLayout>
|
</CompactLayout>
|
||||||
)}
|
)}
|
||||||
{messageLayout === MessageLayout.Bubble && (
|
{isBubble && isOwnMessage && (
|
||||||
|
<Box gap="200" alignItems="End">
|
||||||
|
<BubbleLayout
|
||||||
|
style={{ flexGrow: 1, minWidth: 0 }}
|
||||||
|
before={avatarJSX}
|
||||||
|
header={headerJSX}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
|
{msgContentJSX}
|
||||||
|
</BubbleLayout>
|
||||||
|
{bubbleMetaJSX}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isBubble && !isOwnMessage && (
|
||||||
<BubbleLayout before={avatarJSX} header={headerJSX} onContextMenu={handleContextMenu}>
|
<BubbleLayout before={avatarJSX} header={headerJSX} onContextMenu={handleContextMenu}>
|
||||||
{msgContentJSX}
|
{msgContentJSX}
|
||||||
</BubbleLayout>
|
</BubbleLayout>
|
||||||
)}
|
)}
|
||||||
{messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
|
{messageLayout !== MessageLayout.Compact && !isBubble && (
|
||||||
<ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
|
<ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
|
||||||
{headerJSX}
|
{headerJSX}
|
||||||
{msgContentJSX}
|
{msgContentJSX}
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,11 @@ export const ReactionsContainer = style({
|
||||||
export const ReactionsTooltipText = style({
|
export const ReactionsTooltipText = style({
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const BubbleMeta = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: toRem(2),
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
|
||||||
75
src/app/hooks/useMessageStatus.ts
Normal file
75
src/app/hooks/useMessageStatus.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { MatrixEvent, Room, RoomEvent, RoomEventHandlerMap, EventStatus } from 'matrix-js-sdk';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
|
||||||
|
export enum MessageDeliveryStatus {
|
||||||
|
Sending = 'sending',
|
||||||
|
Sent = 'sent',
|
||||||
|
Read = 'read',
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeliveryStatus(
|
||||||
|
room: Room,
|
||||||
|
mEvent: MatrixEvent,
|
||||||
|
myUserId: string
|
||||||
|
): MessageDeliveryStatus | null {
|
||||||
|
if (mEvent.getSender() !== myUserId) return null;
|
||||||
|
|
||||||
|
const { status } = mEvent;
|
||||||
|
if (
|
||||||
|
status === EventStatus.SENDING ||
|
||||||
|
status === EventStatus.ENCRYPTING ||
|
||||||
|
status === EventStatus.QUEUED
|
||||||
|
) {
|
||||||
|
return MessageDeliveryStatus.Sending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === EventStatus.NOT_SENT || status === EventStatus.CANCELLED) return null;
|
||||||
|
|
||||||
|
const eventId = mEvent.getId();
|
||||||
|
if (!eventId || !eventId.startsWith('$')) return MessageDeliveryStatus.Sending;
|
||||||
|
|
||||||
|
const members = room.getMembers();
|
||||||
|
const otherMembers = members.filter((m) => m.userId !== myUserId && m.membership !== 'invite');
|
||||||
|
|
||||||
|
const hasReader = otherMembers.some((member) => room.hasUserReadEvent(member.userId, eventId));
|
||||||
|
if (hasReader) return MessageDeliveryStatus.Read;
|
||||||
|
|
||||||
|
return MessageDeliveryStatus.Sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMessageStatus(room: Room, mEvent: MatrixEvent): MessageDeliveryStatus | null {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const myUserId = mx.getUserId() ?? '';
|
||||||
|
const isOwnMessage = mEvent.getSender() === myUserId;
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<MessageDeliveryStatus | null>(() =>
|
||||||
|
getDeliveryStatus(room, mEvent, myUserId)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOwnMessage) return undefined;
|
||||||
|
|
||||||
|
setStatus(getDeliveryStatus(room, mEvent, myUserId));
|
||||||
|
|
||||||
|
const handleReceipt: RoomEventHandlerMap[RoomEvent.Receipt] = (_event, r) => {
|
||||||
|
if (r.roomId !== room.roomId) return;
|
||||||
|
setStatus(getDeliveryStatus(room, mEvent, myUserId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLocalEcho: RoomEventHandlerMap[RoomEvent.LocalEchoUpdated] = (event, r) => {
|
||||||
|
if (r.roomId !== room.roomId) return;
|
||||||
|
if (event.getId() !== mEvent.getId()) return;
|
||||||
|
setStatus(getDeliveryStatus(room, mEvent, myUserId));
|
||||||
|
};
|
||||||
|
|
||||||
|
room.on(RoomEvent.Receipt, handleReceipt);
|
||||||
|
room.on(RoomEvent.LocalEchoUpdated, handleLocalEcho);
|
||||||
|
return () => {
|
||||||
|
room.removeListener(RoomEvent.Receipt, handleReceipt);
|
||||||
|
room.removeListener(RoomEvent.LocalEchoUpdated, handleLocalEcho);
|
||||||
|
};
|
||||||
|
}, [room, mEvent, myUserId, isOwnMessage]);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
/* eslint-disable no-continue */
|
|
||||||
import { MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { settingsAtom } from '../state/settings';
|
|
||||||
import { useSetting } from '../state/hooks/settings';
|
|
||||||
import { MessageEvent, StateEvent } from '../../types/matrix/room';
|
|
||||||
import { isMembershipChanged, reactionOrEditEvent } from '../utils/room';
|
|
||||||
|
|
||||||
export const useRoomLatestRenderedEvent = (room: Room) => {
|
|
||||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
|
||||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
|
||||||
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
|
||||||
const [latestEvent, setLatestEvent] = useState<MatrixEvent>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getLatestEvent = (): MatrixEvent | undefined => {
|
|
||||||
const liveEvents = room.getLiveTimeline().getEvents();
|
|
||||||
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
|
|
||||||
const evt = liveEvents[i];
|
|
||||||
|
|
||||||
if (!evt) continue;
|
|
||||||
if (reactionOrEditEvent(evt)) continue;
|
|
||||||
if (evt.getType() === StateEvent.RoomMember) {
|
|
||||||
const membershipChanged = isMembershipChanged(evt);
|
|
||||||
if (membershipChanged && hideMembershipEvents) continue;
|
|
||||||
if (!membershipChanged && hideNickAvatarEvents) continue;
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
evt.getType() === MessageEvent.RoomMessage ||
|
|
||||||
evt.getType() === MessageEvent.RoomMessageEncrypted ||
|
|
||||||
evt.getType() === MessageEvent.Sticker ||
|
|
||||||
evt.getType() === StateEvent.RoomName ||
|
|
||||||
evt.getType() === StateEvent.RoomTopic ||
|
|
||||||
evt.getType() === StateEvent.RoomAvatar
|
|
||||||
) {
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showHiddenEvents) return evt;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimelineEvent: RoomEventHandlerMap[RoomEvent.Timeline] = () => {
|
|
||||||
setLatestEvent(getLatestEvent());
|
|
||||||
};
|
|
||||||
setLatestEvent(getLatestEvent());
|
|
||||||
|
|
||||||
room.on(RoomEvent.Timeline, handleTimelineEvent);
|
|
||||||
return () => {
|
|
||||||
room.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
|
||||||
};
|
|
||||||
}, [room, hideMembershipEvents, hideNickAvatarEvents, showHiddenEvents]);
|
|
||||||
|
|
||||||
return latestEvent;
|
|
||||||
};
|
|
||||||
Loading…
Add table
Reference in a new issue