@@ -3,16 +3,17 @@ import { includes } from "lodash-es";
33import { PaperclipIcon , SearchIcon } from "lucide-react" ;
44import { observer } from "mobx-react-lite" ;
55import { useEffect , useState } from "react" ;
6+ import { toast } from "react-hot-toast" ;
67import AttachmentIcon from "@/components/AttachmentIcon" ;
78import Empty from "@/components/Empty" ;
89import MobileHeader from "@/components/MobileHeader" ;
10+ import { Button } from "@/components/ui/button" ;
911import { Input } from "@/components/ui/input" ;
1012import { Separator } from "@/components/ui/separator" ;
1113import { attachmentServiceClient } from "@/grpcweb" ;
1214import useLoading from "@/hooks/useLoading" ;
1315import useResponsiveWidth from "@/hooks/useResponsiveWidth" ;
1416import i18n from "@/i18n" ;
15- import { memoStore } from "@/store" ;
1617import { Attachment } from "@/types/proto/api/v1/attachment_service" ;
1718import { useTranslate } from "@/utils/i18n" ;
1819
@@ -42,18 +43,51 @@ const Attachments = observer(() => {
4243 searchQuery : "" ,
4344 } ) ;
4445 const [ attachments , setAttachments ] = useState < Attachment [ ] > ( [ ] ) ;
46+ const [ nextPageToken , setNextPageToken ] = useState ( "" ) ;
47+ const [ isLoadingMore , setIsLoadingMore ] = useState ( false ) ;
4548 const filteredAttachments = attachments . filter ( ( attachment ) => includes ( attachment . filename , state . searchQuery ) ) ;
4649 const groupedAttachments = groupAttachmentsByDate ( filteredAttachments . filter ( ( attachment ) => attachment . memo ) ) ;
4750 const unusedAttachments = filteredAttachments . filter ( ( attachment ) => ! attachment . memo ) ;
4851
4952 useEffect ( ( ) => {
50- attachmentServiceClient . listAttachments ( { } ) . then ( ( { attachments } ) => {
51- setAttachments ( attachments ) ;
52- loadingState . setFinish ( ) ;
53- Promise . all ( attachments . map ( ( attachment ) => ( attachment . memo ? memoStore . getOrFetchMemoByName ( attachment . memo ) : null ) ) ) ;
54- } ) ;
53+ const fetchInitialAttachments = async ( ) => {
54+ try {
55+ const { attachments : fetchedAttachments , nextPageToken } = await attachmentServiceClient . listAttachments ( {
56+ pageSize : 50 ,
57+ } ) ;
58+ setAttachments ( fetchedAttachments ) ;
59+ setNextPageToken ( nextPageToken ?? "" ) ;
60+ } catch ( error ) {
61+ console . error ( "Failed to fetch attachments:" , error ) ;
62+ toast . error ( "Failed to load attachments. Please try again." ) ;
63+ } finally {
64+ loadingState . setFinish ( ) ;
65+ }
66+ } ;
67+
68+ fetchInitialAttachments ( ) ;
5569 } , [ ] ) ;
5670
71+ const handleLoadMore = async ( ) => {
72+ if ( ! nextPageToken || isLoadingMore ) {
73+ return ;
74+ }
75+ setIsLoadingMore ( true ) ;
76+ try {
77+ const { attachments : fetchedAttachments , nextPageToken : newPageToken } = await attachmentServiceClient . listAttachments ( {
78+ pageSize : 50 ,
79+ pageToken : nextPageToken ,
80+ } ) ;
81+ setAttachments ( ( prevAttachments ) => [ ...prevAttachments , ...fetchedAttachments ] ) ;
82+ setNextPageToken ( newPageToken ?? "" ) ;
83+ } catch ( error ) {
84+ console . error ( "Failed to load more attachments:" , error ) ;
85+ toast . error ( "Failed to load more attachments. Please try again." ) ;
86+ } finally {
87+ setIsLoadingMore ( false ) ;
88+ }
89+ } ;
90+
5791 return (
5892 < section className = "@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8" >
5993 { ! md && < MobileHeader /> }
@@ -89,61 +123,70 @@ const Attachments = observer(() => {
89123 < p className = "mt-4 text-muted-foreground" > { t ( "message.no-data" ) } </ p >
90124 </ div >
91125 ) : (
92- < div className = { "w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8" } >
93- { Array . from ( groupedAttachments . entries ( ) ) . map ( ( [ monthStr , attachments ] ) => {
94- return (
95- < div key = { monthStr } className = "w-full flex flex-row justify-start items-start" >
96- < div className = "w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start" >
97- < span className = "text-sm opacity-60" > { dayjs ( monthStr ) . year ( ) } </ span >
98- < span className = "font-medium text-xl" >
99- { dayjs ( monthStr ) . toDate ( ) . toLocaleString ( i18n . language , { month : "short" } ) }
100- </ span >
101- </ div >
102- < div className = "w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap" >
103- { attachments . map ( ( attachment ) => {
104- return (
105- < div key = { attachment . name } className = "w-24 sm:w-32 h-auto flex flex-col justify-start items-start" >
106- < div className = "w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80" >
107- < AttachmentIcon attachment = { attachment } strokeWidth = { 0.5 } />
108- </ div >
109- < div className = "w-full max-w-full flex flex-row justify-between items-center mt-1 px-1" >
110- < p className = "text-xs shrink text-muted-foreground truncate" > { attachment . filename } </ p >
126+ < >
127+ < div className = { "w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8" } >
128+ { Array . from ( groupedAttachments . entries ( ) ) . map ( ( [ monthStr , attachments ] ) => {
129+ return (
130+ < div key = { monthStr } className = "w-full flex flex-row justify-start items-start" >
131+ < div className = "w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start" >
132+ < span className = "text-sm opacity-60" > { dayjs ( monthStr ) . year ( ) } </ span >
133+ < span className = "font-medium text-xl" >
134+ { dayjs ( monthStr ) . toDate ( ) . toLocaleString ( i18n . language , { month : "short" } ) }
135+ </ span >
136+ </ div >
137+ < div className = "w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap" >
138+ { attachments . map ( ( attachment ) => {
139+ return (
140+ < div key = { attachment . name } className = "w-24 sm:w-32 h-auto flex flex-col justify-start items-start" >
141+ < div className = "w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80" >
142+ < AttachmentIcon attachment = { attachment } strokeWidth = { 0.5 } />
143+ </ div >
144+ < div className = "w-full max-w-full flex flex-row justify-between items-center mt-1 px-1" >
145+ < p className = "text-xs shrink text-muted-foreground truncate" > { attachment . filename } </ p >
146+ </ div >
111147 </ div >
112- </ div >
113- ) ;
114- } ) }
148+ ) ;
149+ } ) }
150+ </ div >
115151 </ div >
116- </ div >
117- ) ;
118- } ) }
152+ ) ;
153+ } ) }
119154
120- { unusedAttachments . length > 0 && (
121- < >
122- < Separator />
123- < div className = "w-full flex flex-row justify-start items-start" >
124- < div className = "w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start" > </ div >
125- < div className = "w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap" >
126- < div className = "w-full flex flex-row justify-start items-center gap-2" >
127- < span className = "text-muted-foreground" > { t ( "resource.unused-resources" ) } </ span >
128- < span className = "text-muted-foreground opacity-80" > ({ unusedAttachments . length } )</ span >
129- </ div >
130- { unusedAttachments . map ( ( attachment ) => {
131- return (
132- < div key = { attachment . name } className = "w-24 sm:w-32 h-auto flex flex-col justify-start items-start" >
133- < div className = "w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80" >
134- < AttachmentIcon attachment = { attachment } strokeWidth = { 0.5 } />
155+ { unusedAttachments . length > 0 && (
156+ < >
157+ < Separator />
158+ < div className = "w-full flex flex-row justify-start items-start" >
159+ < div className = "w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start" > </ div >
160+ < div className = "w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap" >
161+ < div className = "w-full flex flex-row justify-start items-center gap-2" >
162+ < span className = "text-muted-foreground" > { t ( "resource.unused-resources" ) } </ span >
163+ < span className = "text-muted-foreground opacity-80" > ({ unusedAttachments . length } )</ span >
164+ </ div >
165+ { unusedAttachments . map ( ( attachment ) => {
166+ return (
167+ < div key = { attachment . name } className = "w-24 sm:w-32 h-auto flex flex-col justify-start items-start" >
168+ < div className = "w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80" >
169+ < AttachmentIcon attachment = { attachment } strokeWidth = { 0.5 } />
170+ </ div >
171+ < div className = "w-full max-w-full flex flex-row justify-between items-center mt-1 px-1" >
172+ < p className = "text-xs shrink text-muted-foreground truncate" > { attachment . filename } </ p >
173+ </ div >
135174 </ div >
136- < div className = "w-full max-w-full flex flex-row justify-between items-center mt-1 px-1" >
137- < p className = "text-xs shrink text-muted-foreground truncate" > { attachment . filename } </ p >
138- </ div >
139- </ div >
140- ) ;
141- } ) }
175+ ) ;
176+ } ) }
177+ </ div >
142178 </ div >
143- </ div >
144- </ >
179+ </ >
180+ ) }
181+ </ div >
182+ { nextPageToken && (
183+ < div className = "w-full flex flex-row justify-center items-center mt-4" >
184+ < Button variant = "outline" size = "sm" onClick = { handleLoadMore } disabled = { isLoadingMore } >
185+ { isLoadingMore ? t ( "resource.fetching-data" ) : t ( "memo.load-more" ) }
186+ </ Button >
187+ </ div >
145188 ) }
146- </ div >
189+ </ >
147190 ) }
148191 </ >
149192 ) }
0 commit comments