fix(mobile): fail a fast DM open that leaves the DM hidden

Reopening a hidden DM unhides it, but the relay accepts the open even
when publishing that visibility snapshot fails. The fast open path now
reads the caller's visibility snapshot alongside the DM's metadata and
roster and fails if the DM is still hidden, so the sheet shows an error
and Done can retry instead of opening a DM the sidebar keeps hidden.

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-27 21:50:16 -07:00
parent 8d820e4f62
commit a352ce1fd4
3 changed files with 60 additions and 11 deletions

View File

@@ -255,21 +255,27 @@ Future<Set<String>> _fetchHiddenDmIds(
String myPk,
) async {
try {
final events = await session.fetchHistory(NostrFilters.hiddenDms(myPk));
if (events.isEmpty) return const {};
NostrEvent latest = events.first;
for (final event in events.skip(1)) {
if (event.createdAt > latest.createdAt) latest = event;
}
return {
for (final tag in latest.tags)
if (tag.length >= 2 && tag[0] == 'h') tag[1],
};
return hiddenDmIdsFromEvents(
await session.fetchHistory(NostrFilters.hiddenDms(myPk)),
);
} catch (_) {
return const {};
}
}
/// The DM ids hidden by the newest of the caller's NIP-DV snapshots.
Set<String> hiddenDmIdsFromEvents(List<NostrEvent> events) {
if (events.isEmpty) return const {};
NostrEvent latest = events.first;
for (final event in events.skip(1)) {
if (event.createdAt > latest.createdAt) latest = event;
}
return {
for (final tag in latest.tags)
if (tag.length >= 2 && tag[0] == 'h') tag[1],
};
}
Future<List<NostrEvent>> _fetchHuddleStarts(
RelaySessionNotifier session,
List<String> parentChannelIds,

View File

@@ -145,7 +145,20 @@ class ChannelActions {
}
if (waitForChannelList) return _refreshChannelsAndRead(channelId);
unawaited(_ref.read(channelsProvider.notifier).refresh());
return await findJoinedChannel(channelId) ??
// Reopening a hidden DM unhides it, but the relay still accepts the open
// when that visibility snapshot fails. Check it here so the failure shows
// and Done can retry, rather than leaving the DM hidden from the sidebar.
final pk = _currentPubkey;
final hiddenRequest = pk == null
? Future.value(const <NostrEvent>[])
: _session.fetchHistory(NostrFilters.hiddenDms(pk));
final channelRequest = findJoinedChannel(channelId);
await Future.wait(eagerError: true, [hiddenRequest, channelRequest]);
_ensureCommunityValid();
if (hiddenDmIdsFromEvents(await hiddenRequest).contains(channelId)) {
throw StateError('DM was opened but is still hidden');
}
return await channelRequest ??
(throw StateError('DM was opened but is not visible yet'));
}

View File

@@ -67,6 +67,16 @@ void main() {
throwsStateError,
);
// A reopen whose unhide snapshot the relay failed to publish fails
// visibly instead of opening a DM the sidebar still hides.
session.member = true;
session.hidden = {_dmId};
await expectLater(
actions.openDm(pubkeys: [_peer], waitForChannelList: false),
throwsStateError,
);
session.hidden = {};
// Hidden-DM resurfacing keeps waiting for the list that clears its
// hidden state, so it cannot outrun its own retry coalescing.
session.member = true;
@@ -95,6 +105,7 @@ class _DmSession extends RelaySessionNotifier {
_DmSession(this.self);
final String self;
bool member = true;
Set<String> hidden = {};
void Function()? onRead;
final published = <NostrEvent>[];
Completer<List<NostrEvent>> stalledList = Completer();
@@ -140,6 +151,25 @@ class _DmSession extends RelaySessionNotifier {
NostrFilter filter, {
Duration timeout = const Duration(seconds: 8),
}) async {
if (filter.kinds.single == EventKind.dmVisibility) {
expect(filter.tags, {
'#p': [self],
});
return [
NostrEvent(
id: 'visibility',
pubkey: 'relay',
createdAt: 1,
kind: EventKind.dmVisibility,
tags: [
['p', self],
for (final id in hidden) ['h', id],
],
content: '',
sig: '',
),
];
}
expect(filter.tags, {
'#d': [_dmId],
});