diff --git a/.changeset/tweak_emoji_board_for_speed.md b/.changeset/tweak_emoji_board_for_speed.md new file mode 100644 index 000000000..e11258773 --- /dev/null +++ b/.changeset/tweak_emoji_board_for_speed.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +tweak emoji board for speed optimization (opt-in because of computational load increase on homeserver for thubmnail generation) diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 83804b968..4585d7261 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -30,6 +30,8 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { ImagePack, ImageUsage, PackImageReader } from '$plugins/custom-emoji'; import { getEmoticonSearchStr } from '$plugins/utils'; import { VirtualTile } from '$components/virtualizer'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; import { useEmojiGroupIcons } from './useEmojiGroupIcons'; import { useEmojiGroupLabels } from './useEmojiGroupLabels'; import { @@ -133,7 +135,7 @@ const useGroups = ( return [emojiGroupItems, stickerGroupItems]; }; -const useItemRenderer = (tab: EmojiBoardTab) => { +const useItemRenderer = (tab: EmojiBoardTab, saveStickerEmojiBandwidth: boolean) => { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -148,6 +150,7 @@ const useItemRenderer = (tab: EmojiBoardTab) => { mx={mx} useAuthentication={useAuthentication} image={emoji} + saveStickerEmojiBandwidth={saveStickerEmojiBandwidth} /> ); } @@ -157,6 +160,7 @@ const useItemRenderer = (tab: EmojiBoardTab) => { mx={mx} useAuthentication={useAuthentication} image={emoji} + saveStickerEmojiBandwidth={saveStickerEmojiBandwidth} /> ); }; @@ -167,9 +171,15 @@ const useItemRenderer = (tab: EmojiBoardTab) => { type EmojiSidebarProps = { activeGroupAtom: PrimitiveAtom; packs: ImagePack[]; + saveStickerEmojiBandwidth: boolean; onScrollToGroup: (groupId: string) => void; }; -function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarProps) { +function EmojiSidebar({ + activeGroupAtom, + packs, + saveStickerEmojiBandwidth, + onScrollToGroup, +}: EmojiSidebarProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -201,8 +211,11 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP let label = pack.meta.name; if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; - const url = - mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined; + // limit width and height to 36 to prevent very large icons from breaking the layout, since custom emoji pack icons can be of any size + // trying to get close to the render target size of the icons in the sidebar, which is around 24px + const url = saveStickerEmojiBandwidth + ? mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication, 36, 36) + : mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication); return ( ); @@ -243,9 +256,15 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP type StickerSidebarProps = { activeGroupAtom: PrimitiveAtom; packs: ImagePack[]; + saveStickerEmojiBandwidth: boolean; onScrollToGroup: (groupId: string) => void; }; -function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSidebarProps) { +function StickerSidebar({ + activeGroupAtom, + packs, + saveStickerEmojiBandwidth, + onScrollToGroup, +}: StickerSidebarProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -264,8 +283,11 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide let label = pack.meta.name; if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; - const url = - mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined; + // limit width and height to 36 to prevent very large icons from breaking the layout, since custom emoji pack icons can be of any size + // trying to get close to the render target size of the icons in the sidebar, which is around 24px + const url = saveStickerEmojiBandwidth + ? mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication, 36, 36) + : mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication); return ( ); @@ -377,6 +399,7 @@ export function EmojiBoard({ addToRecentEmoji = true, }: EmojiBoardProps) { const mx = useMatrixClient(); + const [saveStickerEmojiBandwidth] = useSetting(settingsAtom, 'saveStickerEmojiBandwidth'); const emojiTab = tab === EmojiBoardTab.Emoji; const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; @@ -390,7 +413,7 @@ export function EmojiBoard({ const imagePacks = useRelevantImagePacks(usage, imagePackRooms); const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks); const groups = emojiTab ? emojiGroupItems : stickerGroupItems; - const renderItem = useItemRenderer(tab); + const renderItem = useItemRenderer(tab, saveStickerEmojiBandwidth); const searchList = useMemo(() => { let list: Array = []; @@ -424,7 +447,28 @@ export function EmojiBoard({ const virtualizer = useVirtualizer({ count: groups.length, getScrollElement: () => contentScrollRef.current, - estimateSize: () => 40, + estimateSize: (index: number) => { + const group = groups[index]; + if (!group) return emojiTab ? 320 : 420; + + /** + * estimate tile size: stickers are generally larger than emojis, and custom emojis can vary in size but are often larger than standard emojis, so we use a larger estimate for them. + * This is a rough estimate to help the virtualizer calculate the total height and which items are in view. + * The actual rendered size may vary, but this should provide a reasonable approximation for most cases. + */ + const tile = emojiTab ? 48 : 112; + /** + * estimate number of columns that can fit in the view, with a min of 1 to avoid division by zero + */ + const cols = Math.max(1, Math.floor(280 / tile)); + /** + * estimate number of rows based on the number of items and columns + */ + const rows = Math.ceil(group.items.length / cols); + + // calculate total height based on rows, with some padding and a safety margin + return Math.ceil((28 + 24 + rows * tile) * 1.05); // small safety margin + }, overscan: VIRTUAL_OVER_SCAN, }); const vItems = virtualizer.getVirtualItems(); @@ -520,12 +564,14 @@ export function EmojiBoard({ ) : ( ) diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx index 091793571..c593b5db0 100644 --- a/src/app/components/emoji-board/components/Item.tsx +++ b/src/app/components/emoji-board/components/Item.tsx @@ -6,6 +6,31 @@ import { mxcUrlToHttp } from '$utils/matrix'; import { EmojiItemInfo, EmojiType } from '$components/emoji-board/types'; import * as css from './styles.css'; +const ANIMATED_MIME_TYPES = new Set(['image/gif', 'image/apng']); + +const isAnimatedPackImage = (image: PackImageReader): boolean => { + const mimetype = image.info?.mimetype?.toLowerCase(); + if (mimetype && ANIMATED_MIME_TYPES.has(mimetype)) return true; + + const body = image.body?.toLowerCase(); + return !!body && (body.endsWith('.gif') || body.endsWith('.webp') || body.endsWith('.apng')); +}; + +const getPackImageSrc = ( + mx: MatrixClient, + image: PackImageReader, + useAuthentication: boolean | undefined, + saveStickerEmojiBandwidth: boolean, + width: number, + height: number +): string => { + const preserveAnimation = isAnimatedPackImage(image); + + return preserveAnimation || !saveStickerEmojiBandwidth + ? (mxcUrlToHttp(mx, image.url, useAuthentication) ?? '') + : (mxcUrlToHttp(mx, image.url, useAuthentication, width, height) ?? ''); +}; + export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { const label = element.getAttribute('title'); const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; @@ -48,8 +73,14 @@ type CustomEmojiItemProps = { mx: MatrixClient; useAuthentication?: boolean; image: PackImageReader; + saveStickerEmojiBandwidth: boolean; }; -export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) { +export function CustomEmojiItem({ + mx, + useAuthentication, + image, + saveStickerEmojiBandwidth, +}: CustomEmojiItemProps) { return ( ); @@ -77,9 +108,15 @@ type StickerItemProps = { mx: MatrixClient; useAuthentication?: boolean; image: PackImageReader; + saveStickerEmojiBandwidth: boolean; }; -export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) { +export function StickerItem({ + mx, + useAuthentication, + image, + saveStickerEmojiBandwidth, +}: StickerItemProps) { return ( ); diff --git a/src/app/features/settings/experimental/BandwithSavingEmojis.tsx b/src/app/features/settings/experimental/BandwithSavingEmojis.tsx new file mode 100644 index 000000000..7e660fba0 --- /dev/null +++ b/src/app/features/settings/experimental/BandwithSavingEmojis.tsx @@ -0,0 +1,33 @@ +import { SequenceCard } from '$components/sequence-card'; +import { SettingTile } from '$components/setting-tile'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { Box, Switch, Text } from 'folds'; +import { SequenceCardStyle } from '../styles.css'; + +export function BandwidthSavingEmojis() { + const [useBandwidthSaving, setUseBandwidthSaving] = useSetting( + settingsAtom, + 'saveStickerEmojiBandwidth' + ); + + return ( + + Save Bandwidth for Sticker and Emoji Images + + + } + /> + + + ); +} diff --git a/src/app/features/settings/experimental/Experimental.tsx b/src/app/features/settings/experimental/Experimental.tsx index d8375271c..1760bc9c4 100644 --- a/src/app/features/settings/experimental/Experimental.tsx +++ b/src/app/features/settings/experimental/Experimental.tsx @@ -3,6 +3,7 @@ import { Page, PageContent, PageHeader } from '$components/page'; import { InfoCard } from '$components/info-card'; import { LanguageSpecificPronouns } from '../cosmetics/LanguageSpecificPronouns'; import { Sync } from '../general'; +import { BandwidthSavingEmojis } from './BandwithSavingEmojis'; type ExperimentalProps = { requestClose: () => void; @@ -43,6 +44,7 @@ export function Experimental({ requestClose }: ExperimentalProps) { + diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 96cae3872..a5e373a1e 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -94,6 +94,7 @@ export interface Settings { autoplayGifs: boolean; autoplayStickers: boolean; autoplayEmojis: boolean; + saveStickerEmojiBandwidth: boolean; // furry stuff renderAnimals: boolean; @@ -171,6 +172,7 @@ const defaultSettings: Settings = { autoplayGifs: true, autoplayStickers: true, autoplayEmojis: true, + saveStickerEmojiBandwidth: false, // furry stuff renderAnimals: true,