feat: auto-clear status after configurable duration

Adds an 'Auto-clear after' dropdown to the Status Message settings tile
with options: Never / 30 min / 1 hr / 4 hr / 8 hr / Until midnight /
1 day / 7 days.

How it works:
- On save, stores the expiry timestamp in localStorage keyed by userId
  (lotus-status-expiry-<userId>) and sets expiryTs state
- A single useEffect on expiryTs drives the timer — re-saving cancels
  the previous timer cleanly via useEffect cleanup
- On mount, reads stored expiry from localStorage so auto-clear
  survives page reloads (fires immediately if already expired)
- Manual Clear Status also removes the stored expiry and cancels any
  active timer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:10:46 -04:00
parent e280f0e312
commit 01a61e3ec2
+90 -6
View File
@@ -319,19 +319,68 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
);
}
const STATUS_EXPIRY_KEY = (id: string) => `lotus-status-expiry-${id}`;
const CLEAR_AFTER_OPTIONS = [
{ label: 'Never', value: '0' },
{ label: '30 minutes', value: String(30 * 60 * 1000) },
{ label: '1 hour', value: String(60 * 60 * 1000) },
{ label: '4 hours', value: String(4 * 60 * 60 * 1000) },
{ label: '8 hours', value: String(8 * 60 * 60 * 1000) },
{ label: 'Until midnight', value: 'today' },
{ label: '1 day', value: String(24 * 60 * 60 * 1000) },
{ label: '7 days', value: String(7 * 24 * 60 * 60 * 1000) },
];
function getMsFromOption(value: string): number {
if (value === '0') return 0;
if (value === 'today') {
const eod = new Date();
eod.setHours(23, 59, 59, 999);
return eod.getTime() - Date.now();
}
return parseInt(value, 10);
}
function ProfileStatus() {
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const presence = useUserPresence(userId);
const [statusMsg, setStatusMsg] = useState<string>(presence?.status ?? '');
const [clearAfter, setClearAfter] = useState('0');
const [emojiAnchor, setEmojiAnchor] = useState<RectCords>();
const inputRef = useRef<HTMLInputElement>(null);
// Initialise expiry from localStorage so timer survives page reload
const [expiryTs, setExpiryTs] = useState<number>(() => {
const stored = localStorage.getItem(STATUS_EXPIRY_KEY(userId));
return stored ? parseInt(stored, 10) : 0;
});
// Sync input when another device changes the status
useEffect(() => {
setStatusMsg(presence?.status ?? '');
}, [presence?.status]);
// Drive the auto-clear timer off expiryTs so re-saving cancels the old timer
useEffect(() => {
if (!expiryTs) return undefined;
const remaining = expiryTs - Date.now();
if (remaining <= 0) {
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
return undefined;
}
const timer = window.setTimeout(() => {
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
}, remaining);
return () => clearTimeout(timer);
}, [expiryTs, userId, mx]);
const [saveState, saveStatus] = useAsyncCallback(
useCallback(
(msg: string) =>
@@ -352,7 +401,6 @@ function ProfileStatus() {
const end = input.selectionEnd ?? statusMsg.length;
const next = statusMsg.slice(0, start) + unicode + statusMsg.slice(end);
setStatusMsg(next);
// restore cursor after emoji insertion
requestAnimationFrame(() => {
input.focus();
input.setSelectionRange(start + unicode.length, start + unicode.length);
@@ -372,15 +420,25 @@ function ProfileStatus() {
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (saving) return;
saveStatus(statusMsg.trim());
const msg = statusMsg.trim();
saveStatus(msg);
const delayMs = getMsFromOption(clearAfter);
if (msg && delayMs > 0) {
const ts = Date.now() + delayMs;
localStorage.setItem(STATUS_EXPIRY_KEY(userId), String(ts));
setExpiryTs(ts);
} else {
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
}
};
const handleClear = () => {
setStatusMsg('');
mx.setPresence({
presence: 'online',
status_msg: '',
});
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
};
const hasChanges = statusMsg !== (presence?.status ?? '');
@@ -460,6 +518,32 @@ function ProfileStatus() {
<Text size="B400">Save</Text>
</Button>
</Box>
<Box alignItems="Center" gap="200">
<Text size="T200" style={{ opacity: 0.6, whiteSpace: 'nowrap', flexShrink: 0 }}>
Auto-clear after:
</Text>
<select
value={clearAfter}
onChange={(e) => setClearAfter(e.target.value)}
aria-label="Auto-clear status after"
style={{
background: 'var(--bg-surface-variant)',
border: `1px solid var(--border-surface-variant)`,
borderRadius: config.radii.R300,
color: 'inherit',
fontSize: '0.82rem',
padding: `${config.space.S100} ${config.space.S200}`,
cursor: 'pointer',
outline: 'none',
}}
>
{CLEAR_AFTER_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</Box>
{(presence?.status || statusMsg) && (
<Button
size="300"