pose-aware chat bubble behind a Pose Portraits setting
Reads org.gamecult.pose from event content for senders that are known Minds; unknown or missing labels fall back to neutral. Portrait sits beside the bubble on wide screens and stacks above it on phones. 84 downscaled webp portraits (7 Minds x 12 poses, ~415 KB). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
BIN
public/poses/aqua/amused.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/annoyed.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/concerned.webp
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
public/poses/aqua/conduct.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/determined.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/poses/aqua/flourish.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/happy.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/poses/aqua/listen.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/poses/aqua/neutral.webp
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
public/poses/aqua/patch.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/surprised.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/poses/aqua/thinking.webp
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
public/poses/eureka/amused.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/eureka/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/poses/eureka/concerned.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/poses/eureka/determined.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/poses/eureka/happy.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/poses/eureka/hood.webp
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/poses/eureka/inspect.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/eureka/neutral.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/eureka/probe.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/poses/eureka/report.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/poses/eureka/surprised.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/eureka/thinking.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/heimdall/amused.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/heimdall/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/heimdall/badge.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/poses/heimdall/barrier.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/heimdall/concerned.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/poses/heimdall/determined.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/heimdall/happy.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/heimdall/neutral.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/heimdall/protect.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/poses/heimdall/revoke.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/poses/heimdall/surprised.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/heimdall/thinking.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/kiko/amused.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/poses/kiko/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/poses/kiko/concerned.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/kiko/confetti.webp
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/poses/kiko/debug.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/kiko/determined.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/kiko/happy.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/kiko/neutral.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/kiko/sprint.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/poses/kiko/surprised.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/kiko/thinking.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/kiko/tinker.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/poses/libby/amused.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/poses/libby/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/poses/libby/concerned.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/poses/libby/determined.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/poses/libby/happy.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/libby/neutral.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/libby/research.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/libby/schema.webp
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/poses/libby/shush.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/libby/stamp.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/libby/surprised.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/poses/libby/thinking.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/nibu/amused.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/poses/nibu/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/poses/nibu/concerned.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/poses/nibu/console.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/nibu/determined.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/nibu/glitch.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/nibu/happy.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/poses/nibu/lore.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/poses/nibu/neutral.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/poses/nibu/surprised.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/poses/nibu/thinking.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/nibu/triumph.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/poses/weksa/amused.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/weksa/annoyed.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/poses/weksa/argue.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/weksa/concerned.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/poses/weksa/determined.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/weksa/fieldwork.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/poses/weksa/happy.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/poses/weksa/interlingua.webp
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/poses/weksa/map.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/poses/weksa/neutral.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/poses/weksa/surprised.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/poses/weksa/thinking.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -5,11 +5,12 @@ import * as css from './layout.css';
|
|||
|
||||
type BubbleArrowProps = {
|
||||
variant: ContainerColor;
|
||||
pose?: boolean;
|
||||
};
|
||||
function BubbleLeftArrow({ variant }: BubbleArrowProps) {
|
||||
function BubbleLeftArrow({ variant, pose }: BubbleArrowProps) {
|
||||
return (
|
||||
<svg
|
||||
className={css.BubbleLeftArrow}
|
||||
className={classNames(css.BubbleLeftArrow, pose && css.BubbleArrowPose)}
|
||||
width="9"
|
||||
height="8"
|
||||
viewBox="0 0 9 8"
|
||||
|
|
@ -28,14 +29,20 @@ function BubbleLeftArrow({ variant }: BubbleArrowProps) {
|
|||
|
||||
type BubbleLayoutProps = {
|
||||
hideBubble?: boolean;
|
||||
pose?: boolean;
|
||||
before?: ReactNode;
|
||||
header?: ReactNode;
|
||||
};
|
||||
|
||||
export const BubbleLayout = as<'div', BubbleLayoutProps>(
|
||||
({ hideBubble, before, header, children, ...props }, ref) => (
|
||||
<Box gap="300" {...props} ref={ref}>
|
||||
<Box className={css.BubbleBefore} shrink="No">
|
||||
({ hideBubble, pose, before, header, children, ...props }, ref) => (
|
||||
<Box
|
||||
gap="300"
|
||||
{...props}
|
||||
className={classNames(pose && css.BubblePose, props.className)}
|
||||
ref={ref}
|
||||
>
|
||||
<Box className={pose ? css.BubbleBeforePose : css.BubbleBefore} shrink="No">
|
||||
{before}
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column">
|
||||
|
|
@ -48,11 +55,15 @@ export const BubbleLayout = as<'div', BubbleLayoutProps>(
|
|||
className={
|
||||
hideBubble
|
||||
? undefined
|
||||
: classNames(css.BubbleContent, before ? css.BubbleContentArrowLeft : undefined)
|
||||
: classNames(
|
||||
css.BubbleContent,
|
||||
before ? css.BubbleContentArrowLeft : undefined,
|
||||
pose && css.BubbleContentPoseStacked
|
||||
)
|
||||
}
|
||||
direction="Column"
|
||||
>
|
||||
{before ? <BubbleLeftArrow variant="SurfaceVariant" /> : null}
|
||||
{before ? <BubbleLeftArrow variant="SurfaceVariant" pose={pose} /> : null}
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -138,6 +138,34 @@ export const BubbleBefore = style({
|
|||
minWidth: toRem(36),
|
||||
});
|
||||
|
||||
// Pose portraits: beside the bubble on wide screens; on phones the row stacks, so the
|
||||
// portrait costs a little height instead of a column of width, and the left arrow goes.
|
||||
export const BubblePose = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
flexDirection: 'column',
|
||||
gap: config.space.S100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BubbleBeforePose = style({
|
||||
minWidth: toRem(64),
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
minWidth: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BubbleArrowPose = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BubbleContent = style({
|
||||
maxWidth: toRem(800),
|
||||
padding: config.space.S200,
|
||||
|
|
@ -151,6 +179,34 @@ export const BubbleContentArrowLeft = style({
|
|||
borderTopLeftRadius: 0,
|
||||
});
|
||||
|
||||
export const BubbleContentPoseStacked = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
borderTopLeftRadius: config.radii.R500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const PoseButton = style({
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
lineHeight: 0,
|
||||
});
|
||||
|
||||
export const PosePortrait = style({
|
||||
width: toRem(64),
|
||||
height: toRem(64),
|
||||
objectFit: 'contain',
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
width: toRem(48),
|
||||
height: toRem(48),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BubbleLeftArrow = style({
|
||||
width: toRem(9),
|
||||
height: toRem(8),
|
||||
|
|
|
|||
30
src/app/components/message/pose/PoseAvatar.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import * as css from '../layout/layout.css';
|
||||
import { poseSrc } from './poses';
|
||||
|
||||
type PoseAvatarProps = {
|
||||
mind: string;
|
||||
pose: string;
|
||||
userId: string;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export function PoseAvatar({ mind, pose, userId, onClick }: PoseAvatarProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={css.PoseButton}
|
||||
data-user-id={userId}
|
||||
data-pose={pose}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
className={css.PosePortrait}
|
||||
src={poseSrc(mind, pose, import.meta.env.BASE_URL)}
|
||||
alt={`${mind}, ${pose}`}
|
||||
loading="lazy"
|
||||
draggable={false}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
35
src/app/components/message/pose/poses.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { mindOfSender, poseSrc, resolvePose } from './poses';
|
||||
|
||||
describe('resolvePose', () => {
|
||||
it('keeps shared and own-signature labels', () => {
|
||||
expect(resolvePose('aqua', 'amused')).toBe('amused');
|
||||
expect(resolvePose('aqua', 'conduct')).toBe('conduct');
|
||||
expect(resolvePose('kiko', ' Debug ')).toBe('debug');
|
||||
});
|
||||
it('falls back to neutral for missing, unknown, non-string or foreign-signature labels', () => {
|
||||
expect(resolvePose('aqua', undefined)).toBe('neutral');
|
||||
expect(resolvePose('aqua', 'smug')).toBe('neutral');
|
||||
expect(resolvePose('aqua', 7)).toBe('neutral');
|
||||
expect(resolvePose('aqua', 'probe')).toBe('neutral');
|
||||
expect(resolvePose('nobody', 'probe')).toBe('neutral');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mindOfSender', () => {
|
||||
it('matches localpart or first display-name word', () => {
|
||||
expect(mindOfSender('Eureka')).toBe('eureka');
|
||||
expect(mindOfSender('x', 'Libby the Archivist')).toBe('libby');
|
||||
});
|
||||
it('ignores humans and Object prototype names', () => {
|
||||
expect(mindOfSender('alice', 'Alice')).toBeUndefined();
|
||||
expect(mindOfSender('constructor')).toBeUndefined();
|
||||
expect(mindOfSender()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('poseSrc', () => {
|
||||
it('builds the asset path', () => {
|
||||
expect(poseSrc('nibu', 'glitch', '/app/')).toBe('/app/public/poses/nibu/glitch.webp');
|
||||
});
|
||||
});
|
||||
48
src/app/components/message/pose/poses.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Pose labels travel in the event content under POSE_KEY. Art exists per Mind;
|
||||
// every Mind has the shared set plus four signature poses of its own.
|
||||
export const POSE_KEY = 'org.gamecult.pose';
|
||||
|
||||
export const SHARED_POSES = [
|
||||
'neutral',
|
||||
'happy',
|
||||
'amused',
|
||||
'thinking',
|
||||
'concerned',
|
||||
'annoyed',
|
||||
'surprised',
|
||||
'determined',
|
||||
] as const;
|
||||
|
||||
export const MIND_POSES: Record<string, readonly string[]> = {
|
||||
aqua: ['conduct', 'listen', 'patch', 'flourish'],
|
||||
eureka: ['probe', 'inspect', 'report', 'hood'],
|
||||
heimdall: ['badge', 'barrier', 'protect', 'revoke'],
|
||||
kiko: ['tinker', 'confetti', 'sprint', 'debug'],
|
||||
libby: ['stamp', 'research', 'shush', 'schema'],
|
||||
nibu: ['console', 'glitch', 'lore', 'triumph'],
|
||||
weksa: ['map', 'fieldwork', 'argue', 'interlingua'],
|
||||
};
|
||||
|
||||
export const NEUTRAL = 'neutral';
|
||||
|
||||
// A sender is a known Mind when its Matrix localpart or the first word of its
|
||||
// display name is the Mind's name, case-insensitively.
|
||||
export const mindOfSender = (localpart?: string, displayName?: string): string | undefined => {
|
||||
const candidates = [localpart, displayName?.split(/\s+/)[0]];
|
||||
return candidates
|
||||
.map((c) => c?.toLowerCase())
|
||||
.find(
|
||||
(c): c is string => c !== undefined && Object.prototype.hasOwnProperty.call(MIND_POSES, c)
|
||||
);
|
||||
};
|
||||
|
||||
// Unknown, missing or non-string labels, and another Mind's signature, fall back to neutral.
|
||||
export const resolvePose = (mind: string, label: unknown): string => {
|
||||
if (typeof label !== 'string') return NEUTRAL;
|
||||
const l = label.trim().toLowerCase();
|
||||
const known = (SHARED_POSES as readonly string[]).includes(l) || MIND_POSES[mind]?.includes(l);
|
||||
return known ? l : NEUTRAL;
|
||||
};
|
||||
|
||||
export const poseSrc = (mind: string, pose: string, base = '/'): string =>
|
||||
`${base}public/poses/${mind}/${pose}.webp`;
|
||||
|
|
@ -58,7 +58,10 @@ import {
|
|||
isRoomAlias,
|
||||
mxcUrlToHttp,
|
||||
} from '../../../utils/matrix';
|
||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { PoseAvatar } from '../../../components/message/pose/PoseAvatar';
|
||||
import { POSE_KEY, mindOfSender, resolvePose } from '../../../components/message/pose/poses';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
import * as css from './styles.css';
|
||||
|
|
@ -788,6 +791,12 @@ export const Message = as<'div', MessageProps>(
|
|||
</Box>
|
||||
);
|
||||
|
||||
const [poseBubbles] = useSetting(settingsAtom, 'poseBubbles');
|
||||
const mind =
|
||||
poseBubbles && messageLayout === MessageLayout.Bubble
|
||||
? mindOfSender(getMxIdLocalPart(senderId), senderDisplayName)
|
||||
: undefined;
|
||||
|
||||
const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
|
||||
<AvatarBase
|
||||
className={messageLayout === MessageLayout.Bubble ? css.BubbleAvatarBase : undefined}
|
||||
|
|
@ -813,6 +822,15 @@ export const Message = as<'div', MessageProps>(
|
|||
</AvatarBase>
|
||||
);
|
||||
|
||||
const poseJSX = !collapse && mind && (
|
||||
<PoseAvatar
|
||||
mind={mind}
|
||||
pose={resolvePose(mind, mEvent.getContent()[POSE_KEY])}
|
||||
userId={senderId}
|
||||
onClick={onUserClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const msgContentJSX = (
|
||||
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
||||
{reply}
|
||||
|
|
@ -1135,7 +1153,10 @@ export const Message = as<'div', MessageProps>(
|
|||
</CompactLayout>
|
||||
)}
|
||||
{messageLayout === MessageLayout.Bubble && (
|
||||
<BubbleLayout before={avatarJSX} header={headerJSX} onContextMenu={handleContextMenu}>
|
||||
<BubbleLayout
|
||||
pose={!!mind}
|
||||
before={mind ? poseJSX : avatarJSX}
|
||||
header={headerJSX} onContextMenu={handleContextMenu}>
|
||||
{msgContentJSX}
|
||||
</BubbleLayout>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -884,6 +884,7 @@ function Messages() {
|
|||
settingsAtom,
|
||||
'legacyUsernameColor'
|
||||
);
|
||||
const [poseBubbles, setPoseBubbles] = useSetting(settingsAtom, 'poseBubbles');
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideMembershipEvents'
|
||||
|
|
@ -918,6 +919,13 @@ function Messages() {
|
|||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Pose Portraits"
|
||||
description="Show a Mind's pose portrait beside its bubbles (bubble layout only)."
|
||||
after={<Switch variant="Primary" value={poseBubbles} onChange={setPoseBubbles} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Hide Membership Change"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export interface Settings {
|
|||
encUrlPreview: boolean;
|
||||
showHiddenEvents: boolean;
|
||||
legacyUsernameColor: boolean;
|
||||
poseBubbles: boolean;
|
||||
|
||||
showNotifications: boolean;
|
||||
isNotificationSounds: boolean;
|
||||
|
|
@ -73,6 +74,7 @@ const defaultSettings: Settings = {
|
|||
encUrlPreview: false,
|
||||
showHiddenEvents: false,
|
||||
legacyUsernameColor: false,
|
||||
poseBubbles: false,
|
||||
|
||||
showNotifications: true,
|
||||
isNotificationSounds: true,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ const copyFiles = {
|
|||
src: 'public/locales',
|
||||
dest: 'public/',
|
||||
},
|
||||
{
|
||||
src: 'public/poses',
|
||||
dest: 'public/',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||