// SPDX-License-Identifier: AGPL-3.0-or-later import * as Modal from '@app/features/app/components/dialogs/Modal'; import {WHATS_NEW_ENTRIES, type WhatsNewEntry} from '@app/features/app/components/whats_new/WhatsNewEntries'; import styles from '@app/features/app/components/whats_new/WhatsNewModal.module.css'; import {BLUESKY_PROVIDER_NAME} from '@app/features/app/config/I18nDisplayConstants'; import {SafeMarkdown} from '@app/features/messaging/components/markdown'; import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; import markupStyles from '@app/features/theme/styles/Markup.module.css'; import {Button} from '@app/features/ui/button/Button'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import {BlueskyIcon} from '@app/features/ui/components/icons/BlueskyIcon'; import type {ScrollerHandle} from '@app/features/ui/components/Scroller'; import WhatsNew from '@app/features/ui/state/WhatsNew'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import {ExternalUrls} from '@fluxer/constants/src/ExternalUrls'; import {getFormattedLongDate} from '@fluxer/date_utils/src/DateFormatting'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {CaretDownIcon} from '@phosphor-icons/react'; import {useCallback, useEffect, useRef, useState} from 'react'; const WHAT_S_NEW_DESCRIPTOR = msg({ message: "What's new", comment: 'Short label in the whats new modal.', }); const SCROLL_TO_BOTTOM_DESCRIPTOR = msg({ message: 'Scroll to bottom', comment: 'Short label in the whats new modal.', }); const FOLLOW_US_ON_DESCRIPTOR = msg({ message: 'Follow us on {blueskyProviderName}', comment: 'Short label in the whats new modal. Preserve {blueskyProviderName}; it is inserted by code.', }); const GOT_IT_DESCRIPTOR = msg({ message: 'Got it', comment: 'Short label in the whats new modal.', }); const WHATS_NEW_MODAL_KEY = 'whats-new'; export function openWhatsNewModal(): void { const latestEntry = WHATS_NEW_ENTRIES[0]; if (!latestEntry) return; ModalCommands.pushWithKey( modal(() => ( )), WHATS_NEW_MODAL_KEY, ); } interface WhatsNewModalProps { entry: WhatsNewEntry; } function formatEntryDate(date: Date): string { return getFormattedLongDate(date, getCurrentLocale()); } export function WhatsNewModal({entry}: WhatsNewModalProps) { const {i18n} = useLingui(); const initialFocusRef = useRef(null); const scrollerRef = useRef(null); const [isScrollHintVisible, setIsScrollHintVisible] = useState(false); const handleDismiss = useCallback(() => { WhatsNew.dismiss(entry.id); ModalCommands.pop(); }, [entry.id]); const updateScrollHintVisibility = useCallback(() => { const scrollerNode = scrollerRef.current?.getViewportElement(); if (!scrollerNode) { setIsScrollHintVisible(false); return; } const maxScroll = Math.max(0, scrollerNode.scrollHeight - scrollerNode.clientHeight); const distanceFromBottom = maxScroll - scrollerNode.scrollTop; const shouldShowScrollHint = maxScroll > 16 && scrollerNode.scrollTop < 72 && distanceFromBottom > 24; setIsScrollHintVisible(shouldShowScrollHint); }, []); const handleContentScroll = useCallback(() => { updateScrollHintVisibility(); }, [updateScrollHintVisibility]); const handleContentResize = useCallback(() => { updateScrollHintVisibility(); }, [updateScrollHintVisibility]); const handleScrollToBottom = useCallback(() => { scrollerRef.current?.jumpToEndEdge({animate: true}); }, []); useEffect(() => { updateScrollHintVisibility(); }, [entry.id, updateScrollHintVisibility]); return ( {formatEntryDate(entry.date)} {i18n._(FOLLOW_US_ON_DESCRIPTOR, {blueskyProviderName: BLUESKY_PROVIDER_NAME})} {i18n._(GOT_IT_DESCRIPTOR)} ); }