Pārlūkot izejas kodu

refac: emoji picker optimisation

Timothy Jaeryang Baek 4 mēneši atpakaļ
vecāks
revīzija
6ecee8b920

+ 7 - 0
package-lock.json

@@ -16,6 +16,7 @@
 				"@mediapipe/tasks-vision": "^0.10.17",
 				"@pyscript/core": "^0.4.32",
 				"@sveltejs/adapter-node": "^2.0.0",
+				"@sveltejs/svelte-virtual-list": "^3.0.1",
 				"@tiptap/core": "^2.10.0",
 				"@tiptap/extension-code-block-lowlight": "^2.10.0",
 				"@tiptap/extension-highlight": "^2.10.0",
@@ -2291,6 +2292,12 @@
 				"vite": "^5.0.3 || ^6.0.0"
 			}
 		},
+		"node_modules/@sveltejs/svelte-virtual-list": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz",
+			"integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==",
+			"license": "LIL"
+		},
 		"node_modules/@sveltejs/vite-plugin-svelte": {
 			"version": "3.1.1",
 			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz",

+ 1 - 0
package.json

@@ -57,6 +57,7 @@
 		"@mediapipe/tasks-vision": "^0.10.17",
 		"@pyscript/core": "^0.4.32",
 		"@sveltejs/adapter-node": "^2.0.0",
+		"@sveltejs/svelte-virtual-list": "^3.0.1",
 		"@tiptap/core": "^2.10.0",
 		"@tiptap/extension-code-block-lowlight": "^2.10.0",
 		"@tiptap/extension-highlight": "^2.10.0",

+ 128 - 108
src/lib/components/channel/Messages/Message/ReactionPicker.svelte

@@ -1,63 +1,97 @@
 <script lang="ts">
 	import { DropdownMenu } from 'bits-ui';
 	import { flyAndScale } from '$lib/utils/transitions';
-
 	import emojiGroups from '$lib/emoji-groups.json';
 	import emojiShortCodes from '$lib/emoji-shortcodes.json';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import VirtualList from '@sveltejs/svelte-virtual-list';
 
 	export let onClose = () => {};
 	export let onSubmit = (name) => {};
 	export let side = 'top';
 	export let align = 'start';
-
 	export let user = null;
-	let show = false;
 
-	let emojis = {};
+	let show = false;
+	let emojis = emojiShortCodes;
 	let search = '';
+	let flattenedEmojis = [];
+	let emojiRows = [];
 
-	$: if (search) {
-		emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
-			if (key.includes(search)) {
-				acc[key] = emojiShortCodes[key];
-			} else {
-				if (Array.isArray(emojiShortCodes[key])) {
-					const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search));
-					if (filtered.length) {
-						acc[key] = filtered;
-					}
+	// Reactive statement to filter the emojis based on search query
+	$: {
+		if (search) {
+			emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
+				if (key.includes(search)) {
+					acc[key] = emojiShortCodes[key];
 				} else {
-					if (emojiShortCodes[key].includes(search)) {
-						acc[key] = emojiShortCodes[key];
+					if (Array.isArray(emojiShortCodes[key])) {
+						const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search));
+						if (filtered.length) {
+							acc[key] = filtered;
+						}
+					} else {
+						if (emojiShortCodes[key].includes(search)) {
+							acc[key] = emojiShortCodes[key];
+						}
 					}
 				}
+				return acc;
+			}, {});
+		} else {
+			emojis = emojiShortCodes;
+		}
+	}
+	// Flatten emoji groups and group them into rows of 8 for virtual scrolling
+	$: {
+		flattenedEmojis = [];
+		Object.keys(emojiGroups).forEach((group) => {
+			const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji]);
+			if (groupEmojis.length > 0) {
+				flattenedEmojis.push({ type: 'group', label: group });
+				flattenedEmojis.push(
+					...groupEmojis.map((emoji) => ({
+						type: 'emoji',
+						name: emoji,
+						shortCodes:
+							typeof emojiShortCodes[emoji] === 'string'
+								? [emojiShortCodes[emoji]]
+								: emojiShortCodes[emoji]
+					}))
+				);
 			}
-
-			return acc;
-		}, {});
-	} else {
-		emojis = emojiShortCodes;
+		});
+		// Group emojis into rows of 6
+		emojiRows = [];
+		let currentRow = [];
+		flattenedEmojis.forEach((item) => {
+			if (item.type === 'emoji') {
+				currentRow.push(item);
+				if (currentRow.length === 7) {
+					emojiRows.push(currentRow);
+					currentRow = [];
+				}
+			} else if (item.type === 'group') {
+				if (currentRow.length > 0) {
+					emojiRows.push(currentRow); // Push the remaining row
+					currentRow = [];
+				}
+				emojiRows.push([item]); // Add the group label as a separate row
+			}
+		});
+		if (currentRow.length > 0) {
+			emojiRows.push(currentRow); // Push the final row
+		}
 	}
