diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 93ae9585..3be006d0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,7 +32,6 @@ "@types/mime-types": "^2.1.4", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@types/react-window": "^1.8.8", "@types/unzipper": "^0.10.10", "autoprefixer": "^10.4.20", "electron": "^34.0.0", @@ -61,6 +60,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-visually-hidden": "^1.1.0", "@tanstack/react-query": "^5.64.2", + "@tanstack/react-virtual": "^3.11.2", "@tiptap/core": "^2.11.2", "@tiptap/extension-blockquote": "^2.11.2", "@tiptap/extension-bold": "^2.11.2", @@ -103,8 +103,6 @@ "react-hook-form": "^7.53.2", "react-intersection-observer": "^9.15.0", "react-router-dom": "^7.1.3", - "react-virtualized-auto-sizer": "^1.0.24", - "react-window": "^1.8.10", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.1", diff --git a/apps/desktop/src/renderer/components/avatars/avatar-upload.tsx b/apps/desktop/src/renderer/components/avatars/avatar-upload.tsx index f8721f5f..7e9b3b88 100644 --- a/apps/desktop/src/renderer/components/avatars/avatar-upload.tsx +++ b/apps/desktop/src/renderer/components/avatars/avatar-upload.tsx @@ -22,7 +22,7 @@ export const AvatarUpload = ({ onUpload }: AvatarUploadProps) => { }; return ( -
+
{ diff --git a/apps/desktop/src/renderer/components/emojis/emoji-browser-category.tsx b/apps/desktop/src/renderer/components/emojis/emoji-browser-category.tsx index f4846863..b56d55fd 100644 --- a/apps/desktop/src/renderer/components/emojis/emoji-browser-category.tsx +++ b/apps/desktop/src/renderer/components/emojis/emoji-browser-category.tsx @@ -1,7 +1,7 @@ -import { EmojiPickerLabelRow } from '@/shared/types/emojis'; +import { EmojiPickerCategoryRow } from '@/shared/types/emojis'; interface EmojiBrowserCategoryProps { - row: EmojiPickerLabelRow; + row: EmojiPickerCategoryRow; style: React.CSSProperties; } diff --git a/apps/desktop/src/renderer/components/emojis/emoji-browser-emojis.tsx b/apps/desktop/src/renderer/components/emojis/emoji-browser-items.tsx similarity index 71% rename from apps/desktop/src/renderer/components/emojis/emoji-browser-emojis.tsx rename to apps/desktop/src/renderer/components/emojis/emoji-browser-items.tsx index 67a7df25..e469dc3d 100644 --- a/apps/desktop/src/renderer/components/emojis/emoji-browser-emojis.tsx +++ b/apps/desktop/src/renderer/components/emojis/emoji-browser-items.tsx @@ -1,13 +1,13 @@ -import { EmojiPickerEmojiRow } from '@/shared/types/emojis'; +import { EmojiPickerItemsRow } from '@/shared/types/emojis'; import { useQuery } from '@/renderer/hooks/use-query'; import { EmojiPickerItem } from '@/renderer/components/emojis/emoji-picker-item'; -interface EmojiBrowserEmojisProps { - row: EmojiPickerEmojiRow; +interface EmojiBrowserItemsProps { + row: EmojiPickerItemsRow; style: React.CSSProperties; } -export const EmojiBrowserEmojis = ({ row, style }: EmojiBrowserEmojisProps) => { +export const EmojiBrowserItems = ({ row, style }: EmojiBrowserItemsProps) => { const { data } = useQuery({ type: 'emoji_list', category: row.category, diff --git a/apps/desktop/src/renderer/components/emojis/emoji-browser-row.tsx b/apps/desktop/src/renderer/components/emojis/emoji-browser-row.tsx deleted file mode 100644 index ad60a64b..00000000 --- a/apps/desktop/src/renderer/components/emojis/emoji-browser-row.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import { EmojiPickerRowData } from '@/shared/types/emojis'; -import { EmojiBrowserEmojis } from '@/renderer/components/emojis/emoji-browser-emojis'; -import { EmojiBrowserCategory } from '@/renderer/components/emojis/emoji-browser-category'; - -interface EmojiBrowserRowProps { - index: number; - style: React.CSSProperties; - data: { - rows: EmojiPickerRowData[]; - }; -} - -export const EmojiBrowserRow = ({ - index, - data, - style, -}: EmojiBrowserRowProps) => { - const rowData = data.rows[index]; - - if (!rowData) { - return null; - } - - if (rowData.type === 'label') { - return ; - } - - return ; -}; diff --git a/apps/desktop/src/renderer/components/emojis/emoji-browser.tsx b/apps/desktop/src/renderer/components/emojis/emoji-browser.tsx index 30478a72..3ca28795 100644 --- a/apps/desktop/src/renderer/components/emojis/emoji-browser.tsx +++ b/apps/desktop/src/renderer/components/emojis/emoji-browser.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList } from 'react-window'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { EmojiPickerRowData } from '@/shared/types/emojis'; -import { EmojiBrowserRow } from '@/renderer/components/emojis/emoji-browser-row'; import { useQuery } from '@/renderer/hooks/use-query'; +import { EmojiBrowserCategory } from '@/renderer/components/emojis/emoji-browser-category'; +import { EmojiBrowserItems } from '@/renderer/components/emojis/emoji-browser-items'; const EMOJIS_PER_ROW = 10; @@ -19,7 +19,7 @@ export const EmojiBrowser = () => { for (const category of categories) { rows.push({ - type: 'label', + type: 'category', category: category.name, }); @@ -28,7 +28,7 @@ export const EmojiBrowser = () => { for (let i = 0; i < numRowsInCategory; i++) { rows.push({ - type: 'emoji', + type: 'items', category: category.id, page: i, count: EMOJIS_PER_ROW, @@ -39,20 +39,47 @@ export const EmojiBrowser = () => { return rows; }, [categories]); + const parentRef = React.useRef(null); + + const virtualizer = useVirtualizer({ + count: rowDataArray.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 30, + }); + return ( - - {({ width, height }: { width: number; height: number }) => ( - - {EmojiBrowserRow} - - )} - +
+
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const row = rowDataArray[virtualItem.index]!; + const style: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: `${virtualItem.size}px`, + transform: `translateY(${virtualItem.start}px)`, + }; + + if (row.type === 'category') { + return ; + } + + return ; + })} +
+
); }; diff --git a/apps/desktop/src/renderer/components/icons/icon-browser-category.tsx b/apps/desktop/src/renderer/components/icons/icon-browser-category.tsx index b11ea25e..4c7a39d0 100644 --- a/apps/desktop/src/renderer/components/icons/icon-browser-category.tsx +++ b/apps/desktop/src/renderer/components/icons/icon-browser-category.tsx @@ -1,7 +1,7 @@ -import { IconPickerLabelRow } from '@/shared/types/icons'; +import { IconPickerCategoryRow } from '@/shared/types/icons'; interface IconBrowserCategoryProps { - row: IconPickerLabelRow; + row: IconPickerCategoryRow; style: React.CSSProperties; } diff --git a/apps/desktop/src/renderer/components/icons/icon-browser-icons.tsx b/apps/desktop/src/renderer/components/icons/icon-browser-items.tsx similarity index 73% rename from apps/desktop/src/renderer/components/icons/icon-browser-icons.tsx rename to apps/desktop/src/renderer/components/icons/icon-browser-items.tsx index 09ef865c..76612065 100644 --- a/apps/desktop/src/renderer/components/icons/icon-browser-icons.tsx +++ b/apps/desktop/src/renderer/components/icons/icon-browser-items.tsx @@ -1,13 +1,13 @@ -import { IconPickerIconRow } from '@/shared/types/icons'; +import { IconPickerItemsRow } from '@/shared/types/icons'; import { useQuery } from '@/renderer/hooks/use-query'; import { IconPickerItem } from '@/renderer/components/icons/icon-picker-item'; -interface IconBrowserIconsProps { - row: IconPickerIconRow; +interface IconBrowserItemsProps { + row: IconPickerItemsRow; style: React.CSSProperties; } -export const IconBrowserIcons = ({ row, style }: IconBrowserIconsProps) => { +export const IconBrowserItems = ({ row, style }: IconBrowserItemsProps) => { const { data } = useQuery({ type: 'icon_list', category: row.category, diff --git a/apps/desktop/src/renderer/components/icons/icon-browser-row.tsx b/apps/desktop/src/renderer/components/icons/icon-browser-row.tsx deleted file mode 100644 index 783f503a..00000000 --- a/apps/desktop/src/renderer/components/icons/icon-browser-row.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import { IconBrowserCategory } from '@/renderer/components/icons/icon-browser-category'; -import { IconBrowserIcons } from '@/renderer/components/icons/icon-browser-icons'; -import { IconPickerRowData } from '@/shared/types/icons'; - -interface IconPickerIconRowProps { - index: number; - style: React.CSSProperties; - data: { - rows: IconPickerRowData[]; - }; -} - -export const IconPickerIconRow = ({ - index, - style, - data, -}: IconPickerIconRowProps) => { - const rowData = data.rows[index]; - - if (!rowData) { - return null; - } - - if (rowData.type === 'label') { - return ; - } - - return ; -}; diff --git a/apps/desktop/src/renderer/components/icons/icon-browser.tsx b/apps/desktop/src/renderer/components/icons/icon-browser.tsx index 48fa131f..f506d91e 100644 --- a/apps/desktop/src/renderer/components/icons/icon-browser.tsx +++ b/apps/desktop/src/renderer/components/icons/icon-browser.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList } from 'react-window'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { IconPickerRowData } from '@/shared/types/icons'; -import { IconPickerIconRow } from '@/renderer/components/icons/icon-browser-row'; +import { IconBrowserItems } from '@/renderer/components/icons/icon-browser-items'; +import { IconBrowserCategory } from '@/renderer/components/icons/icon-browser-category'; import { useQuery } from '@/renderer/hooks/use-query'; const ICONS_PER_ROW = 10; @@ -19,7 +19,7 @@ export const IconBrowser = () => { for (const category of categories) { rows.push({ - type: 'label', + type: 'category', category: category.name, }); @@ -28,7 +28,7 @@ export const IconBrowser = () => { for (let i = 0; i < numRowsInCategory; i++) { rows.push({ - type: 'icon', + type: 'items', category: category.id, page: i, count: ICONS_PER_ROW, @@ -39,20 +39,47 @@ export const IconBrowser = () => { return rows; }, [categories]); + const parentRef = React.useRef(null); + + const virtualizer = useVirtualizer({ + count: rowDataArray.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 30, + }); + return ( - - {({ width, height }: { width: number; height: number }) => ( - - {IconPickerIconRow} - - )} - +
+
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const row = rowDataArray[virtualItem.index]!; + const style: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: `${virtualItem.size}px`, + transform: `translateY(${virtualItem.start}px)`, + }; + + if (row.type === 'category') { + return ; + } + + return ; + })} +
+
); }; diff --git a/apps/desktop/src/shared/types/emojis.ts b/apps/desktop/src/shared/types/emojis.ts index 3ab63b2e..57b38fde 100644 --- a/apps/desktop/src/shared/types/emojis.ts +++ b/apps/desktop/src/shared/types/emojis.ts @@ -20,15 +20,15 @@ export type EmojiSkin = { unified: string; }; -export type EmojiPickerRowData = EmojiPickerLabelRow | EmojiPickerEmojiRow; +export type EmojiPickerRowData = EmojiPickerCategoryRow | EmojiPickerItemsRow; -export type EmojiPickerLabelRow = { - type: 'label'; +export type EmojiPickerCategoryRow = { + type: 'category'; category: string; }; -export type EmojiPickerEmojiRow = { - type: 'emoji'; +export type EmojiPickerItemsRow = { + type: 'items'; category: string; page: number; count: number; diff --git a/apps/desktop/src/shared/types/icons.ts b/apps/desktop/src/shared/types/icons.ts index 4b600158..a0ff0dad 100644 --- a/apps/desktop/src/shared/types/icons.ts +++ b/apps/desktop/src/shared/types/icons.ts @@ -13,15 +13,15 @@ export type IconCategory = { display_order: number; }; -export type IconPickerRowData = IconPickerLabelRow | IconPickerIconRow; +export type IconPickerRowData = IconPickerCategoryRow | IconPickerItemsRow; -export type IconPickerLabelRow = { - type: 'label'; +export type IconPickerCategoryRow = { + type: 'category'; category: string; }; -export type IconPickerIconRow = { - type: 'icon'; +export type IconPickerItemsRow = { + type: 'items'; category: string; page: number; count: number; diff --git a/package-lock.json b/package-lock.json index 3431803f..9c104b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-visually-hidden": "^1.1.0", "@tanstack/react-query": "^5.64.2", + "@tanstack/react-virtual": "^3.11.2", "@tiptap/core": "^2.11.2", "@tiptap/extension-blockquote": "^2.11.2", "@tiptap/extension-bold": "^2.11.2", @@ -100,8 +101,6 @@ "react-hook-form": "^7.53.2", "react-intersection-observer": "^9.15.0", "react-router-dom": "^7.1.3", - "react-virtualized-auto-sizer": "^1.0.24", - "react-window": "^1.8.10", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.1", @@ -127,7 +126,6 @@ "@types/mime-types": "^2.1.4", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@types/react-window": "^1.8.8", "@types/unzipper": "^0.10.10", "autoprefixer": "^10.4.20", "electron": "^34.0.0", @@ -6323,6 +6321,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz", + "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.11.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", + "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tiptap/core": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.2.tgz", @@ -7145,16 +7170,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-window": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", - "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -14378,12 +14393,6 @@ "node": ">=6" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -16967,33 +16976,6 @@ } } }, - "node_modules/react-virtualized-auto-sizer": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.25.tgz", - "integrity": "sha512-YHsksEGDfsHbHuaBVDYwJmcktblcHGafz4ZVuYPQYuSHMUGjpwmUCrAOcvMSGMwwk1eFWj1M/1GwYpNPuyhaBg==", - "license": "MIT", - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-window": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", - "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "memoize-one": ">=3.1.1 <6" - }, - "engines": { - "node": ">8.0.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",