mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): shrink stored favorite gifs and raise their budget (#2643)
This commit is contained in:
1 parent
5fea319f4e
commit
905af5dd5a
13 files changed
+191
-215
No files matched your search
-50
@@ -1,50 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
padding-inline-start: 0.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listItem::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--text-chat-muted);
|
||||
}
|
||||
|
||||
.listItem strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--text-chat-muted);
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import styles from '@app/features/channel/components/pickers/gif/FavoriteGifFirstTimePromptModal.module.css';
|
||||
import FavoriteGif from '@app/features/expressions/state/FavoriteGif';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {formatUserSettingsPath} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useRef} from 'react';
|
||||
|
||||
const HOW_SHOULD_WE_SAVE_YOUR_GIF_FAVORITES_DESCRIPTOR = msg({
|
||||
message: 'How should we save your GIF favorites?',
|
||||
comment: 'Confirmation prompt in the channel and chat favorite gif first time prompt modal.',
|
||||
});
|
||||
|
||||
interface FavoriteGifFirstTimePromptModalProps {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const FavoriteGifFirstTimePromptModal = observer(function FavoriteGifFirstTimePromptModal({
|
||||
onConfirm,
|
||||
}: FavoriteGifFirstTimePromptModalProps) {
|
||||
const {i18n} = useLingui();
|
||||
const initialFocusRef = useRef<HTMLButtonElement | null>(null);
|
||||
const mediaSettingsPath = formatUserSettingsPath(i18n, 'chat_settings', 'media');
|
||||
const handleConfirm = () => {
|
||||
FavoriteGif.setSaveGifFavoritesAsSavedMedia(false);
|
||||
FavoriteGif.markFirstTimePromptSeen();
|
||||
ModalCommands.pop();
|
||||
onConfirm();
|
||||
};
|
||||
const handleUseSavedMedia = () => {
|
||||
FavoriteGif.setSaveGifFavoritesAsSavedMedia(true);
|
||||
FavoriteGif.markFirstTimePromptSeen();
|
||||
ModalCommands.pop();
|
||||
onConfirm();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
ModalCommands.pop();
|
||||
};
|
||||
return (
|
||||
<Modal.Root
|
||||
size="small"
|
||||
centered
|
||||
initialFocusRef={initialFocusRef}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.modal-root"
|
||||
>
|
||||
<Modal.Header
|
||||
title={i18n._(HOW_SHOULD_WE_SAVE_YOUR_GIF_FAVORITES_DESCRIPTOR)}
|
||||
onClose={handleCancel}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.modal-header"
|
||||
/>
|
||||
<Modal.Content data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.modal-content">
|
||||
<div className={styles.body} data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.body">
|
||||
<p
|
||||
className={styles.description}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.description"
|
||||
>
|
||||
<Trans>
|
||||
You can store starred GIFs as URL-only favorites or upload them to your saved media. Pick the one that
|
||||
fits how you use them. You can change it any time in {mediaSettingsPath}.
|
||||
</Trans>
|
||||
</p>
|
||||
<ul className={styles.list} data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.list">
|
||||
<li
|
||||
className={styles.listItem}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.list-item"
|
||||
>
|
||||
<Trans>
|
||||
<strong data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.strong">
|
||||
URL-only favorites (default)
|
||||
</strong>
|
||||
: synced across your devices, no upload, doesn't count against saved media. The original media may
|
||||
disappear if its host removes it.
|
||||
</Trans>
|
||||
</li>
|
||||
<li
|
||||
className={styles.listItem}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.list-item--2"
|
||||
>
|
||||
<Trans>
|
||||
<strong data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.strong--2">
|
||||
Saved media
|
||||
</strong>
|
||||
: uploaded, taggable, searchable, and persistent, but counts against your saved media limit.
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.hint} data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.hint">
|
||||
<Trans>We'll only ask once.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Footer data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.modal-footer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleUseSavedMedia}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.button.use-saved-media"
|
||||
>
|
||||
<Trans>Use saved media</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
ref={initialFocusRef}
|
||||
data-flx="channel.pickers.gif.favorite-gif-first-time-prompt-modal.button.confirm"
|
||||
>
|
||||
<Trans>Use URL-only (recommended)</Trans>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Root>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {type FavoriteGifEntry, slimFavoriteGifEntry} from './FavoriteGifTypes';
|
||||
|
||||
function format(src: string, width: number) {
|
||||
return {src, proxy_src: `https://media.test/external/sig/${src}`, width, height: Math.round(width * 0.84)};
|
||||
}
|
||||
|
||||
const FAT: FavoriteGifEntry = {
|
||||
url: 'https://klipy.com/gifs/reaction',
|
||||
proxy_url: 'https://media.test/external/sig/tiny.webm',
|
||||
width: 220,
|
||||
height: 185,
|
||||
media: {
|
||||
nanowebm: format('nano.webm', 90),
|
||||
tinywebm: format('tiny.webm', 220),
|
||||
mediumwebm: format('medium.webm', 320),
|
||||
webm: format('full.webm', 498),
|
||||
gif: format('full.gif', 498),
|
||||
},
|
||||
content_type: 'video/webm',
|
||||
placeholder: 'placeholder',
|
||||
};
|
||||
|
||||
describe('slimFavoriteGifEntry', () => {
|
||||
it('drops the media map so a favorite is cheap to sync', () => {
|
||||
expect(slimFavoriteGifEntry(FAT).media).toEqual({});
|
||||
});
|
||||
|
||||
it('promotes a preview that covers a 200px tile at 2x into the top-level fields', () => {
|
||||
const slim = slimFavoriteGifEntry(FAT);
|
||||
expect(slim.proxy_url).toBe(FAT.media.webm.proxy_src);
|
||||
expect(slim.width).toBe(498);
|
||||
expect(slim.content_type).toBe('video/webm');
|
||||
});
|
||||
|
||||
it('keeps url and placeholder untouched', () => {
|
||||
const slim = slimFavoriteGifEntry(FAT);
|
||||
expect(slim.url).toBe(FAT.url);
|
||||
expect(slim.placeholder).toBe(FAT.placeholder);
|
||||
});
|
||||
|
||||
it('is idempotent so the synced roundtrip stays stable', () => {
|
||||
const once = slimFavoriteGifEntry(FAT);
|
||||
expect(slimFavoriteGifEntry(once)).toEqual(once);
|
||||
});
|
||||
|
||||
it('leaves an entry that already carries no media alone', () => {
|
||||
const urlOnly: FavoriteGifEntry = {...FAT, media: {}};
|
||||
expect(slimFavoriteGifEntry(urlOnly)).toBe(urlOnly);
|
||||
});
|
||||
|
||||
it('does not invent a preview when every format is unusable', () => {
|
||||
const broken: FavoriteGifEntry = {...FAT, media: {webm: {src: '', proxy_src: '', width: 0, height: 0}}};
|
||||
const slim = slimFavoriteGifEntry(broken);
|
||||
expect(slim.media).toEqual({});
|
||||
expect(slim.proxy_url).toBe(FAT.proxy_url);
|
||||
});
|
||||
});
|
||||
@@ -105,4 +105,24 @@ export function pickCanonicalPreviewFormat(
|
||||
return null;
|
||||
}
|
||||
|
||||
export const STORED_PREVIEW_DEVICE_PIXEL_RATIO = 2;
|
||||
|
||||
export function slimFavoriteGifEntry(entry: FavoriteGifEntry): FavoriteGifEntry {
|
||||
const best = pickBestPreviewFormat(entry.media, 'any', {
|
||||
cssWidth: PREVIEW_TILE_CSS_WIDTH,
|
||||
devicePixelRatio: STORED_PREVIEW_DEVICE_PIXEL_RATIO,
|
||||
});
|
||||
if (best == null) {
|
||||
return Object.keys(entry.media).length === 0 ? entry : {...entry, media: {}};
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
proxy_url: best.format.proxy_src,
|
||||
width: best.format.width,
|
||||
height: best.format.height,
|
||||
content_type: inferFormatContentType(best.key),
|
||||
media: {},
|
||||
};
|
||||
}
|
||||
|
||||
export {inferFormatContentType};
|
||||
@@ -3,7 +3,6 @@
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import styles from '@app/features/channel/components/GifPicker.module.css';
|
||||
import {safePause, safePlay, useGifVideoPool} from '@app/features/channel/components/GifVideoPool';
|
||||
import {FavoriteGifFirstTimePromptModal} from '@app/features/channel/components/pickers/gif/FavoriteGifFirstTimePromptModal';
|
||||
import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes';
|
||||
import {PickerThumbnail} from '@app/features/channel/components/pickers/shared/PickerThumbnail';
|
||||
import {usePooledVideo} from '@app/features/channel/components/pickers/shared/usePooledVideo';
|
||||
@@ -24,7 +23,6 @@ import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||
import {decodeThumbHashDataURL} from '@app/features/messaging/utils/ThumbHashUtils';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {modal, push} from '@app/features/ui/commands/ModalCommands';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
@@ -400,17 +398,6 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isFavoritePending) return;
|
||||
if (!FavoriteGif.hasSeenFavoriteGifFirstTimePrompt && !isFavorited) {
|
||||
push(
|
||||
modal(() => (
|
||||
<FavoriteGifFirstTimePromptModal
|
||||
onConfirm={() => void performFavoriteToggle()}
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.handle-favorite-click.favorite-gif-first-time-prompt-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
void performFavoriteToggle();
|
||||
};
|
||||
const favoriteTooltipText = (() => {
|
||||
|
||||
Reference in new issue
Block a user