From 16c84abe67c1c3a18137bb40cc9393fcc4265794 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sun, 28 Sep 2025 07:39:40 +0800 Subject: [PATCH] refactor(list-messages): switch from page-based to cursor-based pagination - Changed `list-messages` API from page/page_size to cursor/next_page_token - Aligns pagination approach with Gmail API for easier integration - Frontend updates to handle cursor-based navigation --- protos/rustmailer.proto | 10 +- src/modules/cache/vendor/gmail/sync/client.rs | 4 +- src/modules/cache/vendor/gmail/sync/flow.rs | 8 +- src/modules/common/mod.rs | 27 +- src/modules/grpc/service/message/mod.rs | 4 +- src/modules/grpc/tests.rs | 2 +- src/modules/message/list.rs | 257 ++++++++++++++---- src/modules/message/search/payload.rs | 20 +- src/modules/rest/api/message.rs | 11 +- web/src/api/index.ts | 2 +- web/src/api/mailbox/envelope/api.ts | 34 ++- .../features/mailbox/components/mail-list.tsx | 6 +- web/src/features/mailbox/components/mail.tsx | 128 ++++++--- .../mailbox/components/pagination.tsx | 28 +- web/src/features/settings/license/index.tsx | 5 +- 15 files changed, 401 insertions(+), 145 deletions(-) diff --git a/protos/rustmailer.proto b/protos/rustmailer.proto index 3c05401..633cc5a 100644 --- a/protos/rustmailer.proto +++ b/protos/rustmailer.proto @@ -993,9 +993,11 @@ message ListMessagesRequest { uint64 account_id = 1; // The name of the mailbox to list messages from. string mailbox_name = 2; - // The requested page number (1-based). - uint64 page = 3; - // The number of messages to return per page. + // The token for fetching the next page of results in pagination. + // - If `None`, this indicates that the first page should be returned. + // - If `Some(token)`, the page corresponding to this token will be fetched. + optional string next_page_token = 3; + // The number of messages to return per page. max 500 uint64 page_size = 4; // If true, fetches messages directly from the remote server. bool remote = 5; @@ -1152,7 +1154,7 @@ service MessageService { // Updates flags on messages within a mailbox. rpc UpdateMessageFlags(FlagMessageRequest) returns (Empty); // Lists messages within a mailbox with pagination. - rpc ListMessages(ListMessagesRequest) returns (PagedMessages); + rpc ListMessages(ListMessagesRequest) returns (CursorDataPage); // Lists threads within a mailbox with pagination. rpc ListThreads(ListThreadsRequest) returns (PagedMessages); // Get thread's envelopes within a mailbox. diff --git a/src/modules/cache/vendor/gmail/sync/client.rs b/src/modules/cache/vendor/gmail/sync/client.rs index 9678162..bcf6d7c 100644 --- a/src/modules/cache/vendor/gmail/sync/client.rs +++ b/src/modules/cache/vendor/gmail/sync/client.rs @@ -202,9 +202,9 @@ impl GmailClient { account_id: u64, use_proxy: Option, label_id: &str, - page_token: Option, + page_token: Option<&str>, after: Option<&str>, - max_results: u32, + max_results: u64, ) -> RustMailerResult { let mut url = format!( "https://gmail.googleapis.com/gmail/v1/users/me/messages?labelIds={}&maxResults={}", diff --git a/src/modules/cache/vendor/gmail/sync/flow.rs b/src/modules/cache/vendor/gmail/sync/flow.rs index 5ca8984..36ae92d 100644 --- a/src/modules/cache/vendor/gmail/sync/flow.rs +++ b/src/modules/cache/vendor/gmail/sync/flow.rs @@ -43,9 +43,9 @@ pub async fn fetch_and_save_since_date( account_id, use_proxy, &label.label_id, - page_token, + page_token.as_deref(), Some(date), - ENVELOPE_BATCH_SIZE, + ENVELOPE_BATCH_SIZE as u64, ) .await?; // The total number of messages can only be retrieved via an API query @@ -156,9 +156,9 @@ pub async fn fetch_and_save_full_label( account_id, use_proxy, &label.label_id, - page_token, + page_token.as_deref(), None, - ENVELOPE_BATCH_SIZE, + ENVELOPE_BATCH_SIZE as u64, ) .await?; // Update page_token returned by Gmail API diff --git a/src/modules/common/mod.rs b/src/modules/common/mod.rs index fb65b53..f00042e 100644 --- a/src/modules/common/mod.rs +++ b/src/modules/common/mod.rs @@ -2,7 +2,9 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::base64_encode_url_safe; +use crate::base64_decode_url_safe; +use crate::modules::error::RustMailerResult; +use crate::raise_error; use super::error::code::ErrorCode; use super::error::RustMailerError; @@ -22,14 +24,14 @@ use tracing::error; pub mod auth; pub mod error; pub mod log; +pub mod lru; pub mod paginated; +pub mod parallel; pub mod rustls; pub mod signal; pub mod timeout; pub mod tls; pub mod validator; -pub mod lru; -pub mod parallel; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Object)] pub struct Addr { @@ -219,3 +221,22 @@ impl ResponseError for RustMailerError { } } } + +pub fn decode_page_token(next_page_token: Option<&str>) -> RustMailerResult { + match next_page_token { + Some(token) => { + let decoded = base64_decode_url_safe!(token) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()) + .and_then(|s| s.parse::().ok()); + + decoded.ok_or_else(|| { + raise_error!( + "Invalid next_page_token: not a valid page token".into(), + ErrorCode::InvalidParameter + ) + }) + } + None => Ok(1), + } +} diff --git a/src/modules/grpc/service/message/mod.rs b/src/modules/grpc/service/message/mod.rs index 8d6e3fa..62cb05c 100644 --- a/src/modules/grpc/service/message/mod.rs +++ b/src/modules/grpc/service/message/mod.rs @@ -85,13 +85,13 @@ impl MessageService for RustMailerMessageService { async fn list_messages( &self, request: Request, - ) -> Result, Status> { + ) -> Result, Status> { let req = require_account_access(request, |r| r.account_id)?; let result = list_messages_in_mailbox( req.account_id, &req.mailbox_name, - req.page, + req.next_page_token.as_deref(), req.page_size, req.remote, req.desc, diff --git a/src/modules/grpc/tests.rs b/src/modules/grpc/tests.rs index 090e546..f53ccdc 100644 --- a/src/modules/grpc/tests.rs +++ b/src/modules/grpc/tests.rs @@ -37,7 +37,7 @@ async fn test1() { let request = ListMessagesRequest { account_id: id!(64), mailbox_name: "INBOX".into(), - page: 1, + next_page_token: None, page_size: 10, remote: false, desc: true, diff --git a/src/modules/message/list.rs b/src/modules/message/list.rs index 3220398..83449df 100644 --- a/src/modules/message/list.rs +++ b/src/modules/message/list.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::{ - encode_mailbox_name, + base64_encode_url_safe, encode_mailbox_name, modules::{ account::{entity::MailerType, v2::AccountV2}, cache::{ @@ -12,10 +12,11 @@ use crate::{ client::GmailClient, envelope::GmailEnvelope, labels::GmailLabels, }, }, + common::{decode_page_token, parallel::run_with_limit}, context::executors::RUST_MAIL_CONTEXT, envelope::extractor::extract_envelope, error::{code::ErrorCode, RustMailerResult}, - rest::response::DataPage, + rest::response::{CursorDataPage, DataPage}, }, raise_error, }; @@ -24,19 +25,29 @@ use async_imap::types::Fetch; pub async fn list_messages_in_mailbox( account_id: u64, mailbox_name: &str, - page: u64, + next_page_token: Option<&str>, page_size: u64, remote: bool, desc: bool, -) -> RustMailerResult> { +) -> RustMailerResult> { let account = AccountV2::check_account_active(account_id, false).await?; - validate_pagination_params(page, page_size)?; + if page_size == 0 { + return Err(raise_error!( + "page_size must be greater than 0.".into(), + ErrorCode::InvalidParameter + )); + } + if page_size > 500 { + return Err(raise_error!( + "The page_size exceeds the maximum allowed limit of 500.".into(), + ErrorCode::InvalidParameter + )); + } let remote = remote || account.minimal_sync(); - if remote { - fetch_remote_messages(account_id, mailbox_name, page, page_size, desc).await + fetch_remote_messages(&account, mailbox_name, next_page_token, page_size, desc).await } else { - fetch_local_messages(&account, mailbox_name, page, page_size, desc).await + fetch_local_messages(&account, mailbox_name, next_page_token, page_size, desc).await } } @@ -47,9 +58,9 @@ fn validate_pagination_params(page: u64, page_size: u64) -> RustMailerResult<()> ErrorCode::InvalidParameter )); } - if page_size > 1000 { + if page_size > 500 { return Err(raise_error!( - "The page_size exceeds the maximum allowed limit of 1000.".into(), + "The page_size exceeds the maximum allowed limit of 500.".into(), ErrorCode::InvalidParameter )); } @@ -57,37 +68,126 @@ fn validate_pagination_params(page: u64, page_size: u64) -> RustMailerResult<()> } async fn fetch_remote_messages( - account_id: u64, + account: &AccountV2, mailbox_name: &str, - page: u64, + next_page_token: Option<&str>, page_size: u64, desc: bool, -) -> RustMailerResult> { - let excutor = RUST_MAIL_CONTEXT.imap(account_id).await?; - let (mut fetches, total_items) = excutor - .retrieve_metadata_paginated( - page, - page_size, - encode_mailbox_name!(mailbox_name).as_str(), - desc, - false, - ) - .await?; +) -> RustMailerResult> { + match account.mailer_type { + MailerType::ImapSmtp => { + let page = decode_page_token(next_page_token)?; + let excutor = RUST_MAIL_CONTEXT.imap(account.id).await?; + let (mut fetches, total_items) = excutor + .retrieve_metadata_paginated( + page, + page_size, + encode_mailbox_name!(mailbox_name).as_str(), + desc, + false, + ) + .await?; + if total_items == 0 { + return Ok(CursorDataPage::new( + None, + Some(page_size), + 0, + Some(0), + vec![], + )); + } - if desc { - fetches.reverse(); + if desc { + fetches.reverse(); + } + + let total_pages = (total_items as f64 / page_size as f64).ceil() as u64; + let envelopes = process_fetches(fetches, account.id, mailbox_name).await?; + + let next_page_token = if page == total_pages { + None + } else { + Some(base64_encode_url_safe!((page + 1).to_string())) + }; + + Ok(CursorDataPage::new( + next_page_token, + Some(page_size), + total_items, + Some(total_pages), + envelopes, + )) + } + MailerType::GmailApi => { + let label_map = + GmailClient::reverse_label_map(account.id, account.use_proxy, true).await?; + let label_id = label_map.get(mailbox_name).ok_or_else(|| { + raise_error!( + format!("Label not found for mailbox: {}", mailbox_name), + ErrorCode::ResourceNotFound + ) + })?; + let message_list = GmailClient::list_messages( + account.id, + account.use_proxy, + label_id, + next_page_token, + None, + page_size, + ) + .await?; + + let total = message_list.result_size_estimate.ok_or_else(|| { + raise_error!( + "Missing 'resultSizeEstimate' in Gmail API response".into(), + ErrorCode::InternalError + ) + })?; + + let messages = message_list.messages; + let messages = match messages { + Some(ref msgs) if !msgs.is_empty() => msgs, + _ => { + return Ok(CursorDataPage { + next_page_token: None, + page_size: Some(page_size), + total_items: 0, + items: vec![], + total_pages: Some(0), + }) + } + }; + + let account_id = account.id; + let use_proxy = account.use_proxy; + let next_page_token = message_list.next_page_token; + let batch_messages = + run_with_limit(5, messages.iter().cloned(), move |index| async move { + GmailClient::get_message(account_id, use_proxy, &index.id).await + }) + .await?; + + let envelopes: Vec = batch_messages + .into_iter() + .map(|m| { + let mut envelope: GmailEnvelope = m.try_into()?; + envelope.account_id = account_id; + envelope.label_name = mailbox_name.into(); + Ok(envelope.into_v3(&label_map)) + }) + .collect::>>()?; + + let total_pages = (total as f64 / page_size as f64).ceil() as u64; + + Ok(CursorDataPage { + next_page_token, + page_size: Some(page_size), + total_items: total, + items: envelopes, + total_pages: Some(total_pages), + }) + } } - - let total_pages = (total_items as f64 / page_size as f64).ceil() as u64; - let envelopes = process_fetches(fetches, account_id, mailbox_name).await?; - - Ok(DataPage::new( - Some(page), - Some(page_size), - total_items, - Some(total_pages), - envelopes, - )) } async fn process_fetches( @@ -106,10 +206,11 @@ async fn process_fetches( async fn fetch_local_messages( account: &AccountV2, mailbox_name: &str, - page: u64, + next_page_token: Option<&str>, page_size: u64, desc: bool, -) -> RustMailerResult> { +) -> RustMailerResult> { + let page = decode_page_token(next_page_token)?; match account.mailer_type { MailerType::ImapSmtp => { let mailbox = MailBox::get(account.id, mailbox_name).await.map_err(|_| { @@ -120,27 +221,77 @@ async fn fetch_local_messages( ErrorCode::MailBoxNotCached ) })?; + let DataPage { + current_page: _, + page_size, + total_items, + items, + total_pages, + } = EmailEnvelopeV3::list_messages_in_mailbox(mailbox.id, page, page_size, desc) + .await?; - EmailEnvelopeV3::list_messages_in_mailbox(mailbox.id, page, page_size, desc).await + if total_items == 0 { + Ok(CursorDataPage::new(None, page_size, 0, None, vec![])) + } else { + let total_pages = total_pages.ok_or_else(|| { + raise_error!( + "Internal error: total_pages is None (this should never happen)".into(), + ErrorCode::InternalError + ) + })?; + + let next_page_token = if page == total_pages { + None + } else { + Some(base64_encode_url_safe!((page + 1).to_string())) + }; + + Ok(CursorDataPage::new( + next_page_token, + page_size, + total_items, + Some(total_pages), + items, + )) + } } MailerType::GmailApi => { let target_label = GmailLabels::get_by_name(account.id, mailbox_name).await?; - let envelopes = - GmailEnvelope::list_messages_in_label(target_label.id, page, page_size, desc) - .await?; + let DataPage { + current_page: _, + page_size, + total_items, + items, + total_pages, + } = GmailEnvelope::list_messages_in_label(target_label.id, page, page_size, desc) + .await?; let map = GmailClient::label_map(account.id, account.use_proxy).await?; - Ok(DataPage { - current_page: envelopes.current_page, - page_size: envelopes.page_size, - total_items: envelopes.total_items, - total_pages: envelopes.total_pages, - items: envelopes - .items - .into_iter() - .map(|e| e.into_v3(&map)) - .collect(), - }) + + if total_items == 0 { + Ok(CursorDataPage::new(None, page_size, 0, None, vec![])) + } else { + let total_pages = total_pages.ok_or_else(|| { + raise_error!( + "Internal error: total_pages is None (this should never happen)".into(), + ErrorCode::InternalError + ) + })?; + + let next_page_token = if page == total_pages { + None + } else { + Some(base64_encode_url_safe!((page + 1).to_string())) + }; + + Ok(CursorDataPage::new( + next_page_token, + page_size, + total_items, + Some(total_pages), + items.into_iter().map(|e| e.into_v3(&map)).collect(), + )) + } } } } diff --git a/src/modules/message/search/payload.rs b/src/modules/message/search/payload.rs index 0e11e31..8ab4974 100644 --- a/src/modules/message/search/payload.rs +++ b/src/modules/message/search/payload.rs @@ -2,19 +2,20 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. +use crate::base64_encode_url_safe; use crate::modules::account::entity::MailerType; use crate::modules::cache::imap::address::AddressEntity; use crate::modules::cache::imap::sync::flow::generate_uid_sequence_hashset; use crate::modules::cache::imap::v2::EmailEnvelopeV3; use crate::modules::cache::vendor::gmail::sync::client::GmailClient; use crate::modules::cache::vendor::gmail::sync::envelope::GmailEnvelope; +use crate::modules::common::decode_page_token; use crate::modules::common::paginated::paginate_vec; use crate::modules::common::parallel::run_with_limit; use crate::modules::database::Paginated; use crate::modules::error::code::ErrorCode; use crate::modules::message::search::cache::IMAP_SEARCH_CACHE; use crate::modules::rest::response::CursorDataPage; -use crate::{base64_decode_url_safe, base64_encode_url_safe}; use crate::{ encode_mailbox_name, modules::{ @@ -611,22 +612,7 @@ impl MessageSearchRequest { )); } - let page = match next_page_token { - Some(next_page_token) => { - let decoded = base64_decode_url_safe!(next_page_token) - .ok() - .and_then(|bytes| String::from_utf8(bytes).ok()) - .and_then(|s| s.parse::().ok()); - - decoded.ok_or_else(|| { - raise_error!( - "Invalid next_page_token: not a valid page token".into(), - ErrorCode::InvalidParameter - ) - })? - } - None => 1, - }; + let page = decode_page_token(next_page_token)?; let mailbox = self.mailbox.as_deref().ok_or_else(|| { raise_error!( "IMAP accounts must specify a mailbox (e.g. INBOX, Sent, or custom folder)".into(), diff --git a/src/modules/rest/api/message.rs b/src/modules/rest/api/message.rs index e5de196..d76ef14 100644 --- a/src/modules/rest/api/message.rs +++ b/src/modules/rest/api/message.rs @@ -125,14 +125,17 @@ impl MessageApi { mailbox: Query, /// fetches messages from the IMAP server; otherwise, uses local data. remote: Query>, - /// The page number for pagination (1-based). - page: Query, + /// The token for fetching the next page of results in pagination. + /// + /// - If `None`, this indicates that the first page should be returned. + /// - If `Some(token)`, the page corresponding to this token will be fetched. + next_page_token: Query>, /// The number of messages per page. page_size: Query, /// lists messages in descending order; otherwise, ascending. internal date desc: Query>, context: ClientContext, - ) -> ApiResult>> { + ) -> ApiResult>> { let remote = remote.0.unwrap_or(false); let desc = desc.0.unwrap_or(false); let account_id = account_id.0; @@ -142,7 +145,7 @@ impl MessageApi { list_messages_in_mailbox( account_id, mailbox.0.trim(), - page.0, + next_page_token.0.as_deref(), page_size.0, remote, desc, diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 3637902..91325c2 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -5,7 +5,7 @@ */ export interface PaginatedResponse { - current_page: number | null; + next_page_token: string | null; page_size: number | null; total_items: number; items: S[]; diff --git a/web/src/api/mailbox/envelope/api.ts b/web/src/api/mailbox/envelope/api.ts index 1e3b26f..4837c32 100644 --- a/web/src/api/mailbox/envelope/api.ts +++ b/web/src/api/mailbox/envelope/api.ts @@ -9,13 +9,39 @@ import axiosInstance from "@/api/axiosInstance"; import { EmailEnvelope } from "@/features/mailbox/data/schema"; import { saveAs } from 'file-saver'; -export const list_messages = async (accountId: number, mailbox: string, page: number, page_size: number, remote: boolean) => { - const response = await axiosInstance.get>(`/api/v1/list-messages/${accountId}?mailbox=${mailbox}&page=${page}&page_size=${page_size}&desc=true&remote=${remote}`); +export const list_messages = async (accountId: number, mailbox: string, page_size: number, remote: boolean, next_page_token?: string) => { + const params = new URLSearchParams({ + mailbox, + page_size: String(page_size), + desc: "true", + remote: String(remote), + }); + if (next_page_token) { + params.append("next_page_token", next_page_token); + } + + const response = await axiosInstance.get>( + `/api/v1/list-messages/${accountId}?${params.toString()}` + ); return response.data; }; -export const search_messages = async (accountId: number, page: number, page_size: number, remote: boolean, data: Record) => { - const response = await axiosInstance.post>(`/api/v1/search-message/${accountId}?page=${page}&page_size=${page_size}&desc=true&remote=${remote}`, data); +export const search_messages = async (accountId: number, page_size: number, remote: boolean, data: Record, next_page_token?: string) => { + const params = new URLSearchParams({ + page_size: String(page_size), + desc: "true", + remote: String(remote), + }); + + if (next_page_token) { + params.append("next_page_token", next_page_token); + } + + const response = await axiosInstance.post>( + `/api/v1/search-message/${accountId}?${params.toString()}`, + data + ); + return response.data; }; diff --git a/web/src/features/mailbox/components/mail-list.tsx b/web/src/features/mailbox/components/mail-list.tsx index e4428da..d480f07 100644 --- a/web/src/features/mailbox/components/mail-list.tsx +++ b/web/src/features/mailbox/components/mail-list.tsx @@ -68,7 +68,7 @@ export function MailList({ return (
- {items.map((item, index) => { + {items.map((item) => { const isUnread = item.labels && item.labels.length > 0 ? gmail_unread(item) : !seen(item); @@ -77,7 +77,7 @@ export function MailList({ return (
handleCheckboxChange(checked, item.uid)} onClick={(e) => e.stopPropagation()} - className="h-3 w-3 shrink-0" + className="h-4 w-3 shrink-0" />
diff --git a/web/src/features/mailbox/components/mail.tsx b/web/src/features/mailbox/components/mail.tsx index 99d5656..8cd672f 100644 --- a/web/src/features/mailbox/components/mail.tsx +++ b/web/src/features/mailbox/components/mail.tsx @@ -40,6 +40,7 @@ import { EnvelopeDeleteDialog } from "./delete-dialog" import { useFlagMessageMutation } from "@/hooks/use-flag-messages" import { EnvelopeFilterDialog } from "./envelope-filter-dialog" import Logo from '@/assets/logo.svg' +import { PaginatedResponse } from "@/api" interface MailProps { defaultLayout: number[] | undefined @@ -51,7 +52,7 @@ interface MailProps { interface ListMessagesOptions { accountId: number | undefined; mailbox: string | undefined; - page: number; + next_page_token: string | undefined; page_size: number; remote: boolean; filter?: FilterForm; @@ -114,22 +115,44 @@ function buildPayload(filterForm: FilterForm): any { }; } -const useListMessages = ({ accountId, mailbox, page, page_size, remote, filter }: ListMessagesOptions) => { - return useQuery({ - queryKey: ['mailbox-list-messages', `${accountId}`, mailbox, page, page_size, remote, filter], - queryFn: () => { - if (filter) { - const payload = { - mailbox: mailbox!, - search: buildPayload(filter) - }; - return search_messages(accountId!, page, page_size, remote, payload); - } - return list_messages(accountId!, mailbox!, page, page_size, remote); - }, - enabled: !!accountId && !!mailbox, - }); -}; +// const useListMessages = ({ accountId, mailbox, page, page_size, remote, filter }: ListMessagesOptions) => { +// return useQuery({ +// queryKey: ['mailbox-list-messages', `${accountId}`, mailbox, page, page_size, remote, filter], +// queryFn: () => { +// if (filter) { +// const payload = { +// mailbox: mailbox!, +// search: buildPayload(filter) +// }; +// return search_messages(accountId!, page, page_size, remote, payload); +// } +// return list_messages(accountId!, mailbox!, page, page_size, remote); +// }, +// enabled: !!accountId && !!mailbox, +// }); +// }; + + +export async function listMessagesAPI({ + accountId, + mailbox, + next_page_token, + page_size, + remote, + filter +}: ListMessagesOptions) { + if (filter) { + const payload = { + mailbox, + search: buildPayload(filter) + }; + return await search_messages(accountId!, page_size, remote, payload, next_page_token); + } + + return await list_messages(accountId!, mailbox!, page_size, remote, next_page_token); +} + + export function Mail({ defaultLayout = [20, 80], @@ -143,6 +166,14 @@ export function Mail({ const [selectedAccountId, setSelectedAccountId] = React.useState(lastSelectedAccountId); const [selectedEvelope, setSelectedEvelope] = React.useState(undefined); const [useIMAP, setUseIMAP] = React.useState(false); + + const [envelopes, setEnvelopes] = React.useState | undefined>(undefined); + const [isMessagesLoading, setIsMessagesLoading] = React.useState(false); + const [isError, setIsError] = React.useState(false); + const [error, setError] = React.useState(undefined); + // const [pageTokenMap, setPageTokenMap] = React.useState>({}); + const pageTokenMapRef = React.useRef>({}); + const [page, setPage] = React.useState(0); const [pageSize, setPageSize] = React.useState(10); const [selectedUids, setSelectedUids] = React.useState([]); @@ -159,14 +190,48 @@ export function Mail({ enabled: !!selectedAccountId, }) - const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({ - accountId: selectedAccountId, - mailbox: selectedMailbox?.name, - page: page + 1, - page_size: pageSize, - remote: useIMAP, - filter: currentFilter - }); + + React.useEffect(() => { + if (!selectedAccountId || !selectedMailbox) { + return; + } + + (async () => { + setIsMessagesLoading(true); + setIsError(false); + setError(undefined); + const next_page_token = pageTokenMapRef.current[page]; + try { + const data = await listMessagesAPI({ + accountId: selectedAccountId, + mailbox: selectedMailbox?.name, + page_size: pageSize, + remote: useIMAP, + filter: currentFilter, + next_page_token + }); + setEnvelopes(data); + pageTokenMapRef.current = { + ...pageTokenMapRef.current, + [page + 1]: data.next_page_token ?? undefined, + }; + } catch (error) { + setIsError(true); + setError(error); + } finally { + setIsMessagesLoading(false) + } + })(); + }, [selectedAccountId, selectedMailbox, page, pageSize, useIMAP, currentFilter]) + + // const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({ + // accountId: selectedAccountId, + // mailbox: selectedMailbox?.name, + // page: page + 1, + // page_size: pageSize, + // remote: useIMAP, + // filter: currentFilter + // }); const triggerUpdate = (mailbox: string) => { queryClient.refetchQueries({ @@ -187,11 +252,8 @@ export function Mail({ mailbox: selectedMailbox?.name, search: buildPayload(data) }; - const result = await search_messages(selectedAccountId, 1, 10, useIMAP, payload); - queryClient.setQueryData( - ['mailbox-list-messages', selectedAccountId, selectedMailbox?.name, 1, 10, useIMAP, data], - result - ); + const result = await search_messages(selectedAccountId, 10, useIMAP, payload); + setEnvelopes(result); } catch (error) { console.error('Error fetching messages:', error); toast({ @@ -212,7 +274,10 @@ export function Mail({ setSelectedUids([]); } - + const hasNextPage = () => { + return !!pageTokenMapRef.current[page + 1]; + } + const handlePageChange = (newPage: number) => { setPage(newPage); setSelectedUids([]); @@ -468,6 +533,7 @@ export function Mail({ {selectedMailbox &&
boolean, setPageIndex: (pageIndex: number) => void, setPageSize: (pageSize: number) => void, } @@ -31,6 +30,7 @@ export function EnvelopeListPagination({ totalItems, pageIndex, pageSize, + hasNextPage, setPageIndex, setPageSize, }: PaginationProps) { @@ -42,9 +42,9 @@ export function EnvelopeListPagination({ setPageIndex(0) // Reset to the first page when page size changes } - const goToFirstPage = () => { - setPageIndex(0) - } + // const goToFirstPage = () => { + // setPageIndex(0) + // } const goToPreviousPage = () => { const newPageIndex = Math.max(pageIndex - 1, 0) @@ -56,10 +56,10 @@ export function EnvelopeListPagination({ setPageIndex(newPageIndex) } - const goToLastPage = () => { - const newPageIndex = pageCount - 1 - setPageIndex(newPageIndex) - } + // const goToLastPage = () => { + // const newPageIndex = pageCount - 1 + // setPageIndex(newPageIndex) + // } return (
@@ -89,7 +89,7 @@ export function EnvelopeListPagination({ Page {pageIndex + 1} of {pageCount}
- + */} - + */}
diff --git a/web/src/features/settings/license/index.tsx b/web/src/features/settings/license/index.tsx index 89bd06a..43285b6 100644 --- a/web/src/features/settings/license/index.tsx +++ b/web/src/features/settings/license/index.tsx @@ -231,10 +231,11 @@ export default function License() { const FormSchema = z.object({ license: z .string({ required_error: 'License key is required.' }) - .min(300, { message: "This is not a valid license key." }) - .max(500, { message: "This is not a valid license key." }), + .min(300, { message: "License key is too short. Please check your license." }) + .max(800, { message: "License key is too long. Please check your license." }), }); + function UploadLicenseForm({ className, close }: React.ComponentProps<"form"> & { close: () => void }) { const [open, setOpen] = useState(false); const mutation = useMutation({