/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {Trans, useLingui} from '@lingui/react/macro'; import {CaretDownIcon, CheckIcon} from '@phosphor-icons/react'; import clsx from 'clsx'; import {observer} from 'mobx-react-lite'; import React from 'react'; import * as UserSettingsActionCreators from '~/actions/UserSettingsActionCreators'; import {getStatusTypeLabel, type StatusType, StatusTypes} from '~/Constants'; import {CustomStatusDisplay} from '~/components/common/CustomStatusDisplay/CustomStatusDisplay'; import {CustomStatusBottomSheet} from '~/components/modals/CustomStatusBottomSheet'; import {BottomSheet} from '~/components/uikit/BottomSheet/BottomSheet'; import {StatusIndicator} from '~/components/uikit/StatusIndicator'; import {normalizeCustomStatus} from '~/lib/customStatus'; import PresenceStore from '~/stores/PresenceStore'; import StatusExpiryStore from '~/stores/StatusExpiryStore'; import UserStore from '~/stores/UserStore'; import styles from './StatusChangeBottomSheet.module.css'; const STATUS_ORDER = [StatusTypes.ONLINE, StatusTypes.IDLE, StatusTypes.DND, StatusTypes.INVISIBLE] as const; const STATUS_DESCRIPTIONS: Record<(typeof STATUS_ORDER)[number], React.ReactNode | null> = { [StatusTypes.ONLINE]: null, [StatusTypes.IDLE]: null, [StatusTypes.DND]: You won't receive notifications on desktop, [StatusTypes.INVISIBLE]: You'll appear offline, }; const EXPIRY_OPTIONS = [ {id: 'forever', label: Until I change it, durationMs: null}, {id: '15m', label: 15 minutes, durationMs: 15 * 60 * 1000}, {id: '1h', label: 1 hour, durationMs: 60 * 60 * 1000}, {id: '8h', label: 8 hours, durationMs: 8 * 60 * 60 * 1000}, {id: '24h', label: 24 hours, durationMs: 24 * 60 * 60 * 1000}, {id: '3d', label: 3 days, durationMs: 3 * 24 * 60 * 60 * 1000}, ]; const STATUS_SHEET_SNAP_POINTS: Array = [0, 0.75, 1]; interface StatusChangeBottomSheetProps { isOpen: boolean; onClose: () => void; } interface StatusItemProps { status: StatusType; currentStatus: StatusType; onSelect: (status: StatusType, durationMs: number | null) => void; } const StatusItem = observer(({status, currentStatus, onSelect}: StatusItemProps) => { const {i18n} = useLingui(); const isSelected = currentStatus === status; const description = STATUS_DESCRIPTIONS[status as keyof typeof STATUS_DESCRIPTIONS]; const hasExpiryOptions = status !== StatusTypes.ONLINE; const [showExpiry, setShowExpiry] = React.useState(false); const handleSelect = () => { if (hasExpiryOptions) { setShowExpiry(!showExpiry); } else { onSelect(status, null); } }; const handleExpirySelect = (durationMs: number | null) => { onSelect(status, durationMs); setShowExpiry(false); }; return (
{showExpiry && (
{EXPIRY_OPTIONS.map((option) => ( ))}
)}
); }); interface CustomStatusSectionProps { onOpenEditor: () => void; } const CustomStatusSection = observer(({onOpenEditor}: CustomStatusSectionProps) => { const currentUser = UserStore.getCurrentUser(); const currentUserId = currentUser?.id ?? null; const existingCustomStatus = currentUserId ? PresenceStore.getCustomStatus(currentUserId) : null; const normalizedExisting = normalizeCustomStatus(existingCustomStatus); const hasExistingStatus = Boolean(normalizedExisting); const [isSaving, setIsSaving] = React.useState(false); const handleClear = async () => { if (isSaving) return; setIsSaving(true); try { await UserSettingsActionCreators.update({customStatus: null}); } finally { setIsSaving(false); } }; return (
Custom Status
{hasExistingStatus && ( )}
); }); export const StatusChangeBottomSheet = observer(({isOpen, onClose}: StatusChangeBottomSheetProps) => { const {t} = useLingui(); const currentUser = UserStore.getCurrentUser(); const currentUserId = currentUser?.id ?? null; const status = currentUserId ? PresenceStore.getStatus(currentUserId) : StatusTypes.ONLINE; const [customStatusSheetOpen, setCustomStatusSheetOpen] = React.useState(false); const handleStatusChange = React.useCallback( (statusType: StatusType, durationMs: number | null) => { StatusExpiryStore.setActiveStatusExpiry({ status: statusType, durationMs, }); onClose(); }, [onClose], ); const handleOpenCustomStatusEditor = React.useCallback(() => { setCustomStatusSheetOpen(true); }, []); const handleCloseCustomStatusEditor = React.useCallback(() => { setCustomStatusSheetOpen(false); }, []); return ( <>
Online Status
{STATUS_ORDER.map((statusType, index, arr) => ( {index < arr.length - 1 &&
} ))}
); });