Timeline-refactor-fixes (#1438)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Sat, 7 Oct 2023 07:19:01 +0000 (18:19 +1100)
committerGitHub <noreply@github.com>
Sat, 7 Oct 2023 07:19:01 +0000 (18:19 +1100)
* 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

18 files changed:
package-lock.json
package.json
src/app/components/emoji-board/EmojiBoard.tsx
src/app/hooks/useRoomLatestEvent.ts [deleted file]
src/app/hooks/useRoomLatestRenderedEvent.ts [new file with mode: 0644]
src/app/organisms/room/RoomTimeline.tsx
src/app/organisms/room/RoomViewFollowing.tsx
src/app/organisms/room/RoomViewTyping.css.ts
src/app/organisms/room/RoomViewTyping.tsx
src/app/organisms/room/message/FileContent.tsx
src/app/organisms/room/message/ImageContent.tsx
src/app/organisms/room/msgContent.ts
src/app/organisms/room/reaction-viewer/ReactionViewer.tsx
src/client/state/settings.js
src/config.css.ts [new file with mode: 0644]
src/font.js [deleted file]
src/index.jsx
src/index.scss

index 6f00efe823539c641d8b076aa16f4ed59973a071..6213a1df7717bccfc37e04cf8549e92d364f2cfe 100644 (file)
@@ -10,7 +10,6 @@
       "license": "AGPL-3.0-only",
       "dependencies": {
         "@fontsource/inter": "4.5.14",
-        "@fontsource/roboto": "4.5.8",
         "@khanacademy/simple-markdown": "0.8.6",
         "@matrix-org/olm": "3.2.14",
         "@tanstack/react-virtual": "3.0.0-beta.54",
       "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.14.tgz",
       "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": {
       "version": "0.11.7",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
index 83850a80ee04a138d6f5bfc5243eed50d72f1dc7..8ee5cc5480b24afa3d825d59e7724e65df6fa7d4 100644 (file)
@@ -20,7 +20,6 @@
   "license": "AGPL-3.0-only",
   "dependencies": {
     "@fontsource/inter": "4.5.14",
-    "@fontsource/roboto": "4.5.8",
     "@khanacademy/simple-markdown": "0.8.6",
     "@matrix-org/olm": "3.2.14",
     "@tanstack/react-virtual": "3.0.0-beta.54",
index a7309834cfe250f902c23d44466f45565e45d351..81730e3db41d2d486ff58f17a6cb38f235f38b34 100644 (file)
@@ -46,6 +46,7 @@ import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from
 import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
 import { useDebounce } from '../../hooks/useDebounce';
 import { useThrottle } from '../../hooks/useThrottle';
+import { addRecentEmoji } from '../../plugins/recent-emoji';
 
 const RECENT_GROUP_ID = 'recent_group';
 const SEARCH_GROUP_ID = 'search_group';
@@ -697,7 +698,10 @@ export function EmojiBoard({
     if (!emojiInfo) return;
     if (emojiInfo.type === EmojiType.Emoji) {
       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) {
       onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
diff --git a/src/app/hooks/useRoomLatestEvent.ts b/src/app/hooks/useRoomLatestEvent.ts
deleted file mode 100644 (file)
index 337438c..0000000
+++ /dev/null
@@ -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;
-};
diff --git a/src/app/hooks/useRoomLatestRenderedEvent.ts b/src/app/hooks/useRoomLatestRenderedEvent.ts
new file mode 100644 (file)
index 0000000..295d103
--- /dev/null
@@ -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;
+};
index 03f72a372d989a5547889b755837a811a3f2da1c..b3902d8b4650c147400f1bcabcda1f3748ae61a1 100644 (file)
@@ -167,7 +167,7 @@ export const getFirstLinkedTimeline = (
 
 export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
   const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
-  const timelines = [];
+  const timelines: EventTimeline[] = [];
 
   for (
     let nextTimeline: EventTimeline | null = firstTimeline;
index cd62c429df5af0049a385b12d14966359564e8f8..a49f70d9110689eb77d8fa7616d4b30ffb08c792 100644 (file)
@@ -19,7 +19,7 @@ import { getMemberDisplayName } from '../../utils/room';
 import { getMxIdLocalPart } from '../../utils/matrix';
 import * as css from './RoomViewFollowing.css';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useRoomLatestEvent } from '../../hooks/useRoomLatestEvent';
+import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
 import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
 import { EventReaders } from '../../components/event-readers';
 
@@ -30,7 +30,7 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
   ({ className, room, ...props }, ref) => {
     const mx = useMatrixClient();
     const [open, setOpen] = useState(false);
-    const latestEvent = useRoomLatestEvent(room);
+    const latestEvent = useRoomLatestRenderedEvent(room);
     const latestEventReaders = useRoomEventReaders(room, latestEvent?.getId());
     const followingMembers = latestEventReaders
       .map((readerId) => room.getMember(readerId))
index ef07316f227d5627b65e6d4d09f6acf5efa8e444..9def1aeece1944051e2918f3748bc0d977fa797c 100644 (file)
@@ -22,3 +22,6 @@ export const RoomViewTyping = style([
     animation: `${SlideUpAnime} 100ms ease-in-out`,
   },
 ]);
+export const TypingText = style({
+  flexGrow: 1,
+});
index c7c15ea5b643adabd31eb9cf0f0a7be01e9ea504..c393f3aee1590c0810931a9e568bd00f711ef347 100644 (file)
@@ -1,8 +1,8 @@
 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 classNames from 'classnames';
-import { useAtomValue } from 'jotai';
+import { useAtomValue, useSetAtom } from 'jotai';
 import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../../state/typingMembers';
 import { TypingIndicator } from '../../components/typing-indicator';
 import { getMemberDisplayName } from '../../utils/room';
@@ -15,6 +15,7 @@ export type RoomViewTypingProps = {
 };
 export const RoomViewTyping = as<'div', RoomViewTypingProps>(
   ({ className, room, ...props }, ref) => {
+    const setTypingMembers = useSetAtom(roomIdToTypingMembersAtom);
     const mx = useMatrixClient();
     const typingMembers = useAtomValue(
       useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room])
@@ -29,6 +30,18 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
       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 (
       <Box
         className={classNames(css.RoomViewTyping, className)}
@@ -38,7 +51,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
         ref={ref}
       >
         <TypingIndicator />
-        <Text size="T300" truncate>
+        <Text className={css.TypingText} size="T300" truncate>
           {typingNames.length === 1 && (
             <>
               <b>{typingNames[0]}</b>
@@ -96,6 +109,9 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
             </>
           )}
         </Text>
+        <IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
+          <Icon size="50" src={Icons.Cross} />
+        </IconButton>
       </Box>
     );
   }
index 8484d8499b9e2c637bd8e7490f51be4cc643d02e..c6bd45d414bcdb6cfa9448dcacf5ac204bf519a4 100644 (file)
@@ -94,7 +94,7 @@ function ReadTextFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, '
                 clickOutsideDeactivates: true,
               }}
             >
-              <Modal size="500">
+              <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
                 <TextViewer
                   name={body}
                   text={textState.data}
@@ -159,7 +159,7 @@ function ReadPdfFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, 'i
                 clickOutsideDeactivates: true,
               }}
             >
-              <Modal size="500">
+              <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
                 <PdfViewer
                   name={body}
                   src={pdfState.data}
index 2005b3accceccc3fafe2955449ec869717847191..485bed4f96ca35d10103e1ffa7fba1978bbd2d4c 100644 (file)
@@ -81,7 +81,7 @@ export const ImageContent = as<'div', ImageContentProps>(
                   clickOutsideDeactivates: true,
                 }}
               >
-                <Modal size="500">
+                <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
                   <ImageViewer
                     src={srcState.data}
                     alt={body}
index 5b03af91b82348bf21822587b3f10823f7205ec9..e4cf1cbc085ac8bfcd9b47a1feaf13ded4599e9b 100644 (file)
@@ -12,6 +12,7 @@ import {
 import { encryptFile, getImageInfo, getThumbnailContent, getVideoInfo } from '../../utils/matrix';
 import { TUploadItem } from '../../state/roomInputDrafts';
 import { encodeBlurHash } from '../../utils/blurHash';
+import { scaleYDimension } from '../../utils/common';
 
 const generateThumbnailContent = async (
   mx: MatrixClient,
@@ -52,7 +53,7 @@ export const getImageMsgContent = async (
     body: file.name,
   };
   if (imgEl) {
-    const blurHash = encodeBlurHash(imgEl);
+    const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height));
     const [thumbError, thumbContent] = await to(
       generateThumbnailContent(
         mx,
@@ -107,7 +108,11 @@ export const getVideoMsgContent = async (
       )
     );
     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);
     content.info = {
index 702f04a24759b940be6f8879dfdb0348132c2e6b..5626981efa8b08ce205c08aaff59b86a3f361bbb 100644 (file)
@@ -99,10 +99,9 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
                   const senderId = mEvent.getSender();
                   if (!senderId) return null;
                   const member = room.getMember(senderId);
-                  if (!member) return null;
-                  const name = getName(member);
+                  const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId;
 
-                  const avatarUrl = member.getAvatarUrl(
+                  const avatarUrl = member?.getAvatarUrl(
                     mx.baseUrl,
                     100,
                     100,
@@ -113,12 +112,12 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
 
                   return (
                     <MenuItem
-                      key={member.userId}
+                      key={senderId}
                       style={{ padding: `0 ${config.space.S200}` }}
                       radii="400"
                       onClick={() => {
                         requestClose();
-                        openProfileViewer(member.userId, room.roomId);
+                        openProfileViewer(senderId, room.roomId);
                       }}
                       before={
                         <Avatar size="200">
@@ -127,7 +126,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
                           ) : (
                             <AvatarFallback
                               style={{
-                                background: colorMXID(member.userId),
+                                background: colorMXID(senderId),
                                 color: 'white',
                               }}
                             >
index cc1193cef2eba827896fc4a62e0f7656f48d49b6..d39b2ca10f84afb57aea574a7fbdc793d2225de5 100644 (file)
@@ -4,6 +4,7 @@ import appDispatcher from '../dispatcher';
 
 import cons from './cons';
 import { darkTheme, butterTheme, silverTheme } from '../../colors.css';
+import { onLightFontWeight, onDarkFontWeight } from '../../config.css';
 
 function getSettings() {
   const settings = localStorage.getItem('settings');
@@ -23,6 +24,7 @@ class Settings extends EventEmitter {
     super();
 
     this.themeClasses = [lightTheme, silverTheme, darkTheme, butterTheme];
+    this.fontWeightClasses = [onLightFontWeight, onLightFontWeight, onDarkFontWeight, onDarkFontWeight]
     this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
     this.themeIndex = this.getThemeIndex();
 
@@ -59,6 +61,7 @@ class Settings extends EventEmitter {
     this.themes.forEach((themeName, index) => {
       if (themeName !== '') document.body.classList.remove(themeName);
       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-dark')
     });
@@ -71,6 +74,7 @@ class Settings extends EventEmitter {
     if (this.themes[themeIndex] === undefined) return
     if (this.themes[themeIndex]) document.body.classList.add(this.themes[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');
   }
 
diff --git a/src/config.css.ts b/src/config.css.ts
new file mode 100644 (file)
index 0000000..df04b90
--- /dev/null
@@ -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',
+});
diff --git a/src/font.js b/src/font.js
deleted file mode 100644 (file)
index 94b1f47..0000000
+++ /dev/null
@@ -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';
index e7256e251e17aae82ffcb27c57edac0668dd6d8d..a8a765703935e52cf9429714e59d66112585dac5 100644 (file)
@@ -8,7 +8,6 @@ import { configClass, varsClass } from 'folds';
 
 enableMapSet();
 
-import './font';
 import './index.scss';
 
 import settings from './client/state/settings';
index 04125a1ca0fb537aaee465247d2d69b2eea188e6..90ce60a8b6346123b1804925135fd69b93add0cb 100644 (file)
 
   /* font-weight */
   --fw-light: 300;
-  --fw-normal: 400;
+  --fw-normal: 420;
   --fw-medium: 500;
   --fw-bold: 700;
 
   --fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
 
   --font-emoji: 'Twemoji';
-  --font-primary: 'Roboto', var(--font-emoji), sans-serif;
-  --font-secondary: 'Roboto', var(--font-emoji), sans-serif;
+  --font-primary: 'InterVariable', var(--font-emoji), sans-serif;
+  --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
 }
 
 .silver-theme {
 
   /* override normal font weight for dark mode */
   --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 {