"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",
"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",
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';
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);
+++ /dev/null
-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;
-};
--- /dev/null
+/* 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;
+};
export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
- const timelines = [];
+ const timelines: EventTimeline[] = [];
for (
let nextTimeline: EventTimeline | null = firstTimeline;
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';
({ 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))
animation: `${SlideUpAnime} 100ms ease-in-out`,
},
]);
+export const TypingText = style({
+ flexGrow: 1,
+});
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';
};
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])
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)}
ref={ref}
>
<TypingIndicator />
- <Text size="T300" truncate>
+ <Text className={css.TypingText} size="T300" truncate>
{typingNames.length === 1 && (
<>
<b>{typingNames[0]}</b>
</>
)}
</Text>
+ <IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
+ <Icon size="50" src={Icons.Cross} />
+ </IconButton>
</Box>
);
}
clickOutsideDeactivates: true,
}}
>
- <Modal size="500">
+ <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<TextViewer
name={body}
text={textState.data}
clickOutsideDeactivates: true,
}}
>
- <Modal size="500">
+ <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<PdfViewer
name={body}
src={pdfState.data}
clickOutsideDeactivates: true,
}}
>
- <Modal size="500">
+ <Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<ImageViewer
src={srcState.data}
alt={body}
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,
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,
)
);
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 = {
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,
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">
) : (
<AvatarFallback
style={{
- background: colorMXID(member.userId),
+ background: colorMXID(senderId),
color: 'white',
}}
>
import cons from './cons';
import { darkTheme, butterTheme, silverTheme } from '../../colors.css';
+import { onLightFontWeight, onDarkFontWeight } from '../../config.css';
function getSettings() {
const settings = localStorage.getItem('settings');
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();
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')
});
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');
}
--- /dev/null
+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',
+});
+++ /dev/null
-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';
enableMapSet();
-import './font';
import './index.scss';
import settings from './client/state/settings';
/* 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 {