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:
Eureka Self 2026-11-10 14:09:58 +00:00
commit 9b9b495def
5 changed files with 0 additions and 219 deletions

View file

@ -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: [] });
});
});

View file

@ -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())}`;
};

View file

@ -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">

View file

@ -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' } },
},
]);

View file

@ -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>
);
}