mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: let the composer send and save again in the account it was opened in
This commit is contained in:
@@ -299,6 +299,7 @@
|
||||
"identity_unavailable": "Identity unavailable",
|
||||
"image_load_failed": "Could not load image",
|
||||
"login_switched_body": "You started this message in {account}. Switch back to that account to send it.",
|
||||
"login_switched_save_body": "You started this message in {account}. Switch back to that account to save it.",
|
||||
"login_switched_title": "Account switched",
|
||||
"no_recipient": "(no recipient)",
|
||||
"no_sent_folder": "No Sent folder found",
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { composerLoginMoved } from '../draft-login';
|
||||
import { composerLoginMoved, servedLoginOf } from '../draft-login';
|
||||
import { generateAccountId } from '../account-utils';
|
||||
|
||||
// Real formats: the registry names logins `username@host`, the session names
|
||||
// JMAP accounts with short ids that never equal a registry id.
|
||||
const A = generateAccountId('usera@example.org', 'https://mail.example.org');
|
||||
const B = generateAccountId('userb@example.org', 'https://other.example.net');
|
||||
const client = (username: string, serverUrl: string, isConnected = true) => ({ username, serverUrl, isConnected });
|
||||
|
||||
describe('servedLoginOf', () => {
|
||||
it('names the login a client serves by its registry id, not its JMAP account id', () => {
|
||||
expect(servedLoginOf(client('usera@example.org', 'https://mail.example.org'))).toBe(A);
|
||||
expect(servedLoginOf(client('usera@example.org', 'https://mail.example.org', false))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('composerLoginMoved', () => {
|
||||
it('stays put while the login the composer opened in is active', () => {
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-a', connectedAccountId: 'acc-a' })).toBe(false);
|
||||
expect(composerLoginMoved(A, { activeAccountId: A, servedLogin: servedLoginOf(client('usera@example.org', 'https://mail.example.org')) })).toBe(false);
|
||||
// Offline cold start: the client is not connected yet.
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-a', connectedAccountId: null })).toBe(false);
|
||||
expect(composerLoginMoved(A, { activeAccountId: A, servedLogin: null })).toBe(false);
|
||||
});
|
||||
|
||||
it('moves once another login is active or the client serves another one', () => {
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-b', connectedAccountId: 'acc-b' })).toBe(true);
|
||||
expect(composerLoginMoved(A, { activeAccountId: B, servedLogin: B })).toBe(true);
|
||||
// Mid-switch: the registry still names A, the client already serves B.
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-a', connectedAccountId: 'acc-b' })).toBe(true);
|
||||
expect(composerLoginMoved(A, { activeAccountId: A, servedLogin: servedLoginOf(client('userb@example.org', 'https://other.example.net')) })).toBe(true);
|
||||
});
|
||||
|
||||
it('never blocks a composer opened without a login', () => {
|
||||
expect(composerLoginMoved(null, { activeAccountId: 'acc-b', connectedAccountId: 'acc-b' })).toBe(false);
|
||||
expect(composerLoginMoved(null, { activeAccountId: B, servedLogin: B })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+15
-3
@@ -6,12 +6,24 @@
|
||||
// login, where the same ids name other messages: an autosave or a discard
|
||||
// destroyed one of them (webmail 54b197080, d72bff95f). So the composer
|
||||
// remembers the login it was opened in and writes nothing once that changed.
|
||||
//
|
||||
// Logins are registry ids (`username@host`, lib/account-utils). The JMAP
|
||||
// account id the session names ('b', 'c', ...) is another thing: it repeats
|
||||
// across servers and never equals a registry id.
|
||||
|
||||
import { generateAccountId } from './account-utils';
|
||||
|
||||
export interface ComposerLogins {
|
||||
/** The registry's active account now. */
|
||||
activeAccountId: string | null;
|
||||
/** The account the JMAP client is connected to now; null while it is not connected. */
|
||||
connectedAccountId: string | null;
|
||||
/** The login (registry id) the JMAP client serves now; null while it is not connected. */
|
||||
servedLogin: string | null;
|
||||
}
|
||||
|
||||
/** The registry id of the login a client serves, or null while it serves none. */
|
||||
export function servedLoginOf(client: { isConnected: boolean; username: string | null; serverUrl: string | null }): string | null {
|
||||
if (!client.isConnected || !client.username || !client.serverUrl) return null;
|
||||
return generateAccountId(client.username, client.serverUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,5 +34,5 @@ export interface ComposerLogins {
|
||||
export function composerLoginMoved(owner: string | null | undefined, now: ComposerLogins): boolean {
|
||||
if (!owner) return false;
|
||||
if (now.activeAccountId !== owner) return true;
|
||||
return now.connectedAccountId != null && now.connectedAccountId !== owner;
|
||||
return now.servedLogin != null && now.servedLogin !== owner;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
findComposeIdentityId, findDraftIdentityId, resolveComposeAccountEmail, resolveReplyIdentity,
|
||||
} from '../lib/reply-identity';
|
||||
import { shouldBlockEditorRemoteImages } from '../lib/editor-html';
|
||||
import { composerLoginMoved } from '../lib/draft-login';
|
||||
import { composerLoginMoved, servedLoginOf } from '../lib/draft-login';
|
||||
import {
|
||||
initialDraftPartBlobState, liveBlobId, rebaseDraftPartBlobs, withSnapshotBody,
|
||||
} from '../lib/draft-part-blobs';
|
||||
@@ -1127,7 +1127,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
const ownerLoginRef = React.useRef(useAccountStore.getState().activeAccountId);
|
||||
const loginMoved = () => composerLoginMoved(ownerLoginRef.current, {
|
||||
activeAccountId: useAccountStore.getState().activeAccountId,
|
||||
connectedAccountId: jmapClient.connectedAccountId,
|
||||
servedLogin: servedLoginOf(jmapClient),
|
||||
});
|
||||
const [draftStatus, setDraftStatus] = React.useState<'idle' | 'saving' | 'saved' | 'failed'>('idle');
|
||||
const [savingDraft, setSavingDraft] = React.useState(false);
|
||||
@@ -1443,6 +1443,20 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
Alert.alert(t('email_composer.save_failed', 'Failed to save'), draftUnsavableReason);
|
||||
return;
|
||||
}
|
||||
// Another login is active: saving writes nothing (saveDraftOnce), so
|
||||
// closing now would drop the text as if it had been saved.
|
||||
if (loginMoved()) {
|
||||
const owner = ownerLoginRef.current ? useAccountStore.getState().getAccountById(ownerLoginRef.current) : undefined;
|
||||
Alert.alert(
|
||||
t('email_composer.login_switched_title', 'Account switched'),
|
||||
t(
|
||||
'email_composer.login_switched_save_body',
|
||||
'You started this message in {account}. Switch back to that account to save it.',
|
||||
{ account: owner?.email || owner?.displayName || '' },
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSavingDraft(true);
|
||||
try {
|
||||
await saveDraft({ live: true });
|
||||
|
||||
Reference in New Issue
Block a user