mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: keep a composer left open across an account switch from saving or deleting drafts in the other account
This commit is contained in:
@@ -298,6 +298,8 @@
|
||||
"group_chip": "{name} ({count})",
|
||||
"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_title": "Account switched",
|
||||
"no_recipient": "(no recipient)",
|
||||
"no_sent_folder": "No Sent folder found",
|
||||
"permission_camera": "Camera permission is required to take a photo.",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { composerLoginMoved } from '../draft-login';
|
||||
|
||||
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);
|
||||
// Offline cold start: the client is not connected yet.
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-a', connectedAccountId: 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);
|
||||
// Mid-switch: the registry still names A, the client already serves B.
|
||||
expect(composerLoginMoved('acc-a', { activeAccountId: 'acc-a', connectedAccountId: 'acc-b' })).toBe(true);
|
||||
});
|
||||
|
||||
it('never blocks a composer opened without a login', () => {
|
||||
expect(composerLoginMoved(null, { activeAccountId: 'acc-b', connectedAccountId: 'acc-b' })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// The composer writes through the active login: autosave creates a version
|
||||
// and destroys the previous one by id, Discard destroys the autosaved draft,
|
||||
// Send flags the original `$answered`. A notification or permalink for
|
||||
// another account can switch logins while the composer stays open under the
|
||||
// message it opens. Back in the composer, those writes went to the other
|
||||
// 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.
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the composer opened in `owner` would now write to another login.
|
||||
* A client that is not connected yet (a cold start offline) still counts as
|
||||
* the owner's while the registry says so: its writes cannot go anywhere else.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
findComposeIdentityId, findDraftIdentityId, resolveComposeAccountEmail, resolveReplyIdentity,
|
||||
} from '../lib/reply-identity';
|
||||
import { shouldBlockEditorRemoteImages } from '../lib/editor-html';
|
||||
import { composerLoginMoved } from '../lib/draft-login';
|
||||
import {
|
||||
initialDraftPartBlobState, liveBlobId, rebaseDraftPartBlobs, withSnapshotBody,
|
||||
} from '../lib/draft-part-blobs';
|
||||
@@ -1122,6 +1123,12 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
const lastSaveAtRef = React.useRef(0);
|
||||
const allowLeaveRef = React.useRef(false);
|
||||
const sendingRef = React.useRef(false);
|
||||
// The login this message was started in (see lib/draft-login).
|
||||
const ownerLoginRef = React.useRef(useAccountStore.getState().activeAccountId);
|
||||
const loginMoved = () => composerLoginMoved(ownerLoginRef.current, {
|
||||
activeAccountId: useAccountStore.getState().activeAccountId,
|
||||
connectedAccountId: jmapClient.connectedAccountId,
|
||||
});
|
||||
const [draftStatus, setDraftStatus] = React.useState<'idle' | 'saving' | 'saved' | 'failed'>('idle');
|
||||
const [savingDraft, setSavingDraft] = React.useState(false);
|
||||
|
||||
@@ -1336,6 +1343,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
const saveDraftOnce = async (opts: { live: boolean }): Promise<string | null> => {
|
||||
const identity = primaryIdentity;
|
||||
if (!identity || !draftsMailbox || sendingRef.current) return draftIdRef.current;
|
||||
// Another login is active: its Drafts and ids are not this draft's.
|
||||
if (loginMoved()) return draftIdRef.current;
|
||||
let html = latestRef.current.bodyHtml;
|
||||
if (opts.live && !plainTextMode) {
|
||||
try {
|
||||
@@ -1460,7 +1469,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
// A draft this session created by autosave goes with the discard; a
|
||||
// re-opened draft keeps its last saved version.
|
||||
const autosaved = draftIdRef.current;
|
||||
if (autosaved && !draft) {
|
||||
// In another login that id names another message; the draft stays.
|
||||
if (autosaved && !draft && !loginMoved()) {
|
||||
void (async () => {
|
||||
try { await inflightSaveRef.current; } catch { /* ignore */ }
|
||||
const id = draftIdRef.current ?? autosaved;
|
||||
@@ -2109,6 +2119,18 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
|
||||
const performSendInner = async (holdForSeconds?: number, scheduledAt?: Date) => {
|
||||
if (!primaryIdentity || !sentMailbox) return;
|
||||
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_body',
|
||||
'You started this message in {account}. Switch back to that account to send it.',
|
||||
{ account: owner?.email || owner?.displayName || '' },
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the body straight from the editor DOM at send time. The `change`
|
||||
// messages that feed `bodyHtml` are async and best-effort — trusting them
|
||||
|
||||
Reference in New Issue
Block a user