feat(status): built-in and custom status presets

The Status Message field required typing every status from scratch. Add
a one-click preset row:

- Built-in "Quick statuses" spanning gaming, social, life and work
  (Gaming, In a party, Ranked grind, AFK, Watching, In a meeting,
  Working remotely, Lunch, On vacation, Out sick...), each carrying a
  suggested auto-clear so a click sets the message and the timer at once.
- Custom presets: save the current status as a reusable preset, stored
  in io.lotus.status_presets account data (synced across devices via the
  shared account-data list store), de-duped by normalized label, capped
  at 20, deletable inline.

The existing save path is factored into a shared applyStatus() used by
the Save button and by preset apply, so server writes, the status
localStorage keys, and the auto-clear expiry bookkeeping stay identical.
Ordering/de-dupe logic is pure in utils/statusPresets.ts (upsertPreset,
normalizeLabel) with unit tests; no change to the presence wire format,
expiry monitor, or presence-mode selector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 23:30:08 -04:00
co-authored by Claude Opus 4.8
parent 39e75f4eea
commit d0614710b0
5 changed files with 282 additions and 20 deletions
+43
View File
@@ -0,0 +1,43 @@
import { useCallback } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { createAccountDataListStore } from './createAccountDataListStore';
import { StatusPreset, upsertPreset } from '../utils/statusPresets';
const STATUS_PRESETS_KEY = 'io.lotus.status_presets';
const MAX_PRESETS = 20;
type StatusPresetsContent = {
presets: StatusPreset[];
};
// Shared, concurrency-safe store. See createAccountDataListStore for why the
// snapshot + write queue must be module-scoped (writes are serialized to avoid
// lost updates, since setAccountData replaces the whole content with no merge).
const statusPresetsStore = createAccountDataListStore<StatusPreset[], StatusPresetsContent>({
eventType: STATUS_PRESETS_KEY,
read: (content) => content?.presets ?? [],
write: (presets) => ({ presets }),
});
export function useStatusPresets(): {
presets: StatusPreset[];
addPreset: (preset: StatusPreset) => Promise<void>;
removePreset: (id: string) => Promise<void>;
} {
const mx = useMatrixClient();
const presets = statusPresetsStore.useValue(mx);
const addPreset = useCallback(
(preset: StatusPreset) =>
statusPresetsStore.enqueueWrite(mx, (current) => upsertPreset(current, preset, MAX_PRESETS)),
[mx],
);
const removePreset = useCallback(
(id: string) =>
statusPresetsStore.enqueueWrite(mx, (current) => current.filter((p) => p.id !== id)),
[mx],
);
return { presets, addPreset, removePreset };
}