From 8b89f9ab81d52cae2512b2a8fd2bd9f508fa8ce1 Mon Sep 17 00:00:00 2001 From: Sokhuong Date: Tue, 30 Jun 2026 09:08:28 +0700 Subject: [PATCH 1/4] feat: add infinite scroll --- package.json | 1 + pnpm-lock.yaml | 18 +++++++++++++++ .../clipboard}/components/clipboard-list.tsx | 20 +++++------------ .../clipboard/components/clipboard-tab.tsx | 2 +- .../infinite-scroll-sentinel-list-item.tsx | 22 +++++++++++++++++++ .../components/sortable-clipboard-item.tsx} | 2 +- 6 files changed, 48 insertions(+), 17 deletions(-) rename src/{ => features/clipboard}/components/clipboard-list.tsx (74%) create mode 100644 src/features/clipboard/components/infinite-scroll-sentinel-list-item.tsx rename src/{components/sortable-item.tsx => features/clipboard/components/sortable-clipboard-item.tsx} (80%) diff --git a/package.json b/package.json index e02c492..94ac941 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.75.0", + "react-intersection-observer": "^10.0.3", "shadcn": "^4.1.1", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fab4719..a607bcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: react-hook-form: specifier: ^7.75.0 version: 7.75.0(react@19.2.3) + react-intersection-observer: + specifier: ^10.0.3 + version: 10.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) shadcn: specifier: ^4.1.1 version: 4.1.1(@types/node@25.9.1)(typescript@5.8.3) @@ -4775,6 +4778,15 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-intersection-observer@10.0.3: + resolution: {integrity: sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -10058,6 +10070,12 @@ snapshots: dependencies: react: 19.2.3 + react-intersection-observer@10.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react-is@16.13.1: {} react-is@19.2.5: {} diff --git a/src/components/clipboard-list.tsx b/src/features/clipboard/components/clipboard-list.tsx similarity index 74% rename from src/components/clipboard-list.tsx rename to src/features/clipboard/components/clipboard-list.tsx index 479d4ee..dac0a06 100644 --- a/src/components/clipboard-list.tsx +++ b/src/features/clipboard/components/clipboard-list.tsx @@ -1,15 +1,15 @@ import { load } from '@tauri-apps/plugin-store'; import { useEffect } from 'react'; import { EmptyState } from '@/components/clipboard-empty-state'; -import { Button } from '@/components/ui/button'; +import { InfiniteScrollSentinelListItem } from '@/features/clipboard/components/infinite-scroll-sentinel-list-item'; +import { SortableClipboardItem } from '@/features/clipboard/components/sortable-clipboard-item'; import { useClipboards } from '@/features/clipboard/hooks/use-clipboards'; import { useClipboardSearchQueryStore } from '@/features/clipboard/stores/clipboard-search-query-store'; import { useClipboardPerPageLimitStore } from '@/features/clipboard/stores/use-clipboard-per-page-limit-store'; -import { SortableItem } from './sortable-item'; export const ClipboardList = () => { const { perPageLimit, setPerPageLimit } = useClipboardPerPageLimitStore(); - const { data, hasNextPage, fetchNextPage } = useClipboards(perPageLimit); + const { data, hasNextPage } = useClipboards(perPageLimit); useEffect(() => { const getPerPageLimitFromTauriStore = async () => { @@ -36,20 +36,10 @@ export const ClipboardList = () => { return ( ); }; diff --git a/src/features/clipboard/components/clipboard-tab.tsx b/src/features/clipboard/components/clipboard-tab.tsx index ec183d4..0e7eb8f 100644 --- a/src/features/clipboard/components/clipboard-tab.tsx +++ b/src/features/clipboard/components/clipboard-tab.tsx @@ -1,5 +1,5 @@ import { ClipboardItemSkeletonList } from '@/components/clipboard-item-skeleton'; -import { ClipboardList } from '@/components/clipboard-list'; +import { ClipboardList } from '@/features/clipboard/components/clipboard-list'; import { ClipboardTabHeader } from '@/features/clipboard/components/clipboard-tab-header'; import { useClipboardHistory } from '@/features/clipboard/hooks/use-clipboard-history'; import { useClipboardPerPageLimitStore } from '@/features/clipboard/stores/use-clipboard-per-page-limit-store'; diff --git a/src/features/clipboard/components/infinite-scroll-sentinel-list-item.tsx b/src/features/clipboard/components/infinite-scroll-sentinel-list-item.tsx new file mode 100644 index 0000000..6d909bd --- /dev/null +++ b/src/features/clipboard/components/infinite-scroll-sentinel-list-item.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useClipboards } from '@/features/clipboard/hooks/use-clipboards'; +import { useClipboardPerPageLimitStore } from '@/features/clipboard/stores/use-clipboard-per-page-limit-store'; + +export function InfiniteScrollSentinelListItem() { + const perPageLimit = useClipboardPerPageLimitStore((state) => state.perPageLimit); + const { hasNextPage, fetchNextPage, isFetchingNextPage } = useClipboards(perPageLimit); + const { ref, inView } = useInView(); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + return ( +
  • + Loading... +
  • + ); +} diff --git a/src/components/sortable-item.tsx b/src/features/clipboard/components/sortable-clipboard-item.tsx similarity index 80% rename from src/components/sortable-item.tsx rename to src/features/clipboard/components/sortable-clipboard-item.tsx index 5b122e9..6092563 100644 --- a/src/components/sortable-item.tsx +++ b/src/features/clipboard/components/sortable-clipboard-item.tsx @@ -3,7 +3,7 @@ import type { Clipboard } from '@/bindings'; import { ClipboardItemCard } from '@/features/clipboard/components/clipboard-item-card'; import { ClipboardItemContextProvider } from '@/features/clipboard/components/clipboard-item-context-provider'; -export const SortableItem = memo(function SortableItem({ item }: { item: Clipboard }) { +export const SortableClipboardItem = memo(function SortableItem({ item }: { item: Clipboard }) { return ( From dc3173738c4956de925776e9110320f698c8f570 Mon Sep 17 00:00:00 2001 From: Sokhuong Date: Tue, 30 Jun 2026 09:26:22 +0700 Subject: [PATCH 2/4] fix: handle image preview for Windows --- .../clipboard/components/clipboard-item-content.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/features/clipboard/components/clipboard-item-content.tsx b/src/features/clipboard/components/clipboard-item-content.tsx index 7d16bb5..d720eb1 100644 --- a/src/features/clipboard/components/clipboard-item-content.tsx +++ b/src/features/clipboard/components/clipboard-item-content.tsx @@ -1,7 +1,12 @@ +import { platform } from '@tauri-apps/plugin-os'; import { useClipboardItem } from '@/features/clipboard/hooks/use-clipboard-item'; export function ClipboardItemContent() { const item = useClipboardItem(); + const previewSrc = + platform() === 'windows' + ? `http://image.localhost/preview?id=${item.id}` + : `image://localhost/preview?id=${item.id}`; return ( <> @@ -15,11 +20,7 @@ export function ClipboardItemContent() { {!item.content && (
    - Preview of the clipboard content + Preview of the clipboard content
    )} From cb529aed58da386d805e7283eef44c2fe9ef19ea Mon Sep 17 00:00:00 2001 From: skuong <77433349+skuong@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:30:51 +0000 Subject: [PATCH 3/4] chore(release): 1.27.0 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00cc834..5cdad9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.27.0](https://github.com/skuong/mexboard/compare/v1.26.1...v1.27.0) (2026-06-30) + + +### Bug Fixes + +* handle image preview for Windows ([95ce989](https://github.com/skuong/mexboard/commit/95ce989f79f4b1019688fb17e825e2703dea834f)) + + +### Features + +* add infinite scroll ([31ce43e](https://github.com/skuong/mexboard/commit/31ce43e6f97477a9461f0b49f4969028e74a412b)) + ## [1.26.1](https://github.com/skuong/mexboard/compare/v1.26.0...v1.26.1) (2026-06-30) diff --git a/package.json b/package.json index 94ac941..d4e1ae2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mexboard", "private": false, - "version": "1.26.1", + "version": "1.27.0", "type": "module", "packageManager": "pnpm@11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6a4ee1e..44131ed 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3509,7 +3509,7 @@ dependencies = [ [[package]] name = "mexboard" -version = "1.26.1" +version = "1.27.0" dependencies = [ "apple-native-keyring-store", "arboard", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3567f5f..f5ddda0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mexboard" -version = "1.26.1" +version = "1.27.0" description = "Clipboard manager with symbols/emojis selection and customizable features" authors = ["Sokhuong"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f3e60e5..5673000 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Mexboard", - "version": "1.26.1", + "version": "1.27.0", "identifier": "com.mexboard", "build": { "beforeDevCommand": "pnpm dev", From 9832aa6bb83f4874db87cffb07df504bfb6d9eb5 Mon Sep 17 00:00:00 2001 From: Sokhuong Date: Tue, 30 Jun 2026 10:50:02 +0700 Subject: [PATCH 4/4] feat: add context menu for copy and delete item --- src/components/ui/context-menu.tsx | 251 ++++++++++++++++++ .../components/clipboard-item-card.tsx | 49 +++- 2 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 src/components/ui/context-menu.tsx diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..10de661 --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,251 @@ +import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'; +import { ChevronRightIcon } from 'lucide-react'; +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) { + return ; +} + +function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) { + return ; +} + +function ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) { + return ( + + ); +} + +function ContextMenuContent({ + className, + align = 'start', + alignOffset = 4, + side = 'right', + sideOffset = 0, + ...props +}: ContextMenuPrimitive.Popup.Props & + Pick) { + return ( + + + + + + ); +} + +function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) { + return ; +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: ContextMenuPrimitive.GroupLabel.Props & { + inset?: boolean; +}) { + return ( + + ); +} + +function ContextMenuItem({ + className, + inset, + variant = 'default', + ...props +}: ContextMenuPrimitive.Item.Props & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) { + return ; +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: ContextMenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function ContextMenuSubContent({ ...props }: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: ContextMenuPrimitive.CheckboxItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function ContextMenuRadioGroup({ ...props }: ContextMenuPrimitive.RadioGroup.Props) { + return ; +} + +function ContextMenuRadioItem({ + className, + children, + inset, + ...props +}: ContextMenuPrimitive.RadioItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function ContextMenuSeparator({ className, ...props }: ContextMenuPrimitive.Separator.Props) { + return ( + + ); +} + +function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +export { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuGroup, + ContextMenuItem, + ContextMenuLabel, + ContextMenuPortal, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +}; diff --git a/src/features/clipboard/components/clipboard-item-card.tsx b/src/features/clipboard/components/clipboard-item-card.tsx index 85f3c2a..827d8f4 100644 --- a/src/features/clipboard/components/clipboard-item-card.tsx +++ b/src/features/clipboard/components/clipboard-item-card.tsx @@ -1,10 +1,21 @@ +import { useQueryClient } from '@tanstack/react-query'; import { memo, useRef } from 'react'; import { commands } from '@/bindings'; import { Card, CardContent } from '@/components/ui/card'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuGroup, + ContextMenuItem, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; import { ClipboardItemContent } from '@/features/clipboard/components/clipboard-item-content'; +import { QUERY_KEY } from '@/features/clipboard/constant/query-key'; import { useClipboardItem } from '@/features/clipboard/hooks/use-clipboard-item'; export const ClipboardItemCard = memo(function ClipboardItem() { + const queryClient = useQueryClient(); + const cardRef = useRef(null); const item = useClipboardItem(); @@ -19,9 +30,41 @@ export const ClipboardItemCard = memo(function ClipboardItem() { commands.pasteClipboardToOtherApp(item.id); }} > - - - + + + + + + + + + + { + const { status } = await commands.writeClipboardItemToSystemClipboard(item.id); + if (status === 'ok') { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.CLIPBOARDS] }); + } + }} + className="cursor-pointer" + > + Copy + + + { + const { status } = await commands.deleteClipboardItem(item.id); + if (status === 'ok') { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.CLIPBOARDS] }); + } + }} + className="cursor-pointer" + > + Delete + + + + ); });