diff --git a/src/app/components/message/pose/poses.test.ts b/src/app/components/message/pose/poses.test.ts index 81c2f0c6..c6089f62 100644 --- a/src/app/components/message/pose/poses.test.ts +++ b/src/app/components/message/pose/poses.test.ts @@ -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: [] }); - }); -}); diff --git a/src/app/components/message/pose/poses.ts b/src/app/components/message/pose/poses.ts index 7175196a..69429090 100644 --- a/src/app/components/message/pose/poses.ts +++ b/src/app/components/message/pose/poses.ts @@ -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(); - 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())}`; -}; diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index cf91c638..3fbd81fa 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -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} /> - diff --git a/src/app/features/room/RoomViewPoseStatus.css.ts b/src/app/features/room/RoomViewPoseStatus.css.ts deleted file mode 100644 index d8319d0d..00000000 --- a/src/app/features/room/RoomViewPoseStatus.css.ts +++ /dev/null @@ -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' } }, - }, -]); diff --git a/src/app/features/room/RoomViewPoseStatus.tsx b/src/app/features/room/RoomViewPoseStatus.tsx deleted file mode 100644 index e62f81c0..00000000 --- a/src/app/features/room/RoomViewPoseStatus.tsx +++ /dev/null @@ -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 ( -
- {statuses.map((st) => ( - - {poseStatusLine(st)} - - ))} - {rest.length > 0 && ( - - )} -
- ); -}