nemesis: drop the pose status strip; the portraits already show each Mind's mood (Meta: redundant, wastes vertical space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d2d6fbcc00
commit
9b9b495def
5 changed files with 0 additions and 219 deletions
|
|
@ -3,10 +3,7 @@ import catalogue from './catalogue.json';
|
|||
import {
|
||||
mindOfSender,
|
||||
poseSrc,
|
||||
poseStatusLine,
|
||||
poseStatuses,
|
||||
resolvePose,
|
||||
splitNewestStatus,
|
||||
} from './poses';
|
||||
|
||||
describe('resolvePose', () => {
|
||||
|
|
@ -53,52 +50,5 @@ describe('poseSrc', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('poseStatuses', () => {
|
||||
const ev = (sender: string, ts: number, label: unknown) => ({
|
||||
sender,
|
||||
localpart: sender,
|
||||
ts,
|
||||
label,
|
||||
});
|
||||
it('reports the latest pose per Mind and when its unbroken run began', () => {
|
||||
const out = poseStatuses([
|
||||
ev('aqua', 1, 'happy'),
|
||||
ev('aqua', 2, 'sigh'),
|
||||
ev('aqua', 3, 'sigh'),
|
||||
ev('alice', 4, 'happy'),
|
||||
ev('eureka', 5, 'probe'),
|
||||
]);
|
||||
expect(out).toEqual([
|
||||
{ mind: 'aqua', pose: 'sigh', since: 2 },
|
||||
{ mind: 'eureka', pose: 'probe', since: 5 },
|
||||
]);
|
||||
});
|
||||
it('treats unknown labels as neutral runs', () => {
|
||||
expect(poseStatuses([ev('void', 1, 'bogus'), ev('void', 2, undefined)])).toEqual([
|
||||
{ mind: 'void', pose: 'neutral', since: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('poseStatusLine', () => {
|
||||
it('formats the client-status line', () => {
|
||||
const since = new Date(2026, 10, 4, 9, 5).getTime();
|
||||
expect(poseStatusLine({ mind: 'void', pose: 'reading', since })).toBe(
|
||||
'Ratatoskr: Void is reading, since 09:05'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitNewestStatus', () => {
|
||||
const s = (mind: string, since: number) => ({ mind, pose: 'happy', since });
|
||||
|
||||
it('picks the status that changed last and keeps the others in their order', () => {
|
||||
const { newest, rest } = splitNewestStatus([s('eureka', 100), s('void', 300), s('nibu', 200)]);
|
||||
expect(newest?.mind).toBe('void');
|
||||
expect(rest.map((r) => r.mind)).toEqual(['eureka', 'nibu']);
|
||||
});
|
||||
|
||||
it('has no newest for an empty list', () => {
|
||||
expect(splitNewestStatus([])).toEqual({ newest: undefined, rest: [] });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,50 +40,3 @@ export const resolvePose = (mind: string, label: unknown): string => {
|
|||
export const poseSrc = (mind: string, pose: string, base = '/'): string =>
|
||||
`${base}public/poses/${artPath(mind, pose) ?? artPath(mind, NEUTRAL)}`;
|
||||
|
||||
export type PoseStatus = { mind: string; pose: string; since: number };
|
||||
export type PoseEvent = {
|
||||
sender: string;
|
||||
localpart?: string;
|
||||
name?: string;
|
||||
ts: number;
|
||||
label: unknown;
|
||||
};
|
||||
|
||||
// Latest pose per Mind in a room's events (oldest first), and when that pose
|
||||
// began: the first of its unbroken run of messages carrying the same pose.
|
||||
export const poseStatuses = (events: PoseEvent[]): PoseStatus[] => {
|
||||
const bySender = new Map<string, { mind: string; run: { pose: string; ts: number }[] }>();
|
||||
events.forEach((e) => {
|
||||
const mind = mindOfSender(e.localpart, e.name);
|
||||
if (!mind) return;
|
||||
const entry = bySender.get(e.sender) ?? { mind, run: [] };
|
||||
entry.run.push({ pose: resolvePose(mind, e.label), ts: e.ts });
|
||||
bySender.set(e.sender, entry);
|
||||
});
|
||||
return [...bySender.values()].map(({ mind, run }) => {
|
||||
const last = run[run.length - 1];
|
||||
let i = run.length - 1;
|
||||
while (i > 0 && run[i - 1].pose === last.pose) i -= 1;
|
||||
return { mind, pose: last.pose, since: run[i].ts };
|
||||
});
|
||||
};
|
||||
|
||||
// The status that changed last, and the rest in their given order (phones show only the first).
|
||||
export const splitNewestStatus = (
|
||||
statuses: PoseStatus[]
|
||||
): { newest: PoseStatus | undefined; rest: PoseStatus[] } => {
|
||||
let newest: PoseStatus | undefined;
|
||||
statuses.forEach((st) => {
|
||||
if (!newest || st.since > newest.since) newest = st;
|
||||
});
|
||||
return { newest, rest: statuses.filter((st) => st !== newest) };
|
||||
};
|
||||
|
||||
const two = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
// Client-status text. Rendered locally only; it is never sent to a room.
|
||||
export const poseStatusLine = ({ mind, pose, since }: PoseStatus): string => {
|
||||
const d = new Date(since);
|
||||
const name = mind.charAt(0).toUpperCase() + mind.slice(1);
|
||||
return `Ratatoskr: ${name} is ${pose}, since ${two(d.getHours())}:${two(d.getMinutes())}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { useEditor } from '../../components/editor';
|
|||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||
import { RoomTimeline } from './RoomTimeline';
|
||||
import { RoomViewTyping } from './RoomViewTyping';
|
||||
import { RoomViewPoseStatus } from './RoomViewPoseStatus';
|
||||
import { RoomTombstone } from './RoomTombstone';
|
||||
import { RoomInput } from './RoomInput';
|
||||
import { RoomTrainingLine } from './RoomTrainingLine';
|
||||
|
|
@ -102,7 +101,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
roomInputRef={roomInputRef}
|
||||
editor={editor}
|
||||
/>
|
||||
<RoomViewPoseStatus room={room} />
|
||||
<RoomViewTyping room={room} />
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column">
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
const NARROW = 'screen and (max-width: 480px)';
|
||||
|
||||
export const StatusBlock = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
padding: `0 ${toRem(16)}`,
|
||||
// Phones: the newest mood and its "+N more" share one line.
|
||||
'@media': { [NARROW]: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center' } },
|
||||
},
|
||||
]);
|
||||
|
||||
// On phones only the newest mood shows until the strip is tapped; wide screens show every line.
|
||||
export const StatusRow = style({
|
||||
'@media': {
|
||||
[NARROW]: {
|
||||
selectors: {
|
||||
'&[data-newest="false"]': { display: 'none' },
|
||||
[`${StatusBlock}[data-open="true"] &`]: { display: 'block', flexBasis: '100%' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const StatusToggle = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'none',
|
||||
minHeight: toRem(24),
|
||||
padding: `0 ${config.space.S100}`,
|
||||
color: color.Primary.Main,
|
||||
cursor: 'pointer',
|
||||
'@media': { [NARROW]: { display: 'block' } },
|
||||
},
|
||||
]);
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Text } from 'folds';
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { getMemberDisplayName } from '../../utils/room';
|
||||
import {
|
||||
POSE_KEY,
|
||||
poseStatusLine,
|
||||
poseStatuses,
|
||||
splitNewestStatus,
|
||||
} from '../../components/message/pose/poses';
|
||||
import * as css from './RoomViewPoseStatus.css';
|
||||
|
||||
// Client-status lines about each Mind's current pose. They are drawn from the
|
||||
// timeline already in memory and are never sent as anyone's message.
|
||||
export function RoomViewPoseStatus({ room }: { room: Room }) {
|
||||
const [poseBubbles] = useSetting(settingsAtom, 'poseBubbles');
|
||||
const [, bump] = useState(0);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onTimeline = () => bump((n) => n + 1);
|
||||
room.on(RoomEvent.Timeline, onTimeline);
|
||||
return () => {
|
||||
room.removeListener(RoomEvent.Timeline, onTimeline);
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
if (!poseBubbles) return null;
|
||||
|
||||
const statuses = poseStatuses(
|
||||
room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.filter((e) => e.getType() === 'm.room.message' && e.getSender())
|
||||
.map((e) => {
|
||||
const sender = e.getSender() as string;
|
||||
return {
|
||||
sender,
|
||||
localpart: getMxIdLocalPart(sender),
|
||||
name: getMemberDisplayName(room, sender),
|
||||
ts: e.getTs(),
|
||||
label: e.getContent()[POSE_KEY],
|
||||
};
|
||||
})
|
||||
);
|
||||
if (statuses.length === 0) return null;
|
||||
|
||||
const { newest, rest } = splitNewestStatus(statuses);
|
||||
|
||||
return (
|
||||
<div className={css.StatusBlock} data-open={open}>
|
||||
{statuses.map((st) => (
|
||||
<Text
|
||||
key={st.mind}
|
||||
className={css.StatusRow}
|
||||
data-newest={st === newest}
|
||||
size="T200"
|
||||
priority="300"
|
||||
>
|
||||
{poseStatusLine(st)}
|
||||
</Text>
|
||||
))}
|
||||
{rest.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.StatusToggle}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
<Text as="span" size="T200">
|
||||
{open ? 'fewer' : `+${rest.length} more`}
|
||||
</Text>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue