Change username color in chat with power level color (#2282)
* add active theme context * add chroma js library * add hook for accessible tag color * disable reply user color - temporary * render user color based on tag in room timeline * remove default tag icons * move accessible color function to plugins * render user power color in reply * increase username weight in timeline * add default color for member power level tag * show red slash in power color badge with no color * show power level color in room input reply * show power level username color in notifications * show power level color in notification reply * show power level color in message search * render power level color in room pin menu * add toggle for legacy username colors * drop over saturation from member default color * change border color of power color badge * show legacy username color in direct rooms
This commit is contained in:
parent
7d54eef95b
commit
08e975cd8e
26 changed files with 463 additions and 91 deletions
package-lock.jsonpackage.json
src/app
components
features
message-search
room
settings/general
hooks
pages
plugins
state
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -24,6 +24,7 @@
|
|||
"await-to-js": "3.0.0",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/node": "18.11.18",
|
||||
|
@ -4573,6 +4575,13 @@
|
|||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chroma-js": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.1.tgz",
|
||||
"integrity": "sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
|
@ -5730,6 +5739,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chroma-js": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz",
|
||||
"integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==",
|
||||
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"await-to-js": "3.0.0",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
|
@ -85,6 +86,7 @@
|
|||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/node": "18.11.18",
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
|
|||
import { EventTimelineSet, Room } from 'matrix-js-sdk';
|
||||
import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { LinePlaceholder } from './placeholder';
|
||||
|
@ -11,6 +10,8 @@ import * as css from './Reply.css';
|
|||
import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content';
|
||||
import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
import { useRoomEvent } from '../../hooks/useRoomEvent';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
type ReplyLayoutProps = {
|
||||
userColor?: string;
|
||||
|
@ -49,10 +50,28 @@ type ReplyProps = {
|
|||
replyEventId: string;
|
||||
threadRootId?: string | undefined;
|
||||
onClick?: MouseEventHandler | undefined;
|
||||
getPowerLevel?: (userId: string) => number;
|
||||
getPowerLevelTag?: GetPowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
|
||||
export const Reply = as<'div', ReplyProps>(
|
||||
({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
room,
|
||||
timelineSet,
|
||||
replyEventId,
|
||||
threadRootId,
|
||||
onClick,
|
||||
getPowerLevel,
|
||||
getPowerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
|
||||
const getFromLocalTimeline = useCallback(
|
||||
() => timelineSet?.findEventById(replyEventId),
|
||||
|
@ -62,6 +81,11 @@ export const Reply = as<'div', ReplyProps>(
|
|||
|
||||
const { body } = replyEvent?.getContent() ?? {};
|
||||
const sender = replyEvent?.getSender();
|
||||
const senderPL = sender && getPowerLevel?.(sender);
|
||||
const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined;
|
||||
const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor;
|
||||
|
||||
const fallbackBody = replyEvent?.isRedacted() ? (
|
||||
<MessageDeletedContent />
|
||||
|
@ -79,7 +103,7 @@ export const Reply = as<'div', ReplyProps>(
|
|||
)}
|
||||
<ReplyLayout
|
||||
as="button"
|
||||
userColor={sender ? colorMXID(sender) : undefined}
|
||||
userColor={usernameColor}
|
||||
username={
|
||||
sender && (
|
||||
<Text size="T300" truncate>
|
||||
|
|
|
@ -24,6 +24,10 @@ export const Username = as<'span'>(({ as: AsUsername = 'span', className, ...pro
|
|||
<AsUsername className={classNames(css.Username, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...props }, ref) => (
|
||||
<AsUsernameBold className={classNames(css.UsernameBold, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
|
||||
({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => (
|
||||
<Text
|
||||
|
|
|
@ -157,6 +157,10 @@ export const Username = style({
|
|||
},
|
||||
});
|
||||
|
||||
export const UsernameBold = style({
|
||||
fontWeight: 550,
|
||||
});
|
||||
|
||||
export const MessageTextBody = recipe({
|
||||
base: {
|
||||
wordBreak: 'break-word',
|
||||
|
|
|
@ -9,7 +9,7 @@ type PowerColorBadgeProps = {
|
|||
export const PowerColorBadge = as<'span', PowerColorBadgeProps>(
|
||||
({ as: AsPowerColorBadge = 'span', color, className, style, ...props }, ref) => (
|
||||
<AsPowerColorBadge
|
||||
className={classNames(css.PowerColorBadge, className)}
|
||||
className={classNames(css.PowerColorBadge, { [css.PowerColorBadgeNone]: !color }, className)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
...style,
|
||||
|
|
|
@ -3,13 +3,30 @@ import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
|||
import { color, config, DefaultReset, toRem } from 'folds';
|
||||
|
||||
export const PowerColorBadge = style({
|
||||
display: 'inline-block',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
width: toRem(16),
|
||||
height: toRem(16),
|
||||
backgroundColor: color.Surface.OnContainer,
|
||||
borderRadius: config.radii.Pill,
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const PowerColorBadgeNone = style({
|
||||
selectors: {
|
||||
'&::before': {
|
||||
content: '',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: config.borderWidth.B300,
|
||||
backgroundColor: color.Critical.Main,
|
||||
|
||||
position: 'absolute',
|
||||
transform: `rotateZ(-45deg)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const PowerIconSize = createVar();
|
||||
|
|
|
@ -55,6 +55,8 @@ export function MessageSearch({
|
|||
const allRooms = useRooms(mx, allRoomsAtom, mDirects);
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
@ -297,6 +299,7 @@ export function MessageSearch({
|
|||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={legacyUsernameColor || mDirects.has(groupRoom.roomId)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../components/message';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
import { Image } from '../../components/media';
|
||||
|
@ -32,13 +33,21 @@ import { ImageViewer } from '../../components/image-viewer';
|
|||
import * as customHtmlCss from '../../styles/CustomHtml.css';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../../utils/room';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { ResultItem } from './useMessageSearch';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import {
|
||||
getTagIconSrc,
|
||||
useAccessibleTagColors,
|
||||
usePowerLevelTags,
|
||||
} from '../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../components/power';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
type SearchResultGroupProps = {
|
||||
room: Room;
|
||||
|
@ -47,6 +56,7 @@ type SearchResultGroupProps = {
|
|||
mediaAutoLoad?: boolean;
|
||||
urlPreview?: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
export function SearchResultGroup({
|
||||
room,
|
||||
|
@ -55,11 +65,18 @@ export function SearchResultGroup({
|
|||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
}: SearchResultGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
|
@ -81,7 +98,15 @@ export function SearchResultGroup({
|
|||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler, useAuthentication]
|
||||
[
|
||||
mx,
|
||||
room,
|
||||
linkifyOpts,
|
||||
highlightRegex,
|
||||
mentionClickHandler,
|
||||
spoilerClickHandler,
|
||||
useAuthentication,
|
||||
]
|
||||
);
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
|
||||
|
@ -197,6 +222,17 @@ export function SearchResultGroup({
|
|||
const threadRootId =
|
||||
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
|
||||
|
||||
const senderPowerLevel = getPowerLevel(event.sender);
|
||||
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
|
||||
const tagColor = powerLevelTag?.color
|
||||
? accessibleTagColors?.get(powerLevelTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={event.event_id}
|
||||
|
@ -212,7 +248,14 @@ export function SearchResultGroup({
|
|||
userId={event.sender}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
|
||||
? mxcUrlToHttp(
|
||||
mx,
|
||||
senderAvatarMxc,
|
||||
useAuthentication,
|
||||
48,
|
||||
48,
|
||||
'crop'
|
||||
) ?? undefined
|
||||
: undefined
|
||||
}
|
||||
alt={displayName}
|
||||
|
@ -224,11 +267,14 @@ export function SearchResultGroup({
|
|||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Username style={{ color: colorMXID(event.sender) }}>
|
||||
<Text as="span" truncate>
|
||||
<b>{displayName}</b>
|
||||
</Text>
|
||||
</Username>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
|
@ -244,11 +290,14 @@ export function SearchResultGroup({
|
|||
</Box>
|
||||
{replyEventId && (
|
||||
<Reply
|
||||
mx={mx}
|
||||
room={room}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
|
||||
|
|
|
@ -99,7 +99,6 @@ import {
|
|||
getImageMsgContent,
|
||||
getVideoMsgContent,
|
||||
} from './msgContent';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getMemberDisplayName, getMentionContent, trimReplyFromBody } from '../../utils/room';
|
||||
import { CommandAutocomplete } from './CommandAutocomplete';
|
||||
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands';
|
||||
|
@ -109,26 +108,44 @@ import { ReplyLayout, ThreadIndicator } from '../../components/message';
|
|||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
fileDropContainerRef: RefObject<HTMLElement>;
|
||||
roomId: string;
|
||||
room: Room;
|
||||
getPowerLevelTag: GetPowerLevelTag;
|
||||
accessibleTagColors: Map<string, string>;
|
||||
}
|
||||
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
({ editor, fileDropContainerRef, roomId, room }, ref) => {
|
||||
({ editor, fileDropContainerRef, roomId, room, getPowerLevelTag, accessibleTagColors }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const commands = useCommands(mx, room);
|
||||
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
|
||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||
const replyUserID = replyDraft?.userId;
|
||||
|
||||
const replyPowerTag = getPowerLevelTag(powerLevelAPI.getPowerLevel(powerLevels, replyUserID));
|
||||
const replyPowerColor = replyPowerTag.color
|
||||
? accessibleTagColors.get(replyPowerTag.color)
|
||||
: undefined;
|
||||
const replyUsernameColor =
|
||||
legacyUsernameColor || direct ? colorMXID(replyUserID ?? '') : replyPowerColor;
|
||||
|
||||
const [uploadBoard, setUploadBoard] = useState(true);
|
||||
const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(roomId));
|
||||
const uploadFamilyObserverAtom = createUploadFamilyObserverAtom(
|
||||
|
@ -348,7 +365,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(evt) => {
|
||||
if ((isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) && !evt.nativeEvent.isComposing) {
|
||||
if (
|
||||
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
||||
!evt.nativeEvent.isComposing
|
||||
) {
|
||||
evt.preventDefault();
|
||||
submit();
|
||||
}
|
||||
|
@ -526,7 +546,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<Box direction="Column">
|
||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||
<ReplyLayout
|
||||
userColor={colorMXID(replyDraft.userId)}
|
||||
userColor={replyUsernameColor}
|
||||
username={
|
||||
<Text size="T300" truncate>
|
||||
<b>
|
||||
|
|
|
@ -118,6 +118,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
|
||||
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
|
||||
({ position, className, ...props }, ref) => (
|
||||
|
@ -220,6 +222,8 @@ type RoomTimelineProps = {
|
|||
eventId?: string;
|
||||
roomInputRef: RefObject<HTMLElement>;
|
||||
editor: Editor;
|
||||
getPowerLevelTag: GetPowerLevelTag;
|
||||
accessibleTagColors: Map<string, string>;
|
||||
};
|
||||
|
||||
const PAGINATION_LIMIT = 80;
|
||||
|
@ -422,12 +426,21 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
|
|||
};
|
||||
};
|
||||
|
||||
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
|
||||
export function RoomTimeline({
|
||||
room,
|
||||
eventId,
|
||||
roomInputRef,
|
||||
editor,
|
||||
getPowerLevelTag,
|
||||
accessibleTagColors,
|
||||
}: RoomTimelineProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
|
@ -443,11 +456,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const powerLevels = usePowerLevelsContext();
|
||||
const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } =
|
||||
usePowerLevelsAPI(powerLevels);
|
||||
|
||||
const myPowerLevel = getPowerLevel(mx.getUserId() ?? '');
|
||||
const canRedact = canDoAction('redact', myPowerLevel);
|
||||
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
|
||||
const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel);
|
||||
const [editId, setEditId] = useState<string>();
|
||||
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
@ -996,6 +1011,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent()) as GetContentCallback;
|
||||
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
const senderDisplayName =
|
||||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||
|
||||
|
@ -1029,6 +1045,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1045,6 +1065,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
@ -1071,6 +1094,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const hasReactions = reactions && reactions.length > 0;
|
||||
const { replyEventId, threadRootId } = mEvent;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
|
||||
return (
|
||||
<Message
|
||||
|
@ -1102,6 +1126,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1118,6 +1146,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
|
@ -1181,6 +1212,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||
const hasReactions = reactions && reactions.length > 0;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const senderPowerLevel = getPowerLevel(mEvent.getSender());
|
||||
|
||||
return (
|
||||
<Message
|
||||
|
@ -1215,6 +1247,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
>
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
|
|
|
@ -21,6 +21,8 @@ import { editableActiveElement } from '../../utils/dom';
|
|||
import navigation from '../../../client/state/navigation';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { useAccessibleTagColors, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
|
||||
const FN_KEYS_REGEX = /^F\d+$/;
|
||||
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
|
@ -74,6 +76,10 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
|
||||
: false;
|
||||
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
useKeyDown(
|
||||
window,
|
||||
useCallback(
|
||||
|
@ -103,6 +109,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
eventId={eventId}
|
||||
roomInputRef={roomInputRef}
|
||||
editor={editor}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
/>
|
||||
<RoomViewTyping room={room} />
|
||||
</Box>
|
||||
|
@ -123,6 +131,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||
roomId={roomId}
|
||||
fileDropContainerRef={roomViewRef}
|
||||
ref={roomInputRef}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
/>
|
||||
)}
|
||||
{!canMessage && (
|
||||
|
|
|
@ -44,8 +44,8 @@ import {
|
|||
ModernLayout,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import {
|
||||
canEditEvent,
|
||||
getEventEdits,
|
||||
|
@ -76,6 +76,9 @@ import { getViaServers } from '../../../plugins/via-servers';
|
|||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
|
@ -672,6 +675,9 @@ export type MessageProps = {
|
|||
reply?: ReactNode;
|
||||
reactions?: ReactNode;
|
||||
hideReadReceipts?: boolean;
|
||||
powerLevelTag?: PowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
export const Message = as<'div', MessageProps>(
|
||||
(
|
||||
|
@ -697,6 +703,9 @@ export const Message = as<'div', MessageProps>(
|
|||
reply,
|
||||
reactions,
|
||||
hideReadReceipts,
|
||||
powerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
|
@ -715,6 +724,15 @@ export const Message = as<'div', MessageProps>(
|
|||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
|
||||
|
||||
const tagColor = powerLevelTag?.color
|
||||
? accessibleTagColors?.get(powerLevelTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
|
||||
|
||||
const headerJSX = !collapse && (
|
||||
<Box
|
||||
gap="300"
|
||||
|
@ -723,17 +741,24 @@ export const Message = as<'div', MessageProps>(
|
|||
alignItems="Baseline"
|
||||
grow="Yes"
|
||||
>
|
||||
<Username
|
||||
as="button"
|
||||
style={{ color: colorMXID(senderId) }}
|
||||
data-user-id={senderId}
|
||||
onContextMenu={onUserClick}
|
||||
onClick={onUsernameClick}
|
||||
>
|
||||
<Text as="span" size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'} truncate>
|
||||
<b>{senderDisplayName}</b>
|
||||
</Text>
|
||||
</Username>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username
|
||||
as="button"
|
||||
style={{ color: usernameColor }}
|
||||
data-user-id={senderId}
|
||||
onContextMenu={onUserClick}
|
||||
onClick={onUsernameClick}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'}
|
||||
truncate
|
||||
>
|
||||
<UsernameBold>{senderDisplayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Box shrink="No" gap="100">
|
||||
{messageLayout === MessageLayout.Modern && hover && (
|
||||
<>
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
|
@ -49,7 +50,6 @@ import {
|
|||
getStateEvent,
|
||||
} from '../../../utils/room';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import {
|
||||
|
@ -72,6 +72,15 @@ import { VirtualTile } from '../../../components/virtualizer';
|
|||
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||
import {
|
||||
getTagIconSrc,
|
||||
useAccessibleTagColors,
|
||||
usePowerLevelTags,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { useIsDirectRoom } from '../../../hooks/useRoom';
|
||||
|
||||
type PinnedMessageProps = {
|
||||
room: Room;
|
||||
|
@ -84,6 +93,14 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
const pinnedEvent = useRoomEvent(room, eventId);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const mx = useMatrixClient();
|
||||
const direct = useIsDirectRoom();
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const [unpinState, unpin] = useAsyncCallback(
|
||||
useCallback(() => {
|
||||
|
@ -93,7 +110,7 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
pinned: content.pinned.filter((id) => id !== eventId),
|
||||
};
|
||||
|
||||
return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, newContent);
|
||||
return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, newContent);
|
||||
}, [room, eventId, mx])
|
||||
);
|
||||
|
||||
|
@ -148,6 +165,16 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
|
||||
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
||||
|
||||
const senderPowerLevel = getPowerLevel(sender);
|
||||
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
|
||||
const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor;
|
||||
|
||||
return (
|
||||
<ModernLayout
|
||||
before={
|
||||
|
@ -170,11 +197,14 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Username style={{ color: colorMXID(sender) }}>
|
||||
<Text as="span" truncate>
|
||||
<b>{displayName}</b>
|
||||
</Text>
|
||||
</Username>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={pinnedEvent.getTs()} />
|
||||
</Box>
|
||||
{renderOptions()}
|
||||
|
@ -185,6 +215,10 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
|
|||
replyEventId={pinnedEvent.replyEventId}
|
||||
threadRootId={pinnedEvent.threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)}
|
||||
|
|
|
@ -514,6 +514,10 @@ function SelectMessageSpacing() {
|
|||
}
|
||||
|
||||
function Messages() {
|
||||
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
|
||||
settingsAtom,
|
||||
'legacyUsernameColor'
|
||||
);
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideMembershipEvents'
|
||||
|
@ -536,6 +540,18 @@ function Messages() {
|
|||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Legacy Username Color"
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={legacyUsernameColor}
|
||||
onChange={setLegacyUsernameColor}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Hide Membership Change"
|
||||
|
|
|
@ -4,6 +4,8 @@ import { IPowerLevels } from './usePowerLevels';
|
|||
import { useStateEvent } from './useStateEvent';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { IImageInfo } from '../../types/matrix/common';
|
||||
import { ThemeKind } from './useTheme';
|
||||
import { accessibleColor } from '../plugins/color';
|
||||
|
||||
export type PowerLevelTagIcon = {
|
||||
key?: string;
|
||||
|
@ -63,7 +65,7 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
},
|
||||
100: {
|
||||
name: 'Admin',
|
||||
color: '#a000e4',
|
||||
color: '#0088ff',
|
||||
},
|
||||
50: {
|
||||
name: 'Moderator',
|
||||
|
@ -71,9 +73,11 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
},
|
||||
0: {
|
||||
name: 'Member',
|
||||
color: '#91cfdf',
|
||||
},
|
||||
[-1]: {
|
||||
name: 'Muted',
|
||||
color: '#888888',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -152,3 +156,24 @@ export const getTagIconSrc = (
|
|||
icon?.key?.startsWith('mxc://')
|
||||
? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
|
||||
: icon?.key;
|
||||
|
||||
export const useAccessibleTagColors = (
|
||||
themeKind: ThemeKind,
|
||||
powerLevelTags: PowerLevelTags
|
||||
): Map<string, string> => {
|
||||
const accessibleColors: Map<string, string> = useMemo(() => {
|
||||
const colors: Map<string, string> = new Map();
|
||||
|
||||
getPowers(powerLevelTags).forEach((power) => {
|
||||
const tag = powerLevelTags[power];
|
||||
const { color } = tag;
|
||||
if (!color) return;
|
||||
|
||||
colors.set(color, accessibleColor(themeKind, color));
|
||||
});
|
||||
|
||||
return colors;
|
||||
}, [powerLevelTags, themeKind]);
|
||||
|
||||
return accessibleColors;
|
||||
};
|
||||
|
|
|
@ -10,3 +10,13 @@ export function useRoom(): Room {
|
|||
if (!room) throw new Error('Room not provided!');
|
||||
return room;
|
||||
}
|
||||
|
||||
const IsDirectRoomContext = createContext<boolean>(false);
|
||||
|
||||
export const IsDirectRoomProvider = IsDirectRoomContext.Provider;
|
||||
|
||||
export const useIsDirectRoom = () => {
|
||||
const direct = useContext(IsDirectRoomContext);
|
||||
|
||||
return direct;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
export enum ThemeKind {
|
||||
Light = 'light',
|
||||
|
@ -72,3 +74,37 @@ export const useSystemThemeKind = (): ThemeKind => {
|
|||
|
||||
return themeKind;
|
||||
};
|
||||
|
||||
export const useActiveTheme = (): Theme => {
|
||||
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');
|
||||
|
||||
if (!systemTheme) {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
|
||||
return selectedTheme;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<Theme | null>(null);
|
||||
export const ThemeContextProvider = ThemeContext.Provider;
|
||||
|
||||
export const useTheme = (): Theme => {
|
||||
const theme = useContext(ThemeContext);
|
||||
if (!theme) {
|
||||
throw new Error('No theme provided!');
|
||||
}
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
|
|
@ -109,7 +109,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
return null;
|
||||
}}
|
||||
element={
|
||||
<>
|
||||
<AuthRouteThemeManager>
|
||||
<ClientRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<ClientRoomsNotificationPreferences>
|
||||
|
@ -132,8 +132,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
</ClientRoomsNotificationPreferences>
|
||||
</ClientInitStorageAtom>
|
||||
</ClientRoot>
|
||||
<AuthRouteThemeManager />
|
||||
</>
|
||||
</AuthRouteThemeManager>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { useEffect } from 'react';
|
||||
import React, { ReactNode, 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';
|
||||
import {
|
||||
DarkTheme,
|
||||
LightTheme,
|
||||
ThemeContextProvider,
|
||||
ThemeKind,
|
||||
useActiveTheme,
|
||||
useSystemThemeKind,
|
||||
} from '../hooks/useTheme';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
@ -21,38 +26,15 @@ export function UnAuthRouteThemeManager() {
|
|||
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');
|
||||
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
const activeTheme = useActiveTheme();
|
||||
|
||||
// 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.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
|
||||
document.body.classList.add(...selectedTheme.classNames);
|
||||
}
|
||||
}, [systemTheme, themes, themeId]);
|
||||
document.body.classList.add(...activeTheme.classNames);
|
||||
}, [activeTheme]);
|
||||
|
||||
// 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;
|
||||
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { RoomProvider } from '../../../hooks/useRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useDirectRooms } from './useDirectRooms';
|
||||
|
@ -20,7 +20,7 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
return (
|
||||
<RoomProvider key={room.roomId} value={room}>
|
||||
{children}
|
||||
<IsDirectRoomProvider value>{children}</IsDirectRoomProvider>
|
||||
</RoomProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { RoomProvider } from '../../../hooks/useRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useHomeRooms } from './useHomeRooms';
|
||||
|
@ -28,7 +28,7 @@ export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
return (
|
||||
<RoomProvider key={room.roomId} value={room}>
|
||||
{children}
|
||||
<IsDirectRoomProvider value={false}>{children}</IsDirectRoomProvider>
|
||||
</RoomProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,8 +53,8 @@ import {
|
|||
Reply,
|
||||
Time,
|
||||
Username,
|
||||
UsernameBold,
|
||||
} from '../../../components/message';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
|
@ -84,6 +84,16 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
|||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
getTagIconSrc,
|
||||
useAccessibleTagColors,
|
||||
usePowerLevelTags,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import { PowerIcon } from '../../../components/power';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
|
||||
type RoomNotificationsGroup = {
|
||||
roomId: string;
|
||||
|
@ -194,6 +204,7 @@ type RoomNotificationsGroupProps = {
|
|||
urlPreview?: boolean;
|
||||
hideActivity: boolean;
|
||||
onOpen: (roomId: string, eventId: string) => void;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
function RoomNotificationsGroupComp({
|
||||
room,
|
||||
|
@ -202,10 +213,18 @@ function RoomNotificationsGroupComp({
|
|||
urlPreview,
|
||||
hideActivity,
|
||||
onOpen,
|
||||
legacyUsernameColor,
|
||||
}: RoomNotificationsGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const theme = useTheme();
|
||||
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
|
@ -424,6 +443,17 @@ function RoomNotificationsGroupComp({
|
|||
const threadRootId =
|
||||
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
|
||||
|
||||
const senderPowerLevel = getPowerLevel(event.sender);
|
||||
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
|
||||
const tagColor = powerLevelTag?.color
|
||||
? accessibleTagColors?.get(powerLevelTag.color)
|
||||
: undefined;
|
||||
const tagIconSrc = powerLevelTag?.icon
|
||||
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
|
||||
: undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={notification.event.event_id}
|
||||
|
@ -458,11 +488,14 @@ function RoomNotificationsGroupComp({
|
|||
>
|
||||
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Username style={{ color: colorMXID(event.sender) }}>
|
||||
<Text as="span" truncate>
|
||||
<b>{displayName}</b>
|
||||
</Text>
|
||||
</Username>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username style={{ color: usernameColor }}>
|
||||
<Text as="span" truncate>
|
||||
<UsernameBold>{displayName}</UsernameBold>
|
||||
</Text>
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Time ts={event.origin_server_ts} />
|
||||
</Box>
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
|
@ -482,6 +515,10 @@ function RoomNotificationsGroupComp({
|
|||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
getPowerLevel={getPowerLevel}
|
||||
getPowerLevelTag={getPowerLevelTag}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor}
|
||||
/>
|
||||
)}
|
||||
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
|
||||
|
@ -511,7 +548,9 @@ export function Notifications() {
|
|||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
@ -671,6 +710,9 @@ export function Notifications() {
|
|||
urlPreview={urlPreview}
|
||||
hideActivity={hideActivity}
|
||||
onOpen={navigateRoom}
|
||||
legacyUsernameColor={
|
||||
legacyUsernameColor || mDirects.has(groupRoom.roomId)
|
||||
}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { RoomProvider } from '../../../hooks/useRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useSpace } from '../../../hooks/useSpace';
|
||||
|
@ -10,11 +10,13 @@ import { getAllParents } from '../../../utils/room';
|
|||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
|
||||
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
const space = useSpace();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
|
@ -39,7 +41,7 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
return (
|
||||
<RoomProvider key={room.roomId} value={room}>
|
||||
{children}
|
||||
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
|
||||
</RoomProvider>
|
||||
);
|
||||
}
|
||||
|
|
16
src/app/plugins/color.ts
Normal file
16
src/app/plugins/color.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import chroma from 'chroma-js';
|
||||
import { ThemeKind } from '../hooks/useTheme';
|
||||
|
||||
export const accessibleColor = (themeKind: ThemeKind, color: string): string => {
|
||||
if (!chroma.valid(color)) return color;
|
||||
|
||||
let lightness = chroma(color).lab()[0];
|
||||
if (themeKind === ThemeKind.Dark && lightness < 60) {
|
||||
lightness = 60;
|
||||
}
|
||||
if (themeKind === ThemeKind.Light && lightness > 50) {
|
||||
lightness = 50;
|
||||
}
|
||||
|
||||
return chroma(color).set('lab.l', lightness).hex();
|
||||
};
|
|
@ -30,6 +30,7 @@ export interface Settings {
|
|||
urlPreview: boolean;
|
||||
encUrlPreview: boolean;
|
||||
showHiddenEvents: boolean;
|
||||
legacyUsernameColor: boolean;
|
||||
|
||||
showNotifications: boolean;
|
||||
isNotificationSounds: boolean;
|
||||
|
@ -59,6 +60,7 @@ const defaultSettings: Settings = {
|
|||
urlPreview: true,
|
||||
encUrlPreview: false,
|
||||
showHiddenEvents: false,
|
||||
legacyUsernameColor: false,
|
||||
|
||||
showNotifications: true,
|
||||
isNotificationSounds: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue