fix(mobile): skip a starter whose id an archived channel holds

The relay refuses a kind:9007 whose `h` is an archived channel with
`invalid: channel is archived`, before its duplicate check. Desktop and
mobile create a starter at the same fixed id, so an archived starter there
left invite setup in permanent recovery. Treat that refusal as an archived
starter: join a live namesake if the reload shows one, else skip it.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-11-02 09:47:08 -05:00
parent dc6344e8b7
commit 1c88d48f16
2 changed files with 27 additions and 1 deletions

View File

@@ -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`.

View File

@@ -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;