mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +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",
|
"identity_unavailable": "Identity unavailable",
|
||||||
"image_load_failed": "Could not load image",
|
"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_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",
|
"login_switched_title": "Account switched",
|
||||||
"no_recipient": "(no recipient)",
|
"no_recipient": "(no recipient)",
|
||||||
"no_sent_folder": "No Sent folder found",
|
"no_sent_folder": "No Sent folder found",
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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', () => {
|
describe('composerLoginMoved', () => {
|
||||||
it('stays put while the login the composer opened in is active', () => {
|
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.
|
// 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', () => {
|
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.
|
// 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', () => {
|
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
|
// login, where the same ids name other messages: an autosave or a discard
|
||||||
// destroyed one of them (webmail 54b197080, d72bff95f). So the composer
|
// destroyed one of them (webmail 54b197080, d72bff95f). So the composer
|
||||||
// remembers the login it was opened in and writes nothing once that changed.
|
// 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 {
|
export interface ComposerLogins {
|
||||||
/** The registry's active account now. */
|
/** The registry's active account now. */
|
||||||
activeAccountId: string | null;
|
activeAccountId: string | null;
|
||||||
/** The account the JMAP client is connected to now; null while it is not connected. */
|
/** The login (registry id) the JMAP client serves now; null while it is not connected. */
|
||||||
connectedAccountId: string | null;
|
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 {
|
export function composerLoginMoved(owner: string | null | undefined, now: ComposerLogins): boolean {
|
||||||
if (!owner) return false;
|
if (!owner) return false;
|
||||||
if (now.activeAccountId !== owner) return true;
|
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,
|
findComposeIdentityId, findDraftIdentityId, resolveComposeAccountEmail, resolveReplyIdentity,
|
||||||
} from '../lib/reply-identity';
|
} from '../lib/reply-identity';
|
||||||
import { shouldBlockEditorRemoteImages } from '../lib/editor-html';
|
import { shouldBlockEditorRemoteImages } from '../lib/editor-html';
|
||||||
import { composerLoginMoved } from '../lib/draft-login';
|
import { composerLoginMoved, servedLoginOf } from '../lib/draft-login';
|
||||||
import {
|
import {
|
||||||
initialDraftPartBlobState, liveBlobId, rebaseDraftPartBlobs, withSnapshotBody,
|
initialDraftPartBlobState, liveBlobId, rebaseDraftPartBlobs, withSnapshotBody,
|
||||||
} from '../lib/draft-part-blobs';
|
} from '../lib/draft-part-blobs';
|
||||||
@@ -1127,7 +1127,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
const ownerLoginRef = React.useRef(useAccountStore.getState().activeAccountId);
|
const ownerLoginRef = React.useRef(useAccountStore.getState().activeAccountId);
|
||||||
const loginMoved = () => composerLoginMoved(ownerLoginRef.current, {
|
const loginMoved = () => composerLoginMoved(ownerLoginRef.current, {
|
||||||
activeAccountId: useAccountStore.getState().activeAccountId,
|
activeAccountId: useAccountStore.getState().activeAccountId,
|
||||||
connectedAccountId: jmapClient.connectedAccountId,
|
servedLogin: servedLoginOf(jmapClient),
|
||||||
});
|
});
|
||||||
const [draftStatus, setDraftStatus] = React.useState<'idle' | 'saving' | 'saved' | 'failed'>('idle');
|
const [draftStatus, setDraftStatus] = React.useState<'idle' | 'saving' | 'saved' | 'failed'>('idle');
|
||||||
const [savingDraft, setSavingDraft] = React.useState(false);
|
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);
|
Alert.alert(t('email_composer.save_failed', 'Failed to save'), draftUnsavableReason);
|
||||||
return;
|
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);
|
setSavingDraft(true);
|
||||||
try {
|
try {
|
||||||
await saveDraft({ live: true });
|
await saveDraft({ live: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user