mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-07 14:23:38 +01:00
rework general settings
This commit is contained in:
parent
5824d7c716
commit
97a0fede51
24 changed files with 1289 additions and 199 deletions
|
@ -1,6 +1,7 @@
|
|||
import { Box, Icon, IconSrc } from 'folds';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { CompactLayout, ModernLayout } from '..';
|
||||
import { MessageLayout } from '../../../state/settings';
|
||||
|
||||
export type EventContentProps = {
|
||||
messageLayout: number;
|
||||
|
@ -11,9 +12,9 @@ export type EventContentProps = {
|
|||
export function EventContent({ messageLayout, time, iconSrc, content }: EventContentProps) {
|
||||
const beforeJSX = (
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
{messageLayout === 1 && time}
|
||||
{messageLayout === MessageLayout.Compact && time}
|
||||
<Box
|
||||
grow={messageLayout === 1 ? undefined : 'Yes'}
|
||||
grow={messageLayout === MessageLayout.Compact ? undefined : 'Yes'}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
|
@ -25,11 +26,11 @@ export function EventContent({ messageLayout, time, iconSrc, content }: EventCon
|
|||
const msgContentJSX = (
|
||||
<Box justifyContent="SpaceBetween" alignItems="Baseline" gap="200">
|
||||
{content}
|
||||
{messageLayout !== 1 && time}
|
||||
{messageLayout !== MessageLayout.Compact && time}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return messageLayout === 1 ? (
|
||||
return messageLayout === MessageLayout.Compact ? (
|
||||
<CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>
|
||||
) : (
|
||||
<ModernLayout before={beforeJSX}>{msgContentJSX}</ModernLayout>
|
||||
|
|
|
@ -27,14 +27,14 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
|||
type ClientDrawerLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export function PageNav({ children }: ClientDrawerLayoutProps) {
|
||||
export function PageNav({ size, children }: ClientDrawerLayoutProps & css.PageNavVariants) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const isMobile = screenSize === ScreenSize.Mobile;
|
||||
|
||||
return (
|
||||
<Box
|
||||
grow={isMobile ? 'Yes' : undefined}
|
||||
className={css.PageNav}
|
||||
className={css.PageNav({ size })}
|
||||
shrink={isMobile ? 'Yes' : 'No'}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
|
@ -44,15 +44,17 @@ export function PageNav({ children }: ClientDrawerLayoutProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export const PageNavHeader = as<'header'>(({ className, ...props }, ref) => (
|
||||
<Header
|
||||
className={classNames(css.PageNavHeader, className)}
|
||||
variant="Background"
|
||||
size="600"
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
|
||||
({ className, outlined, ...props }, ref) => (
|
||||
<Header
|
||||
className={classNames(css.PageNavHeader({ outlined }), className)}
|
||||
variant="Background"
|
||||
size="600"
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export function PageNavContent({
|
||||
scrollRef,
|
||||
|
@ -88,11 +90,11 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
|
|||
));
|
||||
|
||||
export const PageHeader = as<'div', css.PageHeaderVariants>(
|
||||
({ className, balance, ...props }, ref) => (
|
||||
({ className, outlined, balance, ...props }, ref) => (
|
||||
<Header
|
||||
as="header"
|
||||
size="600"
|
||||
className={classNames(css.PageHeader({ balance }), className)}
|
||||
className={classNames(css.PageHeader({ balance, outlined }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
|
|
|
@ -2,30 +2,55 @@ import { style } from '@vanilla-extract/css';
|
|||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const PageNav = style({
|
||||
width: toRem(256),
|
||||
});
|
||||
|
||||
export const PageNavHeader = style({
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||
flexShrink: 0,
|
||||
borderBottomWidth: 1,
|
||||
|
||||
selectors: {
|
||||
'button&': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'button&[aria-pressed=true]': {
|
||||
backgroundColor: color.Background.ContainerActive,
|
||||
},
|
||||
'button&:hover, button&:focus-visible': {
|
||||
backgroundColor: color.Background.ContainerHover,
|
||||
},
|
||||
'button&:active': {
|
||||
backgroundColor: color.Background.ContainerActive,
|
||||
export const PageNav = recipe({
|
||||
variants: {
|
||||
size: {
|
||||
'400': {
|
||||
width: toRem(256),
|
||||
},
|
||||
'300': {
|
||||
width: toRem(222),
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: '400',
|
||||
},
|
||||
});
|
||||
export type PageNavVariants = RecipeVariants<typeof PageNav>;
|
||||
|
||||
export const PageNavHeader = recipe({
|
||||
base: {
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||
flexShrink: 0,
|
||||
selectors: {
|
||||
'button&': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'button&[aria-pressed=true]': {
|
||||
backgroundColor: color.Background.ContainerActive,
|
||||
},
|
||||
'button&:hover, button&:focus-visible': {
|
||||
backgroundColor: color.Background.ContainerHover,
|
||||
},
|
||||
'button&:active': {
|
||||
backgroundColor: color.Background.ContainerActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
outlined: {
|
||||
true: {
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
outlined: true,
|
||||
},
|
||||
});
|
||||
export type PageNavHeaderVariants = RecipeVariants<typeof PageNavHeader>;
|
||||
|
||||
export const PageNavContent = style({
|
||||
minHeight: '100%',
|
||||
|
@ -38,7 +63,6 @@ export const PageHeader = recipe({
|
|||
base: {
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
},
|
||||
variants: {
|
||||
balance: {
|
||||
|
@ -46,6 +70,14 @@ export const PageHeader = recipe({
|
|||
paddingLeft: config.space.S200,
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
true: {
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
outlined: true,
|
||||
},
|
||||
});
|
||||
export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
|
||||
|
|
27
src/app/components/setting-tile/SettingTile.tsx
Normal file
27
src/app/components/setting-tile/SettingTile.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Box, Text } from 'folds';
|
||||
|
||||
type SettingTileProps = {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
before?: ReactNode;
|
||||
after?: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export function SettingTile({ title, description, before, after, children }: SettingTileProps) {
|
||||
return (
|
||||
<Box alignItems="Center" gap="300">
|
||||
{before && <Box shrink="No">{before}</Box>}
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="T300">{title}</Text>
|
||||
{description && (
|
||||
<Text size="T200" priority="300">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
{after && <Box shrink="No">{after}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
1
src/app/components/setting-tile/index.ts
Normal file
1
src/app/components/setting-tile/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './SettingTile';
|
|
@ -85,7 +85,7 @@ import {
|
|||
reactionOrEditEvent,
|
||||
} from '../../utils/room';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../state/settings';
|
||||
import { openProfileViewer } from '../../../client/action/navigation';
|
||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||
import { Reactions, Message, Event, EncryptedContent } from './message';
|
||||
|
@ -1028,7 +1028,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
/>
|
||||
)}
|
||||
</Message>
|
||||
|
@ -1124,7 +1124,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1208,7 +1208,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const parsed = parseMemberEvent(mEvent);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1241,7 +1243,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1275,7 +1279,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1309,7 +1315,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1345,7 +1353,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1386,7 +1396,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
|
||||
const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Event
|
||||
|
@ -1541,7 +1553,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<div
|
||||
style={{
|
||||
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
|
||||
messageLayout === 1 ? config.space.S400 : toRem(64)
|
||||
messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
|
@ -1549,7 +1561,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
</div>
|
||||
)}
|
||||
{(canPaginateBack || !rangeAtStart) &&
|
||||
(messageLayout === 1 ? (
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<CompactPlaceholder />
|
||||
<CompactPlaceholder />
|
||||
|
@ -1568,7 +1580,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
{getItems().map(eventRenderer)}
|
||||
|
||||
{(!liveTimelineLinked || !rangeAtEnd) &&
|
||||
(messageLayout === 1 ? (
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<CompactPlaceholder ref={observeFrontAnchor} />
|
||||
<CompactPlaceholder />
|
||||
|
|
|
@ -51,7 +51,12 @@ import {
|
|||
getMemberAvatarMxc,
|
||||
getMemberDisplayName,
|
||||
} from '../../../utils/room';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import {
|
||||
getCanonicalAliasOrRoomId,
|
||||
getMxIdLocalPart,
|
||||
isRoomAlias,
|
||||
mxcUrlToHttp,
|
||||
} from '../../../utils/matrix';
|
||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
|
@ -235,9 +240,9 @@ export const MessageSourceCodeItem = as<
|
|||
const getContent = (evt: MatrixEvent) =>
|
||||
evt.isEncrypted()
|
||||
? {
|
||||
[`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
|
||||
[`<== ORIGINAL_EVENT ==>`]: evt.event,
|
||||
}
|
||||
[`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
|
||||
[`<== ORIGINAL_EVENT ==>`]: evt.event,
|
||||
}
|
||||
: evt.event;
|
||||
|
||||
const getText = (): string => {
|
||||
|
@ -666,7 +671,7 @@ export const Message = as<'div', MessageProps>(
|
|||
const headerJSX = !collapse && (
|
||||
<Box
|
||||
gap="300"
|
||||
direction={messageLayout === 1 ? 'RowReverse' : 'Row'}
|
||||
direction={messageLayout === MessageLayout.Compact ? 'RowReverse' : 'Row'}
|
||||
justifyContent="SpaceBetween"
|
||||
alignItems="Baseline"
|
||||
grow="Yes"
|
||||
|
@ -678,12 +683,12 @@ export const Message = as<'div', MessageProps>(
|
|||
onContextMenu={onUserClick}
|
||||
onClick={onUsernameClick}
|
||||
>
|
||||
<Text as="span" size={messageLayout === 2 ? 'T300' : 'T400'} truncate>
|
||||
<Text as="span" size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'} truncate>
|
||||
<b>{senderDisplayName}</b>
|
||||
</Text>
|
||||
</Username>
|
||||
<Box shrink="No" gap="100">
|
||||
{messageLayout === 0 && hover && (
|
||||
{messageLayout === MessageLayout.Modern && hover && (
|
||||
<>
|
||||
<Text as="span" size="T200" priority="300">
|
||||
{senderId}
|
||||
|
@ -693,12 +698,12 @@ export const Message = as<'div', MessageProps>(
|
|||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === 1} />
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const avatarJSX = !collapse && messageLayout !== 1 && (
|
||||
const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
|
||||
<AvatarBase>
|
||||
<Avatar
|
||||
className={css.MessageAvatar}
|
||||
|
@ -952,26 +957,26 @@ export const Message = as<'div', MessageProps>(
|
|||
</Box>
|
||||
{((!mEvent.isRedacted() && canDelete) ||
|
||||
mEvent.getSender() !== mx.getUserId()) && (
|
||||
<>
|
||||
<Line size="300" />
|
||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||
{!mEvent.isRedacted() && canDelete && (
|
||||
<MessageDeleteItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
{mEvent.getSender() !== mx.getUserId() && (
|
||||
<MessageReportItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Line size="300" />
|
||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||
{!mEvent.isRedacted() && canDelete && (
|
||||
<MessageDeleteItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
{mEvent.getSender() !== mx.getUserId() && (
|
||||
<MessageReportItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
|
@ -990,18 +995,18 @@ export const Message = as<'div', MessageProps>(
|
|||
</Menu>
|
||||
</div>
|
||||
)}
|
||||
{messageLayout === 1 && (
|
||||
{messageLayout === MessageLayout.Compact && (
|
||||
<CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
|
||||
{msgContentJSX}
|
||||
</CompactLayout>
|
||||
)}
|
||||
{messageLayout === 2 && (
|
||||
{messageLayout === MessageLayout.Bubble && (
|
||||
<BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
|
||||
{headerJSX}
|
||||
{msgContentJSX}
|
||||
</BubbleLayout>
|
||||
)}
|
||||
{messageLayout !== 1 && messageLayout !== 2 && (
|
||||
{messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
|
||||
<ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
|
||||
{headerJSX}
|
||||
{msgContentJSX}
|
||||
|
@ -1095,26 +1100,26 @@ export const Event = as<'div', EventProps>(
|
|||
</Box>
|
||||
{((!mEvent.isRedacted() && canDelete && !stateEvent) ||
|
||||
(mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
|
||||
<>
|
||||
<Line size="300" />
|
||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||
{!mEvent.isRedacted() && canDelete && (
|
||||
<MessageDeleteItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
{mEvent.getSender() !== mx.getUserId() && (
|
||||
<MessageReportItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Line size="300" />
|
||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||
{!mEvent.isRedacted() && canDelete && (
|
||||
<MessageDeleteItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
{mEvent.getSender() !== mx.getUserId() && (
|
||||
<MessageReportItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
|
|
617
src/app/features/settings/General.tsx
Normal file
617
src/app/features/settings/General.tsx
Normal file
|
@ -0,0 +1,617 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
CSSProperties,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
as,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
config,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Switch,
|
||||
Text,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { Page, PageContent, PageHeader } from '../../components/page';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { MessageLayout, MessageSpacing, settingsAtom } from '../../state/settings';
|
||||
import { SettingTile } from '../../components/setting-tile';
|
||||
import { KeySymbol } from '../../utils/key-symbol';
|
||||
import { isMacOS } from '../../utils/user-agent';
|
||||
import {
|
||||
DarkTheme,
|
||||
LightTheme,
|
||||
Theme,
|
||||
ThemeKind,
|
||||
useSystemThemeKind,
|
||||
useThemeNames,
|
||||
useThemes,
|
||||
} from '../../hooks/useTheme';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useMessageLayoutItems } from '../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../hooks/useMessageSpacing';
|
||||
|
||||
const SequenceCardStyle: CSSProperties = {
|
||||
padding: config.space.S300,
|
||||
};
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
themeNames: Record<string, string>;
|
||||
themes: Theme[];
|
||||
selected: Theme;
|
||||
onSelect: (theme: Theme) => void;
|
||||
};
|
||||
const ThemeSelector = as<'div', ThemeSelectorProps>(
|
||||
({ themeNames, themes, selected, onSelect, ...props }, ref) => (
|
||||
<Menu {...props} ref={ref}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{themes.map((theme) => (
|
||||
<MenuItem
|
||||
key={theme.id}
|
||||
size="300"
|
||||
variant={theme.id === selected.id ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => onSelect(theme)}
|
||||
>
|
||||
<Text size="T300">{themeNames[theme.id] ?? theme.id}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
)
|
||||
);
|
||||
|
||||
function SelectTheme({ disabled }: { disabled?: boolean }) {
|
||||
const themes = useThemes();
|
||||
const themeNames = useThemeNames();
|
||||
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
const handleThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleThemeSelect = (theme: Theme) => {
|
||||
setThemeId(theme.id);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Primary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={disabled ? undefined : handleThemeMenu}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<Text size="T300">{themeNames[selectedTheme.id] ?? selectedTheme.id}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<ThemeSelector
|
||||
themeNames={themeNames}
|
||||
themes={themes}
|
||||
selected={selectedTheme}
|
||||
onSelect={handleThemeSelect}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemThemePreferences() {
|
||||
const themeKind = useSystemThemeKind();
|
||||
const themeNames = useThemeNames();
|
||||
const themes = useThemes();
|
||||
const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
|
||||
const lightThemes = themes.filter((theme) => theme.kind === ThemeKind.Light);
|
||||
const darkThemes = themes.filter((theme) => theme.kind === ThemeKind.Dark);
|
||||
|
||||
const selectedLightTheme = lightThemes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
const selectedDarkTheme = darkThemes.find((theme) => theme.id === darkThemeId) ?? DarkTheme;
|
||||
|
||||
const [ltCords, setLTCords] = useState<RectCords>();
|
||||
const [dtCords, setDTCords] = useState<RectCords>();
|
||||
|
||||
const handleLightThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setLTCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
const handleDarkThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setDTCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleLightThemeSelect = (theme: Theme) => {
|
||||
setLightThemeId(theme.id);
|
||||
setLTCords(undefined);
|
||||
};
|
||||
|
||||
const handleDarkThemeSelect = (theme: Theme) => {
|
||||
setDarkThemeId(theme.id);
|
||||
setDTCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box wrap="Wrap" gap="400">
|
||||
<SettingTile
|
||||
title="Light Theme:"
|
||||
after={
|
||||
<Chip
|
||||
variant={themeKind === ThemeKind.Light ? 'Primary' : 'Secondary'}
|
||||
outlined={themeKind === ThemeKind.Light}
|
||||
radii="Pill"
|
||||
after={<Icon size="200" src={Icons.ChevronBottom} />}
|
||||
onClick={handleLightThemeMenu}
|
||||
>
|
||||
<Text size="B300">{themeNames[selectedLightTheme.id] ?? selectedLightTheme.id}</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
<PopOut
|
||||
anchor={ltCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setLTCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<ThemeSelector
|
||||
themeNames={themeNames}
|
||||
themes={lightThemes}
|
||||
selected={selectedLightTheme}
|
||||
onSelect={handleLightThemeSelect}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Dark Theme:"
|
||||
after={
|
||||
<Chip
|
||||
variant={themeKind === ThemeKind.Dark ? 'Primary' : 'Secondary'}
|
||||
outlined={themeKind === ThemeKind.Dark}
|
||||
radii="Pill"
|
||||
after={<Icon size="200" src={Icons.ChevronBottom} />}
|
||||
onClick={handleDarkThemeMenu}
|
||||
>
|
||||
<Text size="B300">{themeNames[selectedDarkTheme.id] ?? selectedDarkTheme.id}</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
<PopOut
|
||||
anchor={dtCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setDTCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<ThemeSelector
|
||||
themeNames={themeNames}
|
||||
themes={darkThemes}
|
||||
selected={selectedDarkTheme}
|
||||
onSelect={handleDarkThemeSelect}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function PageZoomInput() {
|
||||
const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
|
||||
const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
|
||||
|
||||
const handleZoomChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
setCurrentZoom(evt.target.value);
|
||||
};
|
||||
|
||||
const handleZoomEnter: KeyboardEventHandler<HTMLInputElement> = (evt) => {
|
||||
if (isKeyHotkey('escape', evt)) {
|
||||
evt.stopPropagation();
|
||||
setCurrentZoom(pageZoom.toString());
|
||||
}
|
||||
if (
|
||||
isKeyHotkey('enter', evt) &&
|
||||
'value' in evt.target &&
|
||||
typeof evt.target.value === 'string'
|
||||
) {
|
||||
const newZoom = parseInt(evt.target.value, 10);
|
||||
if (Number.isNaN(newZoom)) return;
|
||||
const safeZoom = Math.max(Math.min(newZoom, 150), 75);
|
||||
setPageZoom(safeZoom);
|
||||
setCurrentZoom(safeZoom.toString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
style={{ width: toRem(150) }}
|
||||
variant={pageZoom === parseInt(currentZoom, 10) ? 'Secondary' : 'Success'}
|
||||
size="300"
|
||||
radii="300"
|
||||
type="number"
|
||||
min="75"
|
||||
max="150"
|
||||
value={currentZoom}
|
||||
onChange={handleZoomChange}
|
||||
onKeyDown={handleZoomEnter}
|
||||
after={<Text size="T300">%</Text>}
|
||||
outlined
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Appearance() {
|
||||
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Appearance</Text>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="400">
|
||||
<SettingTile
|
||||
title="System Theme"
|
||||
description="Choose between light and dark theme based on system preference."
|
||||
after={<Switch variant="Primary" value={systemTheme} onChange={setSystemTheme} />}
|
||||
/>
|
||||
{systemTheme && <SystemThemePreferences />}
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Theme"
|
||||
description="Theme to use when system theme is not enabled."
|
||||
after={<SelectTheme disabled={systemTheme} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Twitter Emoji"
|
||||
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Editor() {
|
||||
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Editor</Text>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="ENTER for Newline"
|
||||
description={`Use ${
|
||||
isMacOS() ? KeySymbol.Command : 'Ctrl'
|
||||
} + ENTER to send message and ENTER for newline.`}
|
||||
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Markdown Formatting"
|
||||
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectMessageLayout() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const messageLayoutItems = useMessageLayoutItems();
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (layout: MessageLayout) => {
|
||||
setMessageLayout(layout);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">
|
||||
{messageLayoutItems.find((i) => i.layout === messageLayout)?.name ?? messageLayout}
|
||||
</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{messageLayoutItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.layout}
|
||||
size="300"
|
||||
variant={messageLayout === item.layout ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => handleSelect(item.layout)}
|
||||
>
|
||||
<Text size="T300">{item.name}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectMessageSpacing() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const messageSpacingItems = useMessageSpacingItems();
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (layout: MessageSpacing) => {
|
||||
setMessageSpacing(layout);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">
|
||||
{messageSpacingItems.find((i) => i.spacing === messageSpacing)?.name ?? messageSpacing}
|
||||
</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{messageSpacingItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.spacing}
|
||||
size="300"
|
||||
variant={messageSpacing === item.spacing ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => handleSelect(item.spacing)}
|
||||
>
|
||||
<Text size="T300">{item.name}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Messages() {
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideMembershipEvents'
|
||||
);
|
||||
const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideNickAvatarEvents'
|
||||
);
|
||||
const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Messages</Text>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Message Layout" after={<SelectMessageLayout />} />
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Hide Membership Change"
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={hideMembershipEvents}
|
||||
onChange={setHideMembershipEvents}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Hide Profile Change"
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={hideNickAvatarEvents}
|
||||
onChange={setHideNickAvatarEvents}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Disable Media Auto Load"
|
||||
after={<Switch variant="Primary" value={mediaAutoLoad} onChange={setMediaAutoLoad} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Url Preview"
|
||||
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Url Preview in Encrypted Room"
|
||||
after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard style={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Show Hidden Events"
|
||||
after={
|
||||
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type GeneralProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function General({ requestClose }: GeneralProps) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
General
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Appearance />
|
||||
<Editor />
|
||||
<Messages />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
138
src/app/features/settings/Settings.tsx
Normal file
138
src/app/features/settings/Settings.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, config, Icon, IconButton, Icons, IconSrc, MenuItem, Text } from 'folds';
|
||||
import { General } from './General';
|
||||
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
|
||||
enum SettingsPages {
|
||||
GeneralPage,
|
||||
AccountPage,
|
||||
NotificationPage,
|
||||
SessionPage,
|
||||
EncryptionPage,
|
||||
EmojisStickersPage,
|
||||
DeveloperToolsPage,
|
||||
AboutPage,
|
||||
}
|
||||
|
||||
type SettingsMenuItem = {
|
||||
page: SettingsPages;
|
||||
name: string;
|
||||
icon: IconSrc;
|
||||
};
|
||||
|
||||
const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
page: SettingsPages.GeneralPage,
|
||||
name: 'General',
|
||||
icon: Icons.Setting,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.AccountPage,
|
||||
name: 'Account',
|
||||
icon: Icons.User,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.NotificationPage,
|
||||
name: 'Notifications',
|
||||
icon: Icons.Bell,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.SessionPage,
|
||||
name: 'Sessions',
|
||||
icon: Icons.Category,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.EncryptionPage,
|
||||
name: 'Encryption',
|
||||
icon: Icons.ShieldLock,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.EmojisStickersPage,
|
||||
name: 'Emojis & Stickers',
|
||||
icon: Icons.Smile,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.DeveloperToolsPage,
|
||||
name: 'Developer Tools',
|
||||
icon: Icons.Terminal,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.AboutPage,
|
||||
name: 'About',
|
||||
icon: Icons.Info,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
type SettingsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function Settings({ requestClose }: SettingsProps) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const [activePage, setActivePage] = useState<SettingsPages | undefined>(
|
||||
screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage
|
||||
);
|
||||
const menuItems = useSettingsMenuItems();
|
||||
|
||||
const handlePageRequestClose = () => {
|
||||
if (screenSize === ScreenSize.Mobile) {
|
||||
setActivePage(undefined);
|
||||
return;
|
||||
}
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageRoot
|
||||
nav={
|
||||
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
|
||||
<PageNav size="300">
|
||||
<PageNavHeader outlined={false}>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Settings</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<IconButton onClick={requestClose} variant="Background">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</PageNavHeader>
|
||||
<PageNavContent>
|
||||
<div>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
variant="Background"
|
||||
radii="400"
|
||||
aria-pressed={activePage === item.page}
|
||||
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
|
||||
onClick={() => setActivePage(item.page)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
|
||||
}}
|
||||
size="T300"
|
||||
truncate
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
</PageNavContent>
|
||||
</PageNav>
|
||||
)
|
||||
}
|
||||
>
|
||||
{activePage === SettingsPages.GeneralPage && (
|
||||
<General requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
</PageRoot>
|
||||
);
|
||||
}
|
2
src/app/features/settings/index.ts
Normal file
2
src/app/features/settings/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './Settings';
|
||||
export * from './General';
|
26
src/app/hooks/useMessageLayout.ts
Normal file
26
src/app/hooks/useMessageLayout.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useMemo } from 'react';
|
||||
import { MessageLayout } from '../state/settings';
|
||||
|
||||
export type MessageLayoutItem = {
|
||||
name: string;
|
||||
layout: MessageLayout;
|
||||
};
|
||||
|
||||
export const useMessageLayoutItems = (): MessageLayoutItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
layout: MessageLayout.Modern,
|
||||
name: 'Modern',
|
||||
},
|
||||
{
|
||||
layout: MessageLayout.Compact,
|
||||
name: 'Compact',
|
||||
},
|
||||
{
|
||||
layout: MessageLayout.Bubble,
|
||||
name: 'Bubble',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
38
src/app/hooks/useMessageSpacing.ts
Normal file
38
src/app/hooks/useMessageSpacing.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { useMemo } from 'react';
|
||||
import { MessageSpacing } from '../state/settings';
|
||||
|
||||
export type MessageSpacingItem = {
|
||||
name: string;
|
||||
spacing: MessageSpacing;
|
||||
};
|
||||
|
||||
export const useMessageSpacingItems = (): MessageSpacingItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
spacing: '0',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
spacing: '100',
|
||||
name: 'Ultra Small',
|
||||
},
|
||||
{
|
||||
spacing: '200',
|
||||
name: 'Extra Small',
|
||||
},
|
||||
{
|
||||
spacing: '300',
|
||||
name: 'Small',
|
||||
},
|
||||
{
|
||||
spacing: '400',
|
||||
name: 'Normal',
|
||||
},
|
||||
{
|
||||
spacing: '500',
|
||||
name: 'Large',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
74
src/app/hooks/useTheme.ts
Normal file
74
src/app/hooks/useTheme.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
|
||||
export enum ThemeKind {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
kind: ThemeKind;
|
||||
classNames: string[];
|
||||
};
|
||||
|
||||
export const LightTheme: Theme = {
|
||||
id: 'light-theme',
|
||||
kind: ThemeKind.Light,
|
||||
classNames: [lightTheme, onLightFontWeight, 'prism-light'],
|
||||
};
|
||||
|
||||
export const SilverTheme: Theme = {
|
||||
id: 'silver-theme',
|
||||
kind: ThemeKind.Light,
|
||||
classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'],
|
||||
};
|
||||
export const DarkTheme: Theme = {
|
||||
id: 'dark-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
export const ButterTheme: Theme = {
|
||||
id: 'butter-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
|
||||
export const useThemes = (): Theme[] => {
|
||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
||||
|
||||
return themes;
|
||||
};
|
||||
|
||||
export const useThemeNames = (): Record<string, string> =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[LightTheme.id]: 'Light',
|
||||
[SilverTheme.id]: 'Silver',
|
||||
[DarkTheme.id]: 'Dark',
|
||||
[ButterTheme.id]: 'Butter',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
export const useSystemThemeKind = (): ThemeKind => {
|
||||
const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []);
|
||||
const [themeKind, setThemeKind] = useState<ThemeKind>(
|
||||
darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMediaQueryChange = () => {
|
||||
setThemeKind(darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light);
|
||||
};
|
||||
|
||||
darkModeQueryList.addEventListener('change', handleMediaQueryChange);
|
||||
return () => {
|
||||
darkModeQueryList.removeEventListener('change', handleMediaQueryChange);
|
||||
};
|
||||
}, [darkModeQueryList, setThemeKind]);
|
||||
|
||||
return themeKind;
|
||||
};
|
|
@ -55,6 +55,7 @@ import { ScreenSize } from '../hooks/useScreenSize';
|
|||
import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly';
|
||||
import { ClientInitStorageAtom } from './client/ClientInitStorageAtom';
|
||||
import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
|
||||
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
||||
|
||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||
const { hashRouter } = clientConfig;
|
||||
|
@ -79,7 +80,12 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
|
||||
return null;
|
||||
}}
|
||||
element={<AuthLayout />}
|
||||
element={
|
||||
<>
|
||||
<AuthLayout />
|
||||
<UnAuthRouteThemeManager />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Route path={LOGIN_PATH} element={<Login />} />
|
||||
<Route path={REGISTER_PATH} element={<Register />} />
|
||||
|
@ -99,23 +105,26 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
return null;
|
||||
}}
|
||||
element={
|
||||
<ClientRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<ClientBindAtoms>
|
||||
<ClientNonUIFeatures>
|
||||
<ClientLayout
|
||||
nav={
|
||||
<MobileFriendlyClientNav>
|
||||
<SidebarNav />
|
||||
</MobileFriendlyClientNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ClientLayout>
|
||||
</ClientNonUIFeatures>
|
||||
</ClientBindAtoms>
|
||||
</ClientInitStorageAtom>
|
||||
</ClientRoot>
|
||||
<>
|
||||
<ClientRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<ClientBindAtoms>
|
||||
<ClientNonUIFeatures>
|
||||
<ClientLayout
|
||||
nav={
|
||||
<MobileFriendlyClientNav>
|
||||
<SidebarNav />
|
||||
</MobileFriendlyClientNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ClientLayout>
|
||||
</ClientNonUIFeatures>
|
||||
</ClientBindAtoms>
|
||||
</ClientInitStorageAtom>
|
||||
</ClientRoot>
|
||||
<AuthRouteThemeManager />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
|
|
58
src/app/pages/ThemeManager.tsx
Normal file
58
src/app/pages/ThemeManager.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useEffect } from 'react';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
import { DarkTheme, LightTheme, ThemeKind, useSystemThemeKind, useThemes } from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
if (systemThemeKind === ThemeKind.Dark) {
|
||||
document.body.classList.add(...DarkTheme.classNames);
|
||||
}
|
||||
if (systemThemeKind === ThemeKind.Light) {
|
||||
document.body.classList.add(...LightTheme.classNames);
|
||||
}
|
||||
}, [systemThemeKind]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function AuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
const themes = useThemes();
|
||||
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [themeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
|
||||
// apply normal theme if system theme is disabled
|
||||
useEffect(() => {
|
||||
if (!systemTheme) {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
document.body.classList.add(...selectedTheme.classNames);
|
||||
}
|
||||
}, [systemTheme, themes, themeId]);
|
||||
|
||||
// apply preferred system theme if system theme is enabled
|
||||
useEffect(() => {
|
||||
if (systemTheme) {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
|
||||
document.body.classList.add(...selectedTheme.classNames);
|
||||
}
|
||||
}, [systemTheme, systemThemeKind, themes, lightThemeId, darkThemeId]);
|
||||
|
||||
return null;
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
SpaceTabs,
|
||||
InboxTab,
|
||||
ExploreTab,
|
||||
UserTab,
|
||||
SettingsTab,
|
||||
UnverifiedTab,
|
||||
} from './sidebar';
|
||||
import { openCreateRoom, openSearch } from '../../../client/action/navigation';
|
||||
|
@ -76,7 +76,7 @@ export function SidebarNav() {
|
|||
<UnverifiedTab />
|
||||
|
||||
<InboxTab />
|
||||
<UserTab />
|
||||
<SettingsTab />
|
||||
</SidebarStack>
|
||||
</>
|
||||
}
|
||||
|
|
111
src/app/pages/client/sidebar/SettingsTab.tsx
Normal file
111
src/app/pages/client/sidebar/SettingsTab.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { Settings } from '../../../features/settings';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
|
||||
type UserProfile = {
|
||||
avatarUrl?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
const useUserProfile = (userId: string): UserProfile => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [profile, setProfile] = useState<UserProfile>(() => {
|
||||
const user = mx.getUser(userId);
|
||||
return {
|
||||
avatarUrl: user?.avatarUrl,
|
||||
displayName: user?.displayName,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = mx.getUser(userId);
|
||||
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
avatarUrl: myUser.avatarUrl,
|
||||
}));
|
||||
};
|
||||
const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
displayName: myUser.displayName,
|
||||
}));
|
||||
};
|
||||
|
||||
mx.getProfileInfo(userId).then((info) =>
|
||||
setProfile({
|
||||
avatarUrl: info.avatar_url,
|
||||
displayName: info.displayname,
|
||||
})
|
||||
);
|
||||
|
||||
user?.on(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.on(UserEvent.DisplayName, onDisplayNameChange);
|
||||
return () => {
|
||||
user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
|
||||
};
|
||||
}, [mx, userId]);
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
export function SettingsTab() {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const userId = mx.getUserId()!;
|
||||
const profile = useUserProfile(userId);
|
||||
|
||||
const [settings, setSettings] = useState(false);
|
||||
|
||||
const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarUrl = profile.avatarUrl
|
||||
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const openSettings = () => setSettings(true);
|
||||
const closeSettings = () => setSettings(false);
|
||||
|
||||
return (
|
||||
<SidebarItem active={settings}>
|
||||
<SidebarItemTooltip tooltip={displayName}>
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar as="button" ref={triggerRef} onClick={openSettings}>
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
|
||||
/>
|
||||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
{settings && (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: closeSettings,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal size="500" variant="Background">
|
||||
<Settings requestClose={closeSettings} />
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Text } from 'folds';
|
||||
import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
|
||||
import { openSettings } from '../../../../client/action/navigation';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
|
||||
type UserProfile = {
|
||||
avatar_url?: string;
|
||||
displayname?: string;
|
||||
};
|
||||
export function UserTab() {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const userId = mx.getUserId()!;
|
||||
|
||||
const [profile, setProfile] = useState<UserProfile>({});
|
||||
const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarUrl = profile.avatar_url
|
||||
? mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const user = mx.getUser(userId);
|
||||
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
avatar_url: myUser.avatarUrl,
|
||||
}));
|
||||
};
|
||||
const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
avatar_url: myUser.displayName,
|
||||
}));
|
||||
};
|
||||
mx.getProfileInfo(userId).then((info) => setProfile(() => ({ ...info })));
|
||||
user?.on(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.on(UserEvent.DisplayName, onDisplayNameChange);
|
||||
return () => {
|
||||
user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
|
||||
};
|
||||
}, [mx, userId]);
|
||||
|
||||
return (
|
||||
<SidebarItem>
|
||||
<SidebarItemTooltip tooltip="User Settings">
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar as="button" ref={triggerRef} onClick={() => openSettings()}>
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
|
||||
/>
|
||||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
|
@ -3,5 +3,5 @@ export * from './DirectTab';
|
|||
export * from './SpaceTabs';
|
||||
export * from './InboxTab';
|
||||
export * from './ExploreTab';
|
||||
export * from './UserTab';
|
||||
export * from './SettingsTab';
|
||||
export * from './UnverifiedTab';
|
||||
|
|
|
@ -83,8 +83,6 @@ export type InboxNotificationsPathSearchParams = {
|
|||
export const INBOX_NOTIFICATIONS_PATH = `/inbox/${_NOTIFICATIONS_PATH}`;
|
||||
export const INBOX_INVITES_PATH = `/inbox/${_INVITES_PATH}`;
|
||||
|
||||
export const USER_SETTINGS_PATH = '/user-settings/';
|
||||
|
||||
export const SPACE_SETTINGS_PATH = '/space-settings/';
|
||||
|
||||
export const ROOM_SETTINGS_PATH = '/room-settings/';
|
||||
|
|
|
@ -16,7 +16,6 @@ import 'prismjs/components/prism-java';
|
|||
import 'prismjs/components/prism-python';
|
||||
|
||||
import './ReactPrism.css';
|
||||
// we apply theme in client/state/settings.js
|
||||
// using classNames .prism-dark .prism-light from ReactPrism.css
|
||||
|
||||
export default function ReactPrism({
|
||||
|
|
|
@ -2,11 +2,17 @@ import { atom } from 'jotai';
|
|||
|
||||
const STORAGE_KEY = 'settings';
|
||||
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
|
||||
export type MessageLayout = 0 | 1 | 2;
|
||||
export enum MessageLayout {
|
||||
Modern = 0,
|
||||
Compact = 1,
|
||||
Bubble = 2,
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
themeIndex: number;
|
||||
themeId?: string;
|
||||
useSystemTheme: boolean;
|
||||
lightThemeId?: string;
|
||||
darkThemeId?: string;
|
||||
isMarkdown: boolean;
|
||||
editorToolbar: boolean;
|
||||
twitterEmoji: boolean;
|
||||
|
@ -29,8 +35,10 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
themeIndex: 0,
|
||||
themeId: undefined,
|
||||
useSystemTheme: true,
|
||||
lightThemeId: undefined,
|
||||
darkThemeId: undefined,
|
||||
isMarkdown: true,
|
||||
editorToolbar: false,
|
||||
twitterEmoji: false,
|
||||
|
|
|
@ -191,7 +191,7 @@ const darkThemeData = {
|
|||
Other: {
|
||||
FocusRing: 'rgba(255, 255, 255, 0.5)',
|
||||
Shadow: 'rgba(0, 0, 0, 1)',
|
||||
Overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
Overlay: 'rgba(0, 0, 0, 0.8)',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,6 @@ enableMapSet();
|
|||
|
||||
import './index.scss';
|
||||
|
||||
import settings from './client/state/settings';
|
||||
|
||||
import { trimTrailingSlash } from './app/utils/common';
|
||||
import App from './app/pages/App';
|
||||
|
||||
|
@ -19,7 +17,6 @@ import App from './app/pages/App';
|
|||
import './app/i18n';
|
||||
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
settings.applyTheme();
|
||||
|
||||
// Register Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
|
Loading…
Reference in a new issue