Timeline-refactor-fixes (#1438)

* fix type

* fix missing member from reaction

* stop context menu event propagation in msg modal

* prevent encode blur hash from freezing app

* replace roboto font with inter and fix weight

* add recent emoji when selecting emoji

* fix room latest evt hook

* add option to drop typing status
This commit is contained in:
Ajay Bura 2023-10-07 18:19:01 +11:00 committed by GitHub
parent f9b895b32c
commit 1bdb7f4e3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 138 additions and 64 deletions

6
package-lock.json generated
View file

@ -10,7 +10,6 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@fontsource/inter": "4.5.14", "@fontsource/inter": "4.5.14",
"@fontsource/roboto": "4.5.8",
"@khanacademy/simple-markdown": "0.8.6", "@khanacademy/simple-markdown": "0.8.6",
"@matrix-org/olm": "3.2.14", "@matrix-org/olm": "3.2.14",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",
@ -875,11 +874,6 @@
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.14.tgz", "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.14.tgz",
"integrity": "sha512-JDC9AocdPLuGsASkvWw9hS5gtHE7K9dOwL98XLrk5yjYqxy4uVnScG58NUvFMJDVJRl/7c8Wnap6PEs+7Zvj1Q==" "integrity": "sha512-JDC9AocdPLuGsASkvWw9hS5gtHE7K9dOwL98XLrk5yjYqxy4uVnScG58NUvFMJDVJRl/7c8Wnap6PEs+7Zvj1Q=="
}, },
"node_modules/@fontsource/roboto": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.7", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",

View file

@ -20,7 +20,6 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@fontsource/inter": "4.5.14", "@fontsource/inter": "4.5.14",
"@fontsource/roboto": "4.5.8",
"@khanacademy/simple-markdown": "0.8.6", "@khanacademy/simple-markdown": "0.8.6",
"@matrix-org/olm": "3.2.14", "@matrix-org/olm": "3.2.14",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",

View file

@ -46,6 +46,7 @@ import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce'; import { useDebounce } from '../../hooks/useDebounce';
import { useThrottle } from '../../hooks/useThrottle'; import { useThrottle } from '../../hooks/useThrottle';
import { addRecentEmoji } from '../../plugins/recent-emoji';
const RECENT_GROUP_ID = 'recent_group'; const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group'; const SEARCH_GROUP_ID = 'search_group';
@ -697,7 +698,10 @@ export function EmojiBoard({
if (!emojiInfo) return; if (!emojiInfo) return;
if (emojiInfo.type === EmojiType.Emoji) { if (emojiInfo.type === EmojiType.Emoji) {
onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode); onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
if (!evt.altKey && !evt.shiftKey) requestClose(); if (!evt.altKey && !evt.shiftKey) {
addRecentEmoji(mx, emojiInfo.data);
requestClose();
}
} }
if (emojiInfo.type === EmojiType.CustomEmoji) { if (emojiInfo.type === EmojiType.CustomEmoji) {
onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode); onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);

View file

@ -1,29 +0,0 @@
import { MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { useEffect, useState } from 'react';
export const useRoomLatestEvent = (room: Room) => {
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) 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]);
return latestEvent;
};

View file

