diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 2e5d8a7111..dc3fe9202b 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -157,7 +157,10 @@ class MobileInviteJoinRecovery implements InviteJoinRecovery { _ensureScopeCurrent(); } catch (error) { final nameTaken = _isChannelNameTakenError(error); - if (!nameTaken && !_isDuplicateChannelError(error)) rethrow; + final archivedAtId = _isArchivedChannelError(error); + if (!nameTaken && !archivedAtId && !_isDuplicateChannelError(error)) { + rethrow; + } _ensureScopeCurrent(); channels = await _loadChannels(); _ensureScopeCurrent(); @@ -168,6 +171,10 @@ class MobileInviteJoinRecovery implements InviteJoinRecovery { .firstOrNull ?? (nameTaken ? _findListedNamesake(channels, starter.slug) : null); if (holder == null) { + // The relay refuses a create at an archived channel's id before + // its duplicate check, and keeps that id, so the starter can never + // be created there. + if (archivedAtId) continue; // The directory omits archived channels, yet the relay still // keeps the id (desktop uses the same one), so a duplicate with // no match may be an archived starter. Read that id directly. @@ -228,6 +235,11 @@ bool _isUsableStarter(Channel channel) => bool _isDuplicateChannelError(Object error) => error.toString().contains('duplicate: channel already exists'); +/// The relay refuses any channel event whose `h` is an archived channel, +/// a create at that id included: `invalid: channel is archived`. +bool _isArchivedChannelError(Object error) => + error.toString().contains('invalid: channel is archived'); + /// The relay refuses creating an open channel whose name another listed /// channel already holds, ignoring case: /// `invalid: an open channel named "general" already exists`. diff --git a/mobile/test/features/invites/invite_join_provider_test.dart b/mobile/test/features/invites/invite_join_provider_test.dart index 94008e0230..c8e6ed4722 100644 --- a/mobile/test/features/invites/invite_join_provider_test.dart +++ b/mobile/test/features/invites/invite_join_provider_test.dart @@ -496,6 +496,20 @@ void main() { expect(archivedAtId.loads(), 2); expect(focusAfterArchived, welcomeId); + // The relay refuses a create at an archived channel's id with + // `channel is archived`; that starter is skipped without a lookup. + final refusedAsArchived = starterSetup( + channelsOnLoad: (_) => const [], + refusalError: 'invalid: channel is archived', + ); + final focusAfterRefusal = await refusedAsArchived.recovery + .ensureStarterChannels(); + expect(refusedAsArchived.created, ['general', 'welcome-everyone']); + expect(refusedAsArchived.lookups, isEmpty); + expect(refusedAsArchived.joined, isEmpty); + expect(refusedAsArchived.loads(), 2); + expect(focusAfterRefusal, welcomeId); + // The scope fence also holds across the direct lookup, checked on the // last starter so the next starter's own fence cannot mask it. var lookedUp = false;