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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user