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