mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
fix: leave archive off the android widget when the organisation turns archiving off
This commit is contained in:
@@ -178,4 +178,11 @@ describe('widget archive', () => {
|
||||
calls.moves[0].resolve(true);
|
||||
await tap;
|
||||
});
|
||||
|
||||
it('does nothing for a tap on an older drawing once the organisation turned archiving off', async () => {
|
||||
await saveSnapshot({ ...snapshotOf(server.inbox), archiveEnabled: false });
|
||||
await handleWidgetAction('archive', { id: 'm1', accountId: 'acc' }, 7);
|
||||
expect(calls.moves).toEqual([]);
|
||||
expect(await shown()).toEqual(['m1', 'm2', 'm3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { readArchiveEnabled } from '../build';
|
||||
|
||||
// The organisation's archiveActionEnabled gate reaches the widgets through
|
||||
// the policy the app cached (stores/policy-store.ts, key org-policy-cache).
|
||||
|
||||
const WEBMAIL = 'https://webmail.example.org';
|
||||
const account = { id: 'acc', email: 'ada@example.org', webmailUrl: WEBMAIL };
|
||||
|
||||
function cache(features: Record<string, unknown>, webmailUrl = WEBMAIL, scopes: unknown[] = []) {
|
||||
return AsyncStorage.setItem('org-policy-cache', JSON.stringify({
|
||||
state: {
|
||||
byAccount: {
|
||||
acc: {
|
||||
policy: { restrictions: {}, features, defaults: {}, themePolicy: {}, scopes },
|
||||
partial: false,
|
||||
fetchedAt: 1,
|
||||
webmailUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await AsyncStorage.clear();
|
||||
});
|
||||
|
||||
describe('readArchiveEnabled', () => {
|
||||
it('is off when the cached policy turns archiving off', async () => {
|
||||
await cache({ archiveActionEnabled: false });
|
||||
expect(await readArchiveEnabled(account)).toBe(false);
|
||||
});
|
||||
|
||||
it('is on when the policy allows it, without a policy and without a webmail', async () => {
|
||||
expect(await readArchiveEnabled(account)).toBe(true);
|
||||
await cache({ archiveActionEnabled: true });
|
||||
expect(await readArchiveEnabled(account)).toBe(true);
|
||||
await cache({ archiveActionEnabled: false });
|
||||
expect(await readArchiveEnabled({ ...account, webmailUrl: undefined })).toBe(true);
|
||||
expect(await readArchiveEnabled(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores a policy cached from another webmail', async () => {
|
||||
await cache({ archiveActionEnabled: false }, 'https://elsewhere.example.org');
|
||||
expect(await readArchiveEnabled(account)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -159,6 +159,20 @@ describe('widget layouts', () => {
|
||||
for (const w of catalog.widgets) build(w.name, { ...sample, images }, w.cols, w.rows);
|
||||
});
|
||||
|
||||
it('leaves Archive off the triage card when the organisation turned archiving off', () => {
|
||||
const actions = (s: WidgetSnapshot, cols: number) => {
|
||||
const trees = renderFor('TriageWidget', s, { width: dp(cols), height: dp(2), widgetId: 1 }, {}, PREVIEW_NOW);
|
||||
const tree = 'light' in trees ? trees.light : trees;
|
||||
return [...JSON.stringify(buildWidgetTree(tree)).matchAll(/"clickAction":"(\w+)"/g)].map((m) => m[1]);
|
||||
};
|
||||
expect(actions(sample, 4)).toContain('archive');
|
||||
for (const cols of [3, 4, 5]) {
|
||||
const shown = actions({ ...sample, archiveEnabled: false }, cols);
|
||||
expect(shown).not.toContain('archive');
|
||||
expect(shown).toContain('trash');
|
||||
}
|
||||
});
|
||||
|
||||
it('builds the triage card on a message that is gone', () => {
|
||||
build('TriageWidget', sample, 4, 2, { triageId: 'no-such-message' });
|
||||
build('TriageWidget', { ...sample, mail: { ...sample.mail, inbox: [] } }, 4, 2, { triageId: 'm1' });
|
||||
|
||||
@@ -94,7 +94,13 @@ export async function handleWidgetAction(name: string, data: Data, widgetId: num
|
||||
case 'markRead': {
|
||||
const id = str(data.id);
|
||||
if (!id) return;
|
||||
const subject = (await currentView()).mail.inbox.find((m) => m.id === id)?.subject ?? '';
|
||||
const view = await currentView();
|
||||
// A drawing from before the organisation turned archiving off.
|
||||
if (name === 'archive' && view.archiveEnabled === false) {
|
||||
await redrawAll();
|
||||
return;
|
||||
}
|
||||
const subject = view.mail.inbox.find((m) => m.id === id)?.subject ?? '';
|
||||
// The triage card moves on to the next unread message, not back to
|
||||
// the first one.
|
||||
if (name !== 'markRead') await moveTriagePointer(widgetId, id);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getEventEndDate, getEventStartDate } from '../lib/calendar-utils';
|
||||
import { getUserParticipantId, getUserStatus } from '../lib/calendar-participants';
|
||||
import { getBirthday, getContactDisplayName, getDateParts } from '../lib/contact-utils';
|
||||
import { notificationLocale } from '../lib/push-background-task';
|
||||
import { isFeatureEnabled, resolvePolicy, sanitizePolicy } from '../lib/org-policy';
|
||||
import { useSettingsStore } from '../stores/settings-store';
|
||||
import { useKeywordsStore, keywordToken } from '../stores/keywords-store';
|
||||
import { LIGHT_COLORS } from '../theme/tokens';
|
||||
@@ -46,6 +47,30 @@ interface RegistryAccount {
|
||||
displayName?: string;
|
||||
username?: string;
|
||||
avatarColor?: string;
|
||||
webmailUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the account's organisation lets it archive (policy
|
||||
* archiveActionEnabled), from the policy the app cached for it
|
||||
* (stores/policy-store.ts, which this runtime may not have loaded). On
|
||||
* without a webmail or a cached policy from it.
|
||||
*/
|
||||
export async function readArchiveEnabled(account: RegistryAccount | undefined): Promise<boolean> {
|
||||
if (!account?.webmailUrl) return true;
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem('org-policy-cache');
|
||||
const byAccount = raw
|
||||
? (JSON.parse(raw) as { state?: { byAccount?: Record<string, { policy?: unknown; webmailUrl?: string }> } }).state?.byAccount
|
||||
: undefined;
|
||||
const cached = byAccount?.[account.id];
|
||||
const policy = cached?.webmailUrl === account.webmailUrl ? sanitizePolicy(cached.policy) : null;
|
||||
if (!policy) return true;
|
||||
const email = (account.email || account.username || '').trim().toLowerCase();
|
||||
return isFeatureEnabled(resolvePolicy(policy, { email, groups: [] }), 'archiveActionEnabled');
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readRegistry(): Promise<{ accounts: RegistryAccount[]; activeAccountId: string | null }> {
|
||||
@@ -390,6 +415,7 @@ async function buildSnapshot(previous: WidgetSnapshot): Promise<WidgetSnapshot>
|
||||
hour12: timeFormat === '12h',
|
||||
weekStart: calendarFirstDayOfWeek === 0 ? 0 : 1,
|
||||
activeAccountId,
|
||||
archiveEnabled: await readArchiveEnabled(accounts.find((a) => a.id === activeAccountId)),
|
||||
};
|
||||
if (previous.activeAccountId !== activeAccountId) {
|
||||
// Nothing of another account's data may linger in a widget.
|
||||
|
||||
@@ -112,9 +112,11 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, height, widgetId, lo
|
||||
trash: f.t('widgets.mail.delete', 'Delete'),
|
||||
reply: f.t('widgets.mail.reply', 'Reply'),
|
||||
};
|
||||
const room = width - 2 - 24 - 32 - 18;
|
||||
const allFit = buttonWidth(labels.archive) + buttonWidth(labels.trash) + buttonWidth(labels.reply) <= room;
|
||||
const replyFits = 2 * 32 + buttonWidth(labels.reply) <= room;
|
||||
// No Archive button when the organisation turned archiving off.
|
||||
const archive = s.archiveEnabled !== false;
|
||||
const room = width - 2 - 24 - 32 - (archive ? 18 : 12);
|
||||
const allFit = (archive ? buttonWidth(labels.archive) : 0) + buttonWidth(labels.trash) + buttonWidth(labels.reply) <= room;
|
||||
const replyFits = (archive ? 2 : 1) * 32 + buttonWidth(labels.reply) <= room;
|
||||
return (
|
||||
// The buttons draw 32dp tall in a 48dp tap area (slop 8), which takes the
|
||||
// gap above them and 8dp of the bottom padding.
|
||||
@@ -146,8 +148,10 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, height, widgetId, lo
|
||||
</FlexWidget>
|
||||
{notice ? <NoticeBar p={p} f={f} notice={notice} /> : null}
|
||||
<FlexWidget style={{ flexDirection: 'row', width: 'match_parent' }}>
|
||||
<Button p={p} label={allFit ? labels.archive : undefined} iconName="archive" flex={1} slop={8} click={action('archive', target)} />
|
||||
<Spacer size={6} horizontal />
|
||||
{archive ? (
|
||||
<Button p={p} label={allFit ? labels.archive : undefined} iconName="archive" flex={1} slop={8} click={action('archive', target)} />
|
||||
) : null}
|
||||
{archive ? <Spacer size={6} horizontal /> : null}
|
||||
<Button p={p} label={allFit ? labels.trash : undefined} iconName="trash" flex={1} slop={8} click={action('trash', target)} />
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} label={allFit || replyFits ? labels.reply : undefined} iconName="reply" variant="primary" flex={1} slop={8} click={open(links.reply(current))} />
|
||||
|
||||
@@ -200,6 +200,11 @@ export interface WidgetSnapshot {
|
||||
accounts: AccountSummary[];
|
||||
/** The signed-in account the single-account widgets show. */
|
||||
activeAccountId: string | null;
|
||||
/**
|
||||
* False when the active account's organisation turned archiving off
|
||||
* (policy archiveActionEnabled); absent counts as on.
|
||||
*/
|
||||
archiveEnabled?: boolean;
|
||||
mail: {
|
||||
folders: FolderCount[];
|
||||
inbox: MailItem[];
|
||||
|
||||
Reference in New Issue
Block a user