-
-	$: if (show) {
-		init();
-	} else {
-		destroy();
+	const ROW_HEIGHT = 48; // Approximate height for a row with multiple emojis
+	// Handle emoji selection
+	function selectEmoji(emoji) {
+		const selectedCode = emoji.shortCodes[0];
+		onSubmit(selectedCode);
+		show = false;
 	}
-
-	const init = () => {
-		emojis = emojiShortCodes;
-	};
-
-	const destroy = () => {
-		search = '';
-		emojis = {};
-	};
 </script>
 
-<!-- TODO: Rendering Optimisation, This works but it's slow af -->
-
 <DropdownMenu.Root
 	bind:open={show}
 	closeFocus={false}
@@ -72,75 +106,61 @@
 	<DropdownMenu.Trigger>
 		<slot />
 	</DropdownMenu.Trigger>
-
-	<slot name="content">
-		<DropdownMenu.Content
-			class="max-w-full  w-80  bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
-			sideOffset={8}
-			{side}
-			{align}
-			transition={flyAndScale}
-		>
-			<div class="mb-1 px-3 pt-2 pb-2">
-				<input
-					type="text"
-					class="w-full text-sm bg-transparent outline-none"
-					placeholder="Search all emojis"
-					bind:value={search}
-				/>
-			</div>
-			<div class=" w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
-				{#if show}
-					<div>
-						{#if Object.keys(emojis).length === 0}
-							<div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
-						{:else}
-							{#each Object.keys(emojiGroups) as group (group)}
-								{@const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji])}
-								{#if groupEmojis.length > 0}
-									<div class="flex flex-col">
-										<div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
-											{group}
-										</div>
-
-										<div class="flex mb-2 flex-wrap gap-1">
-											{#each groupEmojis as emoji (emoji)}
-												<Tooltip
-													content={(typeof emojiShortCodes[emoji] === 'string'
-														? [emojiShortCodes[emoji]]
-														: emojiShortCodes[emoji]
-													)
-														.map((code) => `:${code}:`)
-														.join(', ')}
-													placement="top"
-												>
-													<button
-														class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
-														on:click={() => {
-															typeof emojiShortCodes[emoji] === 'string'
-																? onSubmit(emojiShortCodes[emoji])
-																: onSubmit(emojiShortCodes[emoji][0]);
-
-															show = false;
-														}}
-													>
-														<img
-															src="/assets/emojis/{emoji.toLowerCase()}.svg"
-															alt={emoji}
-															class="size-5"
-															loading="lazy"
-														/>
-													</button>
-												</Tooltip>
-											{/each}
-										</div>
-									</div>
-								{/if}
-							{/each}
-						{/if}
-					</div>
-				{/if}
-			</div>
-		</DropdownMenu.Content>
-	</slot>
+	<DropdownMenu.Content
+		class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
+		sideOffset={8}
+		{side}
+		{align}
+		transition={flyAndScale}
+	>
+		<div class="mb-1 px-3 pt-2 pb-2">
+			<input
+				type="text"
+				class="w-full text-sm bg-transparent outline-none"
+				placeholder="Search all emojis"
+				bind:value={search}
+			/>
+		</div>
+		<!-- Virtualized Emoji List -->
+		<div class="w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
+			{#if emojiRows.length === 0}
+				<div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
+			{:else}
+				<div class="w-full flex ml-2">
+					<VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item>
+						<div class="w-full">
+							{#if item.length === 1 && item[0].type === 'group'}
+								<!-- Render group header -->
+								<div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
+									{item[0].label}
+								</div>
+							{:else}
+								<!-- Render emojis in a row -->
+								<div class="flex items-center gap-2 w-full">
+									{#each item as emojiItem}
+										<Tooltip
+											content={emojiItem.shortCodes.map((code) => `:${code}:`).join(', ')}
+											placement="top"
+										>
+											<button
+												class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
+												on:click={() => selectEmoji(emojiItem)}
+											>
+												<img
+													src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
+													alt={emojiItem.name}
+													class="size-5"
+													loading="lazy"
+												/>
+											</button>
+										</Tooltip>
+									{/each}
+								</div>
+							{/if}
+						</div>
+					</VirtualList>
+				</div>
+			{/if}
+		</div>
+	</DropdownMenu.Content>
 </DropdownMenu.Root>