// SPDX-License-Identifier: AGPL-3.0-or-later
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import {LongPressable} from '@app/features/app/components/LongPressable';
import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback';
import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAutofocus';
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import styles from '@app/features/channel/components/GifPicker.module.css';
import memeStyles from '@app/features/channel/components/MemesPicker.module.css';
import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEmptyState';
import {PickerSearchInput} from '@app/features/channel/components/shared/PickerSearchInput';
import * as FavoriteMemeCommands from '@app/features/expressions/commands/FavoriteMemeCommands';
import {EditFavoriteMemeModal} from '@app/features/expressions/components/modals/EditFavoriteMemeModal';
import {
ExpressionPickerHeaderPortal,
useExpressionPickerHeaderPortal,
} from '@app/features/expressions/components/popouts/ExpressionPickerPopout';
import type {FavoriteMeme} from '@app/features/expressions/models/FavoriteMeme';
import FavoriteMemes from '@app/features/expressions/state/FavoriteMemes';
import {AUDIO_DESCRIPTOR, GIFS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
import {DeleteIcon, EditIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
import {MenuBottomSheet, type MenuItemType} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {formatDuration as formatDurationBase} from '@fluxer/date_utils/src/DateDuration';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {GifIcon, ImageIcon, MusicNoteIcon, SmileySadIcon, VideoCameraIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import {AnimatePresence} from 'framer-motion';
import {matchSorter} from 'match-sorter';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
const ALL_DESCRIPTOR = msg({
message: 'All',
comment: 'Tab label in the mobile memes (saved media) picker. Shows all saved media regardless of type.',
});
const IMAGES_DESCRIPTOR = msg({
message: 'Images',
comment: 'Tab label in the mobile memes picker. Filters to saved images.',
});
const VIDEOS_DESCRIPTOR = msg({
message: 'Videos',
comment: 'Tab label in the mobile memes picker. Filters to saved videos.',
});
const SEARCH_SAVED_MEDIA_DESCRIPTOR = msg({
message: 'Search saved media',
comment: 'Placeholder text in the mobile memes picker search input.',
});
const NO_SAVED_MEDIA_DESCRIPTOR = msg({
message: 'No saved media',
comment: 'Empty state title in the mobile memes picker when the user has saved nothing yet.',
});
const SAVE_SOME_MEDIA_FROM_MESSAGES_TO_GET_STARTED_DESCRIPTOR = msg({
message: 'Save some media from messages to get started!',
comment: 'Empty state body in the mobile memes picker explaining how to save media. Tone can be friendly.',
});
const NO_RESULTS_DESCRIPTOR = msg({
message: 'No results',
comment: 'Empty state title in the mobile memes picker when the search query matches nothing.',
});
const TRY_A_DIFFERENT_SEARCH_TERM_OR_FILTER_DESCRIPTOR = msg({
message: 'Try a different search term or filter',
comment: 'Empty state body in the mobile memes picker suggesting query changes.',
});
const SELECT_DESCRIPTOR = msg({
message: 'Select {accessibleName}',
comment:
'Accessible label for a saved media tile in the mobile memes picker. accessibleName is the media filename or alt text.',
});
const DELETE_SAVED_MEDIA_DESCRIPTOR = msg({
message: 'Delete saved media',
comment: 'Accessible label and tooltip for the delete button on a saved media tile in the mobile memes picker.',
});
const ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_DESCRIPTOR = msg({
message: 'Delete "{memeName}"? Can\'t be undone.',
comment:
'Error message in the channel and chat mobile memes picker. Preserve {memeName}; it is inserted by code. Keep the tone plain and specific.',
});
const DELETE_DESCRIPTOR = msg({
message: 'Delete',
comment:
'Button or menu action label in the channel and chat mobile memes picker. Keep it concise. Keep the tone plain and specific.',
});
const EDIT_SAVED_MEDIA_DESCRIPTOR = msg({
message: 'Edit saved media',
comment: 'Button or menu action label in the channel and chat mobile memes picker. Keep it concise.',
});
const GRID_ITEM_PRELOAD_ROOT_MARGIN = '800px 0px';
type ContentType = 'all' | 'image' | 'video' | 'audio' | 'gif';
interface FilterOption {
type: ContentType;
label: string;
icon?: React.ReactNode;
}
const formatDuration = (seconds: number | null | undefined, locale: string): string => {
if (!seconds || seconds <= 0) return formatDurationBase(0, locale);
return formatDurationBase(seconds, locale);
};
const getFileExtension = (filename: string, contentType: string): string => {
const extension = filename.split('.').pop()?.toUpperCase();
if (extension && extension.length <= 4) {
return extension;
}
const typeMatch = contentType.match(/\/([^;]+)/);
return typeMatch?.[1]?.toUpperCase() || 'FILE';
};
const GifIndicator = observer(() => (