@ -0,0 +1,57 @@
/* 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';
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 (evt.isRelation()) continue;
if (evt.getType() === StateEvent.RoomMember) {
const membershipChanged = evt.getContent().membership !== evt.getPrevContent().membership;
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;
};

View file

@ -167,7 +167,7 @@ export const getFirstLinkedTimeline = (
export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => { export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward); const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
const timelines = []; const timelines: EventTimeline[] = [];
for ( for (
let nextTimeline: EventTimeline | null = firstTimeline; let nextTimeline: EventTimeline | null = firstTimeline;

View file

@ -19,7 +19,7 @@ import { getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart } from '../../utils/matrix';
import * as css from './RoomViewFollowing.css'; import * as css from './RoomViewFollowing.css';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomLatestEvent } from '../../hooks/useRoomLatestEvent'; import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders'; import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
import { EventReaders } from '../../components/event-readers'; import { EventReaders } from '../../components/event-readers';
@ -30,7 +30,7 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
({ className, room, ...props }, ref) => { ({ className, room, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const latestEvent = useRoomLatestEvent(room); const latestEvent = useRoomLatestRenderedEvent(room);
const latestEventReaders = useRoomEventReaders(room, latestEvent?.getId()); const latestEventReaders = useRoomEventReaders(room, latestEvent?.getId());
const followingMembers = latestEventReaders const followingMembers = latestEventReaders
.map((readerId) => room.getMember(readerId)) .map((readerId) => room.getMember(readerId))

View file

@ -22,3 +22,6 @@ export const RoomViewTyping = style([
animation: `${SlideUpAnime} 100ms ease-in-out`, animation: `${SlideUpAnime} 100ms ease-in-out`,
}, },
]); ]);
export const TypingText = style({
flexGrow: 1,
});

View file

@ -1,8 +1,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Box, Text, as } from 'folds'; import { Box, Icon, IconButton, Icons, Text, as } from 'folds';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers'; import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers';
import { TypingIndicator } from '../../components/typing-indicator'; import { TypingIndicator } from '../../components/typing-indicator';
import { getMemberDisplayName } from '../../utils/room'; import { getMemberDisplayName } from '../../utils/room';
@ -15,6 +15,7 @@ export type RoomViewTypingProps = {
}; };
export const RoomViewTyping = as<'div', RoomViewTypingProps>( export const RoomViewTyping = as<'div', RoomViewTypingProps>(
({ className, room, ...props }, ref) => { ({ className, room, ...props }, ref) => {
const setTypingMembers = useSetAtom(roomIdToTypingMembersAtom);
const mx = useMatrixClient(); const mx = useMatrixClient();
const typingMembers = useAtomValue( const typingMembers = useAtomValue(
useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room]) useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room])
@ -29,6 +30,18 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
return null; return null;
} }
const handleDropAll = () => {
// some homeserver does not timeout typing status
// we have given option so user can drop their typing status
typingMembers.forEach((member) =>
setTypingMembers({
type: 'DELETE',
roomId: room.roomId,
member,
})
);
};
return ( return (
<Box <Box
className={classNames(css.RoomViewTyping, className)} className={classNames(css.RoomViewTyping, className)}
@ -38,7 +51,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
ref={ref} ref={ref}
> >
<TypingIndicator /> <TypingIndicator />
<Text size="T300" truncate> <Text className={css.TypingText} size="T300" truncate>
{typingNames.length === 1 && ( {typingNames.length === 1 && (
<> <>
<b>{typingNames[0]}</b> <b>{typingNames[0]}</b>
@ -96,6 +109,9 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
</> </>
)} )}
</Text> </Text>
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
<Icon size="50" src={Icons.Cross} />
</IconButton>
</Box> </Box>
); );
} }

View file

@ -94,7 +94,7 @@ function ReadTextFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, '
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
<Modal size="500"> <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<TextViewer <TextViewer
name={body} name={body}
text={textState.data} text={textState.data}
@ -159,7 +159,7 @@ function ReadPdfFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, 'i
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
<Modal size="500"> <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<PdfViewer <PdfViewer
name={body} name={body}
src={pdfState.data} src={pdfState.data}

View file

@ -81,7 +81,7 @@ export const ImageContent = as<'div', ImageContentProps>(
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
<Modal size="500"> <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<ImageViewer <ImageViewer
src={srcState.data} src={srcState.data}
alt={body} alt={body}

View file

@ -12,6 +12,7 @@ import {
import { encryptFile, getImageInfo, getThumbnailContent, getVideoInfo } from '../../utils/matrix'; import { encryptFile, getImageInfo, getThumbnailContent, getVideoInfo } from '../../utils/matrix';
import { TUploadItem } from '../../state/roomInputDrafts'; import { TUploadItem } from '../../state/roomInputDrafts';
import { encodeBlurHash } from '../../utils/blurHash'; import { encodeBlurHash } from '../../utils/blurHash';
import { scaleYDimension } from '../../utils/common';
const generateThumbnailContent = async ( const generateThumbnailContent = async (
mx: MatrixClient, mx: MatrixClient,
@ -52,7 +53,7 @@ export const getImageMsgContent = async (
body: file.name, body: file.name,
}; };
if (imgEl) { if (imgEl) {
const blurHash = encodeBlurHash(imgEl); const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height));
const [thumbError, thumbContent] = await to( const [thumbError, thumbContent] = await to(
generateThumbnailContent( generateThumbnailContent(
mx, mx,
@ -107,7 +108,11 @@ export const getVideoMsgContent = async (
) )
); );
if (thumbContent && thumbContent.thumbnail_info) { if (thumbContent && thumbContent.thumbnail_info) {
thumbContent.thumbnail_info[MATRIX_BLUR_HASH_PROPERTY_NAME] = encodeBlurHash(videoEl); thumbContent.thumbnail_info[MATRIX_BLUR_HASH_PROPERTY_NAME] = encodeBlurHash(
videoEl,
512,
scaleYDimension(videoEl.videoWidth, 512, videoEl.videoHeight)
);
} }
if (thumbError) console.warn(thumbError); if (thumbError) console.warn(thumbError);
content.info = { content.info = {

View file

@ -99,10 +99,9 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
const senderId = mEvent.getSender(); const senderId = mEvent.getSender();
if (!senderId) return null; if (!senderId) return null;
const member = room.getMember(senderId); const member = room.getMember(senderId);
if (!member) return null; const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId;
const name = getName(member);
const avatarUrl = member.getAvatarUrl( const avatarUrl = member?.getAvatarUrl(
mx.baseUrl, mx.baseUrl,
100, 100,
100, 100,
@ -113,12 +112,12 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
return ( return (
<MenuItem <MenuItem
key={member.userId} key={senderId}
style={{ padding: `0 ${config.space.S200}` }} style={{ padding: `0 ${config.space.S200}` }}
radii="400" radii="400"
onClick={() => { onClick={() => {
requestClose(); requestClose();
openProfileViewer(member.userId, room.roomId); openProfileViewer(senderId, room.roomId);
}} }}
before={ before={
<Avatar size="200"> <Avatar size="200">
@ -127,7 +126,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
) : ( ) : (
<AvatarFallback <AvatarFallback
style={{ style={{
background: colorMXID(member.userId), background: colorMXID(senderId),
color: 'white', color: 'white',
}} }}
> >

View file

@ -4,6 +4,7 @@ import appDispatcher from '../dispatcher';
import cons from './cons'; import cons from './cons';
import { darkTheme, butterTheme, silverTheme } from '../../colors.css'; import { darkTheme, butterTheme, silverTheme } from '../../colors.css';
import { onLightFontWeight, onDarkFontWeight } from '../../config.css';
function getSettings() { function getSettings() {
const settings = localStorage.getItem('settings'); const settings = localStorage.getItem('settings');
@ -23,6 +24,7 @@ class Settings extends EventEmitter {
super(); super();
this.themeClasses = [lightTheme, silverTheme, darkTheme, butterTheme]; this.themeClasses = [lightTheme, silverTheme, darkTheme, butterTheme];
this.fontWeightClasses = [onLightFontWeight, onLightFontWeight, onDarkFontWeight, onDarkFontWeight]
this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme']; this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
this.themeIndex = this.getThemeIndex(); this.themeIndex = this.getThemeIndex();
@ -59,6 +61,7 @@ class Settings extends EventEmitter {
this.themes.forEach((themeName, index) => { this.themes.forEach((themeName, index) => {
if (themeName !== '') document.body.classList.remove(themeName); if (themeName !== '') document.body.classList.remove(themeName);
document.body.classList.remove(this.themeClasses[index]); document.body.classList.remove(this.themeClasses[index]);
document.body.classList.remove(this.fontWeightClasses[index]);
document.body.classList.remove('prism-light') document.body.classList.remove('prism-light')
document.body.classList.remove('prism-dark') document.body.classList.remove('prism-dark')
}); });
@ -71,6 +74,7 @@ class Settings extends EventEmitter {
if (this.themes[themeIndex] === undefined) return if (this.themes[themeIndex] === undefined) return
if (this.themes[themeIndex]) document.body.classList.add(this.themes[themeIndex]); if (this.themes[themeIndex]) document.body.classList.add(this.themes[themeIndex]);
document.body.classList.add(this.themeClasses[themeIndex]); document.body.classList.add(this.themeClasses[themeIndex]);
document.body.classList.add(this.fontWeightClasses[themeIndex]);
document.body.classList.add(themeIndex < 2 ? 'prism-light' : 'prism-dark'); document.body.classList.add(themeIndex < 2 ? 'prism-light' : 'prism-dark');
} }

26
src/config.css.ts Normal file
View file

@ -0,0 +1,26 @@
import { createTheme } from '@vanilla-extract/css';
import { config } from 'folds';
export const onLightFontWeight = createTheme(config.fontWeight, {
W100: '100',
W200: '200',
W300: '300',
W400: '420',
W500: '500',
W600: '600',
W700: '700',
W800: '800',
W900: '900',
});
export const onDarkFontWeight = createTheme(config.fontWeight, {
W100: '100',
W200: '200',
W300: '300',
W400: '350',
W500: '450',
W600: '550',
W700: '650',
W800: '750',
W900: '850',
});

View file

@ -1,5 +0,0 @@
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/inter/variable.css';

View file

@ -8,7 +8,6 @@ import { configClass, varsClass } from 'folds';
enableMapSet(); enableMapSet();
import './font';
import './index.scss'; import './index.scss';
import settings from './client/state/settings'; import settings from './client/state/settings';

View file

@ -158,7 +158,7 @@
/* font-weight */ /* font-weight */
--fw-light: 300; --fw-light: 300;
--fw-normal: 400; --fw-normal: 420;
--fw-medium: 500; --fw-medium: 500;
--fw-bold: 700; --fw-bold: 700;
@ -193,8 +193,8 @@
--fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99); --fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
--font-emoji: 'Twemoji'; --font-emoji: 'Twemoji';
--font-primary: 'Roboto', var(--font-emoji), sans-serif; --font-primary: 'InterVariable', var(--font-emoji), sans-serif;
--font-secondary: 'Roboto', var(--font-emoji), sans-serif; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
} }
.silver-theme { .silver-theme {
@ -291,8 +291,10 @@
/* override normal font weight for dark mode */ /* override normal font weight for dark mode */
--fw-normal: 350; --fw-normal: 350;
--fw-medium: 450;
--fw-bold: 550;
--font-secondary: 'InterVariable', 'Roboto', var(--font-emoji), sans-serif; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
} }
.butter-theme { .butter-theme {