Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tweak_emoji_board_for_speed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

tweak emoji board for speed optimization (opt-in because of computational load increase on homeserver for thubmnail generation)
68 changes: 57 additions & 11 deletions src/app/components/emoji-board/EmojiBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

Expand All @@ -148,6 +150,7 @@ const useItemRenderer = (tab: EmojiBoardTab) => {
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
saveStickerEmojiBandwidth={saveStickerEmojiBandwidth}
/>
);
}
Expand All @@ -157,6 +160,7 @@ const useItemRenderer = (tab: EmojiBoardTab) => {
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
saveStickerEmojiBandwidth={saveStickerEmojiBandwidth}
/>
);
};
Expand All @@ -167,9 +171,15 @@ const useItemRenderer = (tab: EmojiBoardTab) => {
type EmojiSidebarProps = {
activeGroupAtom: PrimitiveAtom<string | undefined>;
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();

Expand Down Expand Up @@ -201,16 +211,19 @@ 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 (
<ImageGroupIcon
key={pack.id}
active={activeGroupId === pack.id}
id={pack.id}
label={label ?? 'Unknown Pack'}
url={url}
url={url ?? undefined}
onClick={handleScrollToGroup}
/>
);
Expand Down Expand Up @@ -243,9 +256,15 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
type StickerSidebarProps = {
activeGroupAtom: PrimitiveAtom<string | undefined>;
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();

Expand All @@ -264,16 +283,19 @@ 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 (
<ImageGroupIcon
key={pack.id}
active={activeGroupId === pack.id}
id={pack.id}
label={label ?? 'Unknown Pack'}
url={url}
url={url ?? undefined}
onClick={handleScrollToGroup}
/>
);
Expand Down Expand Up @@ -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;
Expand All @@ -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<PackImageReader | IEmoji> = [];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -520,12 +564,14 @@ export function EmojiBoard({
<EmojiSidebar
activeGroupAtom={activeGroupIdAtom}
packs={imagePacks}
saveStickerEmojiBandwidth={saveStickerEmojiBandwidth}
onScrollToGroup={handleScrollToGroup}
/>
) : (
<StickerSidebar
activeGroupAtom={activeGroupIdAtom}
packs={imagePacks}
saveStickerEmojiBandwidth={saveStickerEmojiBandwidth}
onScrollToGroup={handleScrollToGroup}
/>
)
Expand Down
45 changes: 41 additions & 4 deletions src/app/components/emoji-board/components/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<Box
as="button"
Expand All @@ -67,7 +98,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
loading="lazy"
className={css.CustomEmojiImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
src={getPackImageSrc(mx, image, useAuthentication, saveStickerEmojiBandwidth, 32, 32)}
/>
</Box>
);
Expand All @@ -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 (
<Box
as="button"
Expand All @@ -97,7 +134,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
src={getPackImageSrc(mx, image, useAuthentication, saveStickerEmojiBandwidth, 125, 125)}
/>
</Box>
);
Expand Down
33 changes: 33 additions & 0 deletions src/app/features/settings/experimental/BandwithSavingEmojis.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box direction="Column" gap="100">
<Text size="L400">Save Bandwidth for Sticker and Emoji Images</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="100"
>
<SettingTile
title="Enable bandwidth saving for stickers and emojis"
description="If enabled, sticker and emoji images will be optimized to save bandwidth. This helps reduce data usage when viewing these images. But will increase server computation load."
after={
<Switch variant="Primary" value={useBandwidthSaving} onChange={setUseBandwidthSaving} />
}
/>
</SequenceCard>
</Box>
);
}
2 changes: 2 additions & 0 deletions src/app/features/settings/experimental/Experimental.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,6 +44,7 @@ export function Experimental({ requestClose }: ExperimentalProps) {
<Box direction="Column" gap="700">
<Sync />
<LanguageSpecificPronouns />
<BandwidthSavingEmojis />
</Box>
</PageContent>
</Scroll>
Expand Down
2 changes: 2 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface Settings {
autoplayGifs: boolean;
autoplayStickers: boolean;
autoplayEmojis: boolean;
saveStickerEmojiBandwidth: boolean;

// furry stuff
renderAnimals: boolean;
Expand Down Expand Up @@ -171,6 +172,7 @@ const defaultSettings: Settings = {
autoplayGifs: true,
autoplayStickers: true,
autoplayEmojis: true,
saveStickerEmojiBandwidth: false,

// furry stuff
renderAnimals: true,
Expand Down
Loading