feat: implement tag counts fetching and display in sidebar and email components
This commit is contained in:
@@ -108,6 +108,7 @@ export default function Home() {
|
||||
selectedKeyword,
|
||||
selectKeyword,
|
||||
hasMoreEmails,
|
||||
fetchTagCounts,
|
||||
} = useEmailStore();
|
||||
|
||||
// Keyboard shortcuts handlers
|
||||
@@ -299,6 +300,9 @@ export default function Home() {
|
||||
await fetchEmails(client);
|
||||
}
|
||||
|
||||
// Fetch tag counts
|
||||
fetchTagCounts(client);
|
||||
|
||||
// Setup push notifications after successful data load
|
||||
try {
|
||||
// Register state change callback
|
||||
@@ -330,7 +334,7 @@ export default function Home() {
|
||||
client.closePushNotifications();
|
||||
}
|
||||
};
|
||||
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, handleStateChange, setPushConnected]);
|
||||
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, handleStateChange, setPushConnected]);
|
||||
|
||||
// Handle mark-as-read with delay based on settings
|
||||
useEffect(() => {
|
||||
@@ -587,6 +591,9 @@ export default function Home() {
|
||||
|
||||
// Refresh emails list to show color in list
|
||||
await fetchEmails(client, selectedMailbox);
|
||||
|
||||
// Refresh tag counts
|
||||
fetchTagCounts(client);
|
||||
} catch (error) {
|
||||
console.error("Failed to set color tag:", error);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useUIStore } from "@/stores/ui-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useVacationStore } from "@/stores/vacation-store";
|
||||
import { useSettingsStore, KEYWORD_PALETTE, KeywordDefinition } from "@/stores/settings-store";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { debug } from "@/lib/debug";
|
||||
|
||||
@@ -227,11 +228,15 @@ function TagItem({
|
||||
isSelected,
|
||||
isCollapsed,
|
||||
onTagSelect,
|
||||
totalCount,
|
||||
unreadCount,
|
||||
}: {
|
||||
kw: KeywordDefinition;
|
||||
isSelected: boolean;
|
||||
isCollapsed: boolean;
|
||||
onTagSelect?: (keywordId: string | null) => void;
|
||||
totalCount: number;
|
||||
unreadCount: number;
|
||||
}) {
|
||||
const t = useTranslations('notifications');
|
||||
const palette = KEYWORD_PALETTE[kw.color];
|
||||
@@ -272,7 +277,26 @@ function TagItem({
|
||||
title={isCollapsed ? kw.label : undefined}
|
||||
>
|
||||
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", palette?.dot || "bg-gray-400", !isCollapsed && "mr-2")} />
|
||||
{!isCollapsed && <span className="truncate">{kw.label}</span>}
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<span className="truncate">{kw.label}</span>
|
||||
<span className="flex items-center gap-1.5 ml-2 flex-shrink-0">
|
||||
{unreadCount > 0 && (
|
||||
<span className={cn(
|
||||
"text-xs rounded-full px-2 py-0.5 font-medium",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-foreground text-background"
|
||||
)}>
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{totalCount}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -321,6 +345,7 @@ export function Sidebar({
|
||||
} catch { return true; }
|
||||
});
|
||||
const emailKeywords = useSettingsStore(s => s.emailKeywords);
|
||||
const tagCounts = useEmailStore(s => s.tagCounts);
|
||||
const t = useTranslations('sidebar');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -532,6 +557,8 @@ export function Sidebar({
|
||||
isSelected={isSelected}
|
||||
isCollapsed={isCollapsed}
|
||||
onTagSelect={onTagSelect}
|
||||
totalCount={tagCounts[kw.id]?.total ?? 0}
|
||||
unreadCount={tagCounts[kw.id]?.unread ?? 0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEmailStore } from "@/stores/email-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useDragDropContext } from "@/contexts/drag-drop-context";
|
||||
|
||||
|
||||
interface UseTagDropOptions {
|
||||
tagId: string;
|
||||
onSuccess?: (count: number, tagLabel: string) => void;
|
||||
@@ -25,7 +26,7 @@ interface UseTagDropReturn {
|
||||
export function useTagDrop({ tagId, onSuccess, onError }: UseTagDropOptions): UseTagDropReturn {
|
||||
const [isOver, setIsOver] = useState(false);
|
||||
const { client } = useAuthStore();
|
||||
const { fetchEmails, selectedMailbox } = useEmailStore();
|
||||
const { fetchEmails, fetchTagCounts, selectedMailbox } = useEmailStore();
|
||||
const { isDragging, endDrag } = useDragDropContext();
|
||||
|
||||
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
@@ -93,6 +94,9 @@ export function useTagDrop({ tagId, onSuccess, onError }: UseTagDropOptions): Us
|
||||
// Refresh the email list
|
||||
await fetchEmails(client, selectedMailbox);
|
||||
|
||||
// Refresh tag counts
|
||||
fetchTagCounts(client);
|
||||
|
||||
onSuccess?.(emailIds.length, tagId);
|
||||
} catch (error) {
|
||||
console.error("Failed to tag emails:", error);
|
||||
@@ -100,7 +104,7 @@ export function useTagDrop({ tagId, onSuccess, onError }: UseTagDropOptions): Us
|
||||
} finally {
|
||||
endDrag();
|
||||
}
|
||||
}, [client, isDragging, tagId, fetchEmails, selectedMailbox, endDrag, onSuccess, onError]);
|
||||
}, [client, isDragging, tagId, fetchEmails, fetchTagCounts, selectedMailbox, endDrag, onSuccess, onError]);
|
||||
|
||||
return {
|
||||
dropHandlers: {
|
||||
|
||||
@@ -548,6 +548,53 @@ export class JMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async getTagCounts(tagIds: string[]): Promise<Record<string, { total: number; unread: number }>> {
|
||||
if (tagIds.length === 0) return {};
|
||||
try {
|
||||
const methodCalls: JMAPMethodCall[] = [];
|
||||
for (let i = 0; i < tagIds.length; i++) {
|
||||
const keyword = `$label:${tagIds[i]}`;
|
||||
// Total count for this tag
|
||||
methodCalls.push(["Email/query", {
|
||||
accountId: this.accountId,
|
||||
filter: { hasKeyword: keyword },
|
||||
limit: 0,
|
||||
calculateTotal: true,
|
||||
}, `total_${i}`]);
|
||||
// Unread count for this tag
|
||||
methodCalls.push(["Email/query", {
|
||||
accountId: this.accountId,
|
||||
filter: {
|
||||
operator: "AND",
|
||||
conditions: [
|
||||
{ hasKeyword: keyword },
|
||||
{ notKeyword: "$seen" },
|
||||
],
|
||||
},
|
||||
limit: 0,
|
||||
calculateTotal: true,
|
||||
}, `unread_${i}`]);
|
||||
}
|
||||
|
||||
const response = await this.request(methodCalls);
|
||||
const result: Record<string, { total: number; unread: number }> = {};
|
||||
|
||||
for (let i = 0; i < tagIds.length; i++) {
|
||||
const totalResp = response.methodResponses?.[i * 2]?.[1];
|
||||
const unreadResp = response.methodResponses?.[i * 2 + 1]?.[1];
|
||||
result[tagIds[i]] = {
|
||||
total: totalResp?.total ?? 0,
|
||||
unread: unreadResp?.total ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to get tag counts:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async getEmail(emailId: string, accountId?: string): Promise<Email | null> {
|
||||
try {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
|
||||
@@ -31,6 +31,7 @@ interface EmailStore {
|
||||
|
||||
// Keyword/tag filter
|
||||
selectedKeyword: string | null;
|
||||
tagCounts: Record<string, { total: number; unread: number }>;
|
||||
|
||||
// Advanced search state
|
||||
searchFilters: SearchFilters;
|
||||
@@ -47,6 +48,7 @@ interface EmailStore {
|
||||
setSearchQuery: (query: string) => void;
|
||||
setQuota: (quota: { used: number; total: number } | null) => void;
|
||||
selectKeyword: (keyword: string | null) => void;
|
||||
fetchTagCounts: (client: JMAPClient) => Promise<void>;
|
||||
toggleEmailSelection: (emailId: string) => void;
|
||||
selectRangeEmails: (targetEmailId: string) => void;
|
||||
lastSelectedEmailId: string | null;
|
||||
@@ -143,6 +145,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Keyword/tag filter
|
||||
selectedKeyword: null,
|
||||
tagCounts: {},
|
||||
|
||||
// Advanced search state
|
||||
searchFilters: { ...DEFAULT_SEARCH_FILTERS },
|
||||
@@ -162,6 +165,20 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
expandedThreadIds: new Set(),
|
||||
threadEmailsCache: new Map(),
|
||||
}),
|
||||
fetchTagCounts: async (client) => {
|
||||
try {
|
||||
const keywords = useSettingsStore.getState().emailKeywords;
|
||||
if (keywords.length === 0) {
|
||||
set({ tagCounts: {} });
|
||||
return;
|
||||
}
|
||||
const tagIds = keywords.map(k => k.id);
|
||||
const counts = await client.getTagCounts(tagIds);
|
||||
set({ tagCounts: counts });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tag counts:', error);
|
||||
}
|
||||
},
|
||||
selectMailbox: (mailboxId) => set({
|
||||
selectedMailbox: mailboxId,
|
||||
selectedEmail: null,
|
||||
@@ -1040,6 +1057,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Handle Email state changes - refresh current mailbox
|
||||
if (accountChanges.Email) {
|
||||
await get().refreshCurrentMailbox(client);
|
||||
get().fetchTagCounts(client);
|
||||
}
|
||||
|
||||
// Handle Mailbox state changes - refresh mailbox list
|
||||
|
||||
Reference in New Issue
Block a user