mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: open the folder a mail folder link names instead of only the mail tab
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import type { Mailbox } from '../api/types';
|
||||
|
||||
/** Roles a folder link may name instead of an id (webmail lib/deep-links.ts). */
|
||||
const ALIASED_ROLES = new Set(['inbox', 'sent', 'drafts', 'trash', 'archive', 'junk']);
|
||||
|
||||
/**
|
||||
* The folder a `mail/folder/<ref>` link names: an exact id first, then a role
|
||||
* alias (the own account's folder before a shared one's). Null when nothing in
|
||||
* the loaded folders matches, a link to a folder that was deleted or lives in
|
||||
* another account (webmail resolveFolderRef).
|
||||
*/
|
||||
export function resolveFolderRef(ref: string, mailboxes: readonly Mailbox[]): string | null {
|
||||
if (mailboxes.some((m) => m.id === ref)) return ref;
|
||||
if (ALIASED_ROLES.has(ref)) {
|
||||
const byRole = mailboxes.find((m) => m.role === ref && !m.isShared)
|
||||
?? mailboxes.find((m) => m.role === ref);
|
||||
return byRole?.id ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { handleDeepLink, parseDeepLink } from '../linking';
|
||||
import { usePendingMailFolder } from '../pending-mail-folder';
|
||||
import { resolveFolderRef } from '../../lib/folder-ref';
|
||||
import type { Mailbox } from '../../api/types';
|
||||
|
||||
// A `mail/folder/<ref>` link opened only the Mail tab; the folder it named
|
||||
// was dropped (file 10, "/mail/folder/<ref> deep link opens only the tab").
|
||||
|
||||
const box = (id: string, role: string | null, isShared = false) => ({ id, name: id, role, isShared } as unknown as Mailbox);
|
||||
|
||||
describe('mail/folder deep link', () => {
|
||||
it('parks the folder for the mail list', async () => {
|
||||
const link = parseDeepLink('bulwarkmobile://mail/folder/archive');
|
||||
const navigation = { isReady: () => true, navigate: vi.fn() };
|
||||
expect(await handleDeepLink(link!, { navigation: navigation as never, resolveThreadId: async () => null })).toBe(true);
|
||||
expect(usePendingMailFolder.getState().consume()).toBe('archive');
|
||||
expect(navigation.navigate).toHaveBeenCalledWith('MainTabs', { screen: 'Mail' });
|
||||
});
|
||||
|
||||
it('resolves an id or a role, the own folder first, and nothing for an unknown one', () => {
|
||||
const boxes = [box('g:in', 'inbox', true), box('in', 'inbox'), box('f1', null)];
|
||||
expect(resolveFolderRef('f1', boxes)).toBe('f1');
|
||||
expect(resolveFolderRef('inbox', boxes)).toBe('in');
|
||||
expect(resolveFolderRef('trash', boxes)).toBeNull();
|
||||
expect(resolveFolderRef('gone', boxes)).toBeNull();
|
||||
});
|
||||
|
||||
it('is opened by the mail list', () => {
|
||||
const list = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailListScreen.tsx'), 'utf8');
|
||||
expect(list).toMatch(/const id = ref === null \? null : resolveFolderRef\(ref, mailboxes\);/);
|
||||
expect(list).toMatch(/if \(id !== currentMailboxId\) void selectMailbox\(id\);/);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import type { RootStackParamList } from './types';
|
||||
import { setPendingSettingsTab } from './pending-settings-tab';
|
||||
import { setPendingCalendarOpen } from './pending-calendar-open';
|
||||
import { setPendingMailSearch } from './pending-mail-search';
|
||||
import { setPendingMailFolder } from './pending-mail-folder';
|
||||
import { setPendingSignInLink, usePendingSignInLinkStore } from './pending-sign-in-link';
|
||||
import { insecurePairingLinkError, parseQrLoginPayload, type QrLoginPayload } from '../lib/oauth';
|
||||
import { MAX_ACCOUNTS } from '../lib/account-utils';
|
||||
@@ -283,6 +284,8 @@ export async function handleDeepLink(link: DeepLink, nav: DeepLinkNavigator): Pr
|
||||
navigation.navigate('MainTabs', { screen: 'Mail' } as never);
|
||||
return true;
|
||||
case 'folder':
|
||||
// The mail list opens the folder once its folders are loaded.
|
||||
setPendingMailFolder(link.ref);
|
||||
navigation.navigate('MainTabs', { screen: 'Mail' } as never);
|
||||
return true;
|
||||
case 'calendar':
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// A `mail/folder/<ref>` deep link arrives before the mail list is mounted or
|
||||
// its folders are loaded. The reference is parked here and consumed by
|
||||
// EmailListScreen, which opens the folder once it can resolve it.
|
||||
interface PendingMailFolderState {
|
||||
ref: string | null;
|
||||
set: (ref: string | null) => void;
|
||||
consume: () => string | null;
|
||||
}
|
||||
|
||||
export const usePendingMailFolder = create<PendingMailFolderState>((set, get) => ({
|
||||
ref: null,
|
||||
set: (ref) => set({ ref }),
|
||||
consume: () => {
|
||||
const ref = get().ref;
|
||||
if (ref !== null) set({ ref: null });
|
||||
return ref;
|
||||
},
|
||||
}));
|
||||
|
||||
export function setPendingMailFolder(ref: string | null): void {
|
||||
usePendingMailFolder.getState().set(ref);
|
||||
}
|
||||
@@ -68,6 +68,8 @@ import { draftContextFromEmail, isDraftEmail } from '../lib/draft-context';
|
||||
import { getFullEmail, emptyMailbox as apiEmptyMailbox } from '../api/email';
|
||||
import type { RootStackParamList } from '../navigation/types';
|
||||
import { usePendingMailSearch } from '../navigation/pending-mail-search';
|
||||
import { usePendingMailFolder } from '../navigation/pending-mail-folder';
|
||||
import { resolveFolderRef } from '../lib/folder-ref';
|
||||
import type { Attachment, Email } from '../api/types';
|
||||
import { createFrameMonitor, logModuleTimes, perfOnce, perfStart } from '../lib/perf-marks';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
@@ -1202,6 +1204,18 @@ export default function EmailListScreen({
|
||||
}
|
||||
}, [ensureMailboxes, mailboxes.length]);
|
||||
|
||||
// A `mail/folder/<ref>` link opens its folder once the folders are here;
|
||||
// one that names no folder of this account leaves the list as it is.
|
||||
const pendingFolder = usePendingMailFolder((s) => s.ref);
|
||||
React.useEffect(() => {
|
||||
if (pendingFolder === null || mailboxes.length === 0) return;
|
||||
const ref = usePendingMailFolder.getState().consume();
|
||||
const id = ref === null ? null : resolveFolderRef(ref, mailboxes);
|
||||
if (!id) return;
|
||||
clearSearchAndFilters();
|
||||
if (id !== currentMailboxId) void selectMailbox(id);
|
||||
}, [pendingFolder, mailboxes, currentMailboxId, selectMailbox, clearSearchAndFilters]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mailboxes.length > 0 && !currentMailboxId) {
|
||||
const own = ownMailboxes(mailboxes);
|
||||
|
||||
Reference in New Issue
Block a user