Merge branch 'dev'
This commit is contained in:
@@ -151,15 +151,26 @@ describe('filter-store', () => {
|
||||
});
|
||||
|
||||
describe('fetchFilters', () => {
|
||||
it('should set isOpaque for scripts without metadata', async () => {
|
||||
it('parses external rules from scripts without metadata', async () => {
|
||||
const mockClient = {
|
||||
getSieveCapabilities: () => null,
|
||||
getSieveScripts: async () => [{ id: 's1', name: 'main', blobId: 'b1', isActive: true }],
|
||||
getSieveScriptContent: async () => 'require ["fileinto"];\nif header :contains "From" "x" { fileinto "Y"; }',
|
||||
};
|
||||
await useFilterStore.getState().fetchFilters(mockClient as unknown as IJMAPClient);
|
||||
expect(useFilterStore.getState().isOpaque).toBe(false);
|
||||
expect(useFilterStore.getState().rules).toHaveLength(1);
|
||||
expect(useFilterStore.getState().rules[0].origin).toBe('external');
|
||||
});
|
||||
|
||||
it('sets isOpaque for truly unparseable content', async () => {
|
||||
const mockClient = {
|
||||
getSieveCapabilities: () => null,
|
||||
getSieveScripts: async () => [{ id: 's1', name: 'main', blobId: 'b1', isActive: true }],
|
||||
getSieveScriptContent: async () => '/* @metadata:begin\n{corrupt\n@metadata:end */',
|
||||
};
|
||||
await useFilterStore.getState().fetchFilters(mockClient as unknown as IJMAPClient);
|
||||
expect(useFilterStore.getState().isOpaque).toBe(true);
|
||||
expect(useFilterStore.getState().rules).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse rules from metadata-bearing script', async () => {
|
||||
|
||||
@@ -49,6 +49,7 @@ interface AuthState {
|
||||
syncIdentities: () => void;
|
||||
refreshIdentities: () => Promise<void>;
|
||||
getClientForAccount: (accountId: string) => JMAPClient | undefined;
|
||||
getAllConnectedClients: () => Map<string, JMAPClient>;
|
||||
}
|
||||
|
||||
const ERROR_PATTERNS: Array<{ key: string; matches: string[] }> = [
|
||||
@@ -1529,6 +1530,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
getClientForAccount: (accountId: string) => {
|
||||
return clients.get(accountId);
|
||||
},
|
||||
|
||||
getAllConnectedClients: () => {
|
||||
return new Map(clients);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
|
||||
+236
-9
@@ -1,10 +1,14 @@
|
||||
import { create } from "zustand";
|
||||
import { Email, Mailbox, StateChange } from "@/lib/jmap/types";
|
||||
import { Email, Mailbox, StateChange, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID } from "@/lib/jmap/types";
|
||||
import type { UnifiedMailboxRole } from "@/lib/jmap/types";
|
||||
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useCalendarStore } from "@/stores/calendar-store";
|
||||
import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils";
|
||||
import { emailHooks } from "@/lib/plugin-hooks";
|
||||
import { fetchUnifiedEmails, fetchUnifiedMailboxCounts, type UnifiedAccountClient, type UnifiedMailboxCounts } from "@/lib/unified-mailbox";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
|
||||
interface EmailStore {
|
||||
emails: Email[];
|
||||
@@ -39,6 +43,12 @@ interface EmailStore {
|
||||
isAdvancedSearchOpen: boolean;
|
||||
searchAbortController: AbortController | null;
|
||||
|
||||
// Unified mailbox state
|
||||
isUnifiedView: boolean;
|
||||
unifiedRole: UnifiedMailboxRole | null;
|
||||
unifiedErrors: Map<string, string>; // accountId -> error message
|
||||
unifiedCounts: UnifiedMailboxCounts[];
|
||||
|
||||
setEmails: (emails: Email[]) => void;
|
||||
setMailboxes: (mailboxes: Mailbox[]) => void;
|
||||
selectEmail: (email: Email | null) => void;
|
||||
@@ -77,7 +87,7 @@ interface EmailStore {
|
||||
|
||||
// Batch operations
|
||||
batchMarkAsRead: (client: IJMAPClient, read: boolean) => Promise<void>;
|
||||
batchDelete: (client: IJMAPClient) => Promise<void>;
|
||||
batchDelete: (client: IJMAPClient, permanent?: boolean) => Promise<void>;
|
||||
batchMoveToMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||
|
||||
// Spam operations
|
||||
@@ -107,6 +117,12 @@ interface EmailStore {
|
||||
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
||||
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||
|
||||
// Unified mailbox operations
|
||||
fetchUnifiedEmails: (accounts: UnifiedAccountClient[], role: UnifiedMailboxRole) => Promise<void>;
|
||||
loadMoreUnifiedEmails: (accounts: UnifiedAccountClient[]) => Promise<void>;
|
||||
refreshUnifiedCounts: (accounts: UnifiedAccountClient[]) => Promise<void>;
|
||||
exitUnifiedView: () => void;
|
||||
|
||||
// Mock data for demo
|
||||
loadMockData: () => void;
|
||||
}
|
||||
@@ -175,6 +191,12 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
isAdvancedSearchOpen: false,
|
||||
searchAbortController: null,
|
||||
|
||||
// Unified mailbox state
|
||||
isUnifiedView: false,
|
||||
unifiedRole: null,
|
||||
unifiedErrors: new Map(),
|
||||
unifiedCounts: [],
|
||||
|
||||
// Spam undo cache
|
||||
spamUndoCache: new Map(),
|
||||
|
||||
@@ -334,11 +356,52 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
loadMoreEmails: async (client) => {
|
||||
const { isLoadingMore, hasMoreEmails, emails, selectedMailbox, searchQuery, selectedKeyword } = get();
|
||||
const { isLoadingMore, hasMoreEmails, emails, selectedMailbox, searchQuery, selectedKeyword, isUnifiedView, unifiedRole } = get();
|
||||
|
||||
// Don't load if already loading or no more emails
|
||||
if (isLoadingMore || !hasMoreEmails) return;
|
||||
|
||||
// Unified view uses a different fan-out loader. Rebuild the per-account
|
||||
// client list from auth/account stores and delegate.
|
||||
if (isUnifiedView && unifiedRole) {
|
||||
set({ isLoadingMore: true, error: null });
|
||||
try {
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
const position = emails.length;
|
||||
const authAccounts = useAccountStore.getState().accounts.filter(a => a.isConnected);
|
||||
const allClients = useAuthStore.getState().getAllConnectedClients();
|
||||
const built: UnifiedAccountClient[] = [];
|
||||
for (const a of authAccounts) {
|
||||
const c = allClients.get(a.id);
|
||||
if (!c) continue;
|
||||
try {
|
||||
const mailboxes = await c.getMailboxes();
|
||||
built.push({ accountId: a.id, accountLabel: a.label || a.email, client: c, mailboxes });
|
||||
} catch {
|
||||
/* skip account on mailbox fetch failure */
|
||||
}
|
||||
}
|
||||
const result = await fetchUnifiedEmails(built, unifiedRole, emailsPerPage, position);
|
||||
const currentEmails = get().emails;
|
||||
const existingIds = new Set(currentEmails.map(e => e.id));
|
||||
const newEmails = result.emails.filter(e => !existingIds.has(e.id));
|
||||
set({
|
||||
emails: [...currentEmails, ...newEmails],
|
||||
hasMoreEmails: result.hasMore,
|
||||
totalEmails: result.total,
|
||||
isLoadingMore: false,
|
||||
unifiedErrors: result.errors,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load more unified emails:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : "Failed to load more emails",
|
||||
isLoadingMore: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
set({ isLoadingMore: true, error: null });
|
||||
try {
|
||||
// Get emails per page from settings
|
||||
@@ -919,7 +982,26 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const emailIdsArray = Array.from(selectedEmailIds);
|
||||
await client.batchMarkAsRead(emailIdsArray, read);
|
||||
|
||||
if (get().isUnifiedView) {
|
||||
// Group emails by accountId for cross-account operations
|
||||
const emailsByAccount = new Map<string, string[]>();
|
||||
for (const emailId of emailIdsArray) {
|
||||
const email = emails.find(e => e.id === emailId);
|
||||
const acctId = email?.accountId || '__default__';
|
||||
if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []);
|
||||
emailsByAccount.get(acctId)!.push(emailId);
|
||||
}
|
||||
|
||||
const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => {
|
||||
const acctClient = acctId === '__default__' ? client : useAuthStore.getState().getClientForAccount(acctId);
|
||||
if (!acctClient) return;
|
||||
await acctClient.batchMarkAsRead(ids, read);
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
} else {
|
||||
await client.batchMarkAsRead(emailIdsArray, read);
|
||||
}
|
||||
|
||||
// Update local state
|
||||
const updatedEmails = emails.map(email =>
|
||||
@@ -962,14 +1044,60 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
batchDelete: async (client) => {
|
||||
const { selectedEmailIds, emails, mailboxes } = get();
|
||||
batchDelete: async (client, permanent = false) => {
|
||||
const { selectedEmailIds, emails, mailboxes, selectedMailbox } = get();
|
||||
if (selectedEmailIds.size === 0) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const emailIdsArray = Array.from(selectedEmailIds);
|
||||
await client.batchDeleteEmails(emailIdsArray);
|
||||
|
||||
// Determine if the current folder forces permanent deletion.
|
||||
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||
const isInTrash = currentMailbox?.role === 'trash';
|
||||
const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
|
||||
const isInJunk = currentMailbox?.role === 'junk';
|
||||
const forceDestroy = permanent || isInTrash || (isInJunk && permanentlyDeleteJunk);
|
||||
|
||||
// Group emails by accountId (handles unified view and search results spanning accounts).
|
||||
const emailsByAccount = new Map<string, string[]>();
|
||||
for (const emailId of emailIdsArray) {
|
||||
const email = emails.find(e => e.id === emailId);
|
||||
const acctId = email?.accountId || '__default__';
|
||||
if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []);
|
||||
emailsByAccount.get(acctId)!.push(emailId);
|
||||
}
|
||||
|
||||
const getClient = (acctId: string) =>
|
||||
acctId === '__default__' ? client : useAuthStore.getState().getClientForAccount(acctId);
|
||||
|
||||
if (forceDestroy) {
|
||||
const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => {
|
||||
const acctClient = getClient(acctId);
|
||||
if (!acctClient) return;
|
||||
await acctClient.batchDeleteEmails(ids);
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
} else {
|
||||
// Move to trash per account.
|
||||
const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => {
|
||||
const acctClient = getClient(acctId);
|
||||
if (!acctClient) return;
|
||||
const trashMailbox = mailboxes.find(mb => {
|
||||
if (mb.role !== 'trash') return false;
|
||||
if (acctId === '__default__') return !mb.isShared;
|
||||
return mb.accountId === acctId;
|
||||
});
|
||||
if (!trashMailbox) {
|
||||
// No trash available for this account — fall back to destroy so the action isn't silently dropped.
|
||||
await acctClient.batchDeleteEmails(ids);
|
||||
return;
|
||||
}
|
||||
const trashId = trashMailbox.originalId || trashMailbox.id;
|
||||
await acctClient.batchMoveEmails(ids, trashId, trashMailbox.accountId);
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
// Remove deleted emails from local state
|
||||
const remainingEmails = emails.filter(e => !selectedEmailIds.has(e.id));
|
||||
@@ -1020,7 +1148,26 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const emailIdsArray = Array.from(selectedEmailIds);
|
||||
await client.batchMoveEmails(emailIdsArray, toMailboxId);
|
||||
|
||||
if (get().isUnifiedView) {
|
||||
// Group emails by accountId for cross-account operations
|
||||
const emailsByAccount = new Map<string, string[]>();
|
||||
for (const emailId of emailIdsArray) {
|
||||
const email = emails.find(e => e.id === emailId);
|
||||
const acctId = email?.accountId || '__default__';
|
||||
if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []);
|
||||
emailsByAccount.get(acctId)!.push(emailId);
|
||||
}
|
||||
|
||||
const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => {
|
||||
const acctClient = acctId === '__default__' ? client : useAuthStore.getState().getClientForAccount(acctId);
|
||||
if (!acctClient) return;
|
||||
await acctClient.batchMoveEmails(ids, toMailboxId);
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
} else {
|
||||
await client.batchMoveEmails(emailIdsArray, toMailboxId);
|
||||
}
|
||||
|
||||
// Update local state - remove from current view since they moved
|
||||
const remainingEmails = emails.filter(e => !selectedEmailIds.has(e.id));
|
||||
@@ -1032,7 +1179,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
});
|
||||
|
||||
// Refresh emails to get updated list
|
||||
await get().fetchEmails(client, get().selectedMailbox);
|
||||
if (!get().isUnifiedView) {
|
||||
await get().fetchEmails(client, get().selectedMailbox);
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : "Failed to move emails",
|
||||
@@ -1481,6 +1630,84 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// Unified mailbox operations
|
||||
fetchUnifiedEmails: async (accounts, role) => {
|
||||
set({
|
||||
isLoading: true,
|
||||
error: null,
|
||||
isUnifiedView: true,
|
||||
unifiedRole: role,
|
||||
selectedKeyword: null,
|
||||
});
|
||||
try {
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
const result = await fetchUnifiedEmails(accounts, role, emailsPerPage, 0);
|
||||
set({
|
||||
emails: result.emails,
|
||||
hasMoreEmails: result.hasMore,
|
||||
totalEmails: result.total,
|
||||
isLoading: false,
|
||||
unifiedErrors: result.errors,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch unified emails:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : "Failed to fetch unified emails",
|
||||
isLoading: false,
|
||||
emails: [],
|
||||
hasMoreEmails: false,
|
||||
totalEmails: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadMoreUnifiedEmails: async (accounts) => {
|
||||
const { isLoadingMore, hasMoreEmails, emails, unifiedRole } = get();
|
||||
if (isLoadingMore || !hasMoreEmails || !unifiedRole) return;
|
||||
|
||||
set({ isLoadingMore: true, error: null });
|
||||
try {
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
const position = emails.length;
|
||||
const result = await fetchUnifiedEmails(accounts, unifiedRole, emailsPerPage, position);
|
||||
|
||||
const currentEmails = get().emails;
|
||||
const existingIds = new Set(currentEmails.map(e => e.id));
|
||||
const newEmails = result.emails.filter(e => !existingIds.has(e.id));
|
||||
|
||||
set({
|
||||
emails: [...currentEmails, ...newEmails],
|
||||
hasMoreEmails: result.hasMore,
|
||||
totalEmails: result.total,
|
||||
isLoadingMore: false,
|
||||
unifiedErrors: result.errors,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load more unified emails:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : "Failed to load more unified emails",
|
||||
isLoadingMore: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refreshUnifiedCounts: async (accounts) => {
|
||||
try {
|
||||
const counts = fetchUnifiedMailboxCounts(accounts);
|
||||
set({ unifiedCounts: counts });
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh unified counts:', error);
|
||||
}
|
||||
},
|
||||
|
||||
exitUnifiedView: () => {
|
||||
set({
|
||||
isUnifiedView: false,
|
||||
unifiedRole: null,
|
||||
unifiedErrors: new Map(),
|
||||
});
|
||||
},
|
||||
|
||||
loadMockData: () => {
|
||||
const mockEmails: Email[] = [
|
||||
{
|
||||
|
||||
+54
-15
@@ -16,6 +16,7 @@ interface FilterStore {
|
||||
isOpaque: boolean;
|
||||
rawScript: string;
|
||||
vacationSettings: VacationSieveConfig | null;
|
||||
externalRequires: string[];
|
||||
|
||||
setSupported: (supported: boolean) => void;
|
||||
fetchFilters: (client: IJMAPClient) => Promise<void>;
|
||||
@@ -43,6 +44,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
isOpaque: false,
|
||||
rawScript: '',
|
||||
vacationSettings: null,
|
||||
externalRequires: [],
|
||||
|
||||
setSupported: (supported) => set({ isSupported: supported }),
|
||||
|
||||
@@ -74,10 +76,22 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
|
||||
if (result.isOpaque) {
|
||||
debug.log('filters', 'Sieve script is opaque (hand-edited)');
|
||||
set({ isLoading: false, isOpaque: true, rules: [], vacationSettings: result.vacation || null });
|
||||
set({
|
||||
isLoading: false,
|
||||
isOpaque: true,
|
||||
rules: [],
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
});
|
||||
} else {
|
||||
debug.log('filters', 'Parsed', result.rules.length, 'filter rules');
|
||||
set({ isLoading: false, isOpaque: false, rules: result.rules, vacationSettings: result.vacation || null });
|
||||
set({
|
||||
isLoading: false,
|
||||
isOpaque: false,
|
||||
rules: result.rules,
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
debug.error('Failed to fetch filters:', error);
|
||||
@@ -91,13 +105,13 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
saveFilters: async (client) => {
|
||||
set({ isSaving: true, error: null });
|
||||
try {
|
||||
const { isOpaque, rawScript, rules, activeScriptId, vacationSettings } = get();
|
||||
const { isOpaque, rawScript, rules, activeScriptId, vacationSettings, externalRequires } = get();
|
||||
|
||||
let content: string;
|
||||
if (isOpaque) {
|
||||
content = rawScript;
|
||||
} else {
|
||||
content = generateScript(rules, vacationSettings || undefined);
|
||||
content = generateScript(rules, vacationSettings || undefined, { externalRequires });
|
||||
}
|
||||
|
||||
if (activeScriptId) {
|
||||
@@ -124,40 +138,60 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
},
|
||||
|
||||
addRule: (rule) => {
|
||||
set((state) => ({ rules: [...state.rules, rule] }));
|
||||
// Insert new bulwark rules before external/opaque rules so Bulwark's
|
||||
// managed section stays contiguous.
|
||||
set((state) => {
|
||||
const bulwark = state.rules.filter(r => !r.origin || r.origin === 'bulwark');
|
||||
const external = state.rules.filter(r => r.origin === 'external' || r.origin === 'opaque');
|
||||
return { rules: [...bulwark, rule, ...external] };
|
||||
});
|
||||
},
|
||||
|
||||
updateRule: (ruleId, updates) => {
|
||||
set((state) => ({
|
||||
rules: state.rules.map(r => r.id === ruleId ? { ...r, ...updates } : r),
|
||||
rules: state.rules.map(r => {
|
||||
if (r.id !== ruleId) return r;
|
||||
if (r.origin === 'external' || r.origin === 'opaque') return r; // read-only
|
||||
return { ...r, ...updates };
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteRule: (ruleId) => {
|
||||
set((state) => ({
|
||||
rules: state.rules.filter(r => r.id !== ruleId),
|
||||
rules: state.rules.filter(r => {
|
||||
if (r.id !== ruleId) return true;
|
||||
return r.origin === 'external' || r.origin === 'opaque';
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
reorderRules: (ruleIds) => {
|
||||
// Only reorder bulwark rules; external rules always stay at the end in
|
||||
// their original order.
|
||||
set((state) => {
|
||||
const ruleMap = new Map(state.rules.map(r => [r.id, r]));
|
||||
const reordered = ruleIds.map(id => ruleMap.get(id)).filter(Boolean) as FilterRule[];
|
||||
return { rules: reordered };
|
||||
const bulwarkMap = new Map(
|
||||
state.rules.filter(r => !r.origin || r.origin === 'bulwark').map(r => [r.id, r]),
|
||||
);
|
||||
const external = state.rules.filter(r => r.origin === 'external' || r.origin === 'opaque');
|
||||
const reordered = ruleIds.map(id => bulwarkMap.get(id)).filter(Boolean) as FilterRule[];
|
||||
return { rules: [...reordered, ...external] };
|
||||
});
|
||||
},
|
||||
|
||||
toggleRule: (ruleId) => {
|
||||
set((state) => ({
|
||||
rules: state.rules.map(r =>
|
||||
r.id === ruleId ? { ...r, enabled: !r.enabled } : r
|
||||
),
|
||||
rules: state.rules.map(r => {
|
||||
if (r.id !== ruleId) return r;
|
||||
if (r.origin === 'external' || r.origin === 'opaque') return r; // read-only
|
||||
return { ...r, enabled: !r.enabled };
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
setRawScript: (content) => set({ rawScript: content }),
|
||||
|
||||
resetToVisualBuilder: () => set({ isOpaque: false, rawScript: '', rules: [] }),
|
||||
resetToVisualBuilder: () => set({ isOpaque: false, rawScript: '', rules: [], externalRequires: [] }),
|
||||
|
||||
syncVacationToScript: async (client, vacation) => {
|
||||
try {
|
||||
@@ -173,6 +207,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
const activeScript = scripts.find(s => s.isActive) || scripts[0];
|
||||
|
||||
let rules = previousRules;
|
||||
let externalRequires = get().externalRequires;
|
||||
|
||||
// If there's an active script, try to parse our metadata from it.
|
||||
// If the server overwrote it (no metadata), fall back to stored rules.
|
||||
@@ -181,11 +216,12 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
const parsed = parseScript(content);
|
||||
if (!parsed.isOpaque) {
|
||||
rules = parsed.rules;
|
||||
externalRequires = parsed.externalRequires;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a combined script with our metadata, rules, and vacation
|
||||
const content = generateScript(rules, vacation.isEnabled ? vacation : undefined);
|
||||
const content = generateScript(rules, vacation.isEnabled ? vacation : undefined, { externalRequires });
|
||||
|
||||
if (activeScript) {
|
||||
// Preserve the script's current activation state — don't pass activate: true
|
||||
@@ -198,6 +234,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
rules,
|
||||
vacationSettings: vacation,
|
||||
isOpaque: false,
|
||||
externalRequires,
|
||||
});
|
||||
} else {
|
||||
// Don't activate; there may be a server-managed 'vacation' script active.
|
||||
@@ -209,6 +246,7 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
rules,
|
||||
vacationSettings: vacation,
|
||||
isOpaque: false,
|
||||
externalRequires,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,5 +267,6 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
isOpaque: false,
|
||||
rawScript: '',
|
||||
vacationSettings: null,
|
||||
externalRequires: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -176,12 +176,18 @@ interface SettingsState {
|
||||
hideAccountSwitcher: boolean;
|
||||
showRailAccountList: boolean;
|
||||
|
||||
// Unified Mailbox
|
||||
enableUnifiedMailbox: boolean;
|
||||
|
||||
// Email Display
|
||||
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
|
||||
|
||||
// Experimental
|
||||
senderFavicons: boolean;
|
||||
|
||||
// Sidebar
|
||||
colorfulSidebarIcons: boolean; // Tint folder icons by role (inbox blue, junk red, etc.)
|
||||
|
||||
// Folders
|
||||
folderIcons: Record<string, string>; // mailboxId -> icon name
|
||||
|
||||
@@ -310,12 +316,18 @@ const DEFAULT_SETTINGS = {
|
||||
hideAccountSwitcher: false,
|
||||
showRailAccountList: false,
|
||||
|
||||
// Unified Mailbox
|
||||
enableUnifiedMailbox: false,
|
||||
|
||||
// Email Display
|
||||
disableThreading: false,
|
||||
|
||||
// Experimental
|
||||
senderFavicons: true,
|
||||
|
||||
// Sidebar
|
||||
colorfulSidebarIcons: true,
|
||||
|
||||
// Folders
|
||||
folderIcons: {} as Record<string, string>,
|
||||
|
||||
@@ -448,7 +460,9 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
toolbarPosition: state.toolbarPosition,
|
||||
hideAccountSwitcher: state.hideAccountSwitcher,
|
||||
showRailAccountList: state.showRailAccountList,
|
||||
enableUnifiedMailbox: state.enableUnifiedMailbox,
|
||||
senderFavicons: state.senderFavicons,
|
||||
colorfulSidebarIcons: state.colorfulSidebarIcons,
|
||||
folderIcons: state.folderIcons,
|
||||
emailKeywords: state.emailKeywords,
|
||||
attachmentReminderEnabled: state.attachmentReminderEnabled,
|
||||
|
||||
Reference in New Issue
Block a user