mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix tablet navigation and reaction rollback
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -421,7 +421,7 @@ class ActivityPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _WideActivityDetail extends StatelessWidget {
|
||||
class _WideActivityDetail extends HookWidget {
|
||||
final InboxItem? item;
|
||||
final Channel? channel;
|
||||
final String? initialMessageId;
|
||||
@@ -440,13 +440,17 @@ class _WideActivityDetail extends StatelessWidget {
|
||||
return const _WideActivityEmptyDetail();
|
||||
}
|
||||
|
||||
return Navigator(
|
||||
key: ValueKey('wide-activity-detail-${item!.id}'),
|
||||
onGenerateRoute: (_) => MaterialPageRoute<void>(
|
||||
builder: (_) => ChannelDetailPage(
|
||||
channel: channel!,
|
||||
initialMessageId: initialMessageId,
|
||||
initialThreadRootId: initialThreadRootId,
|
||||
final navigatorKey = useMemoized(GlobalKey<NavigatorState>.new, [item!.id]);
|
||||
return NavigatorPopHandler(
|
||||
onPopWithResult: (_) => navigatorKey.currentState?.maybePop(),
|
||||
child: Navigator(
|
||||
key: navigatorKey,
|
||||
onGenerateRoute: (_) => MaterialPageRoute<void>(
|
||||
builder: (_) => ChannelDetailPage(
|
||||
channel: channel!,
|
||||
initialMessageId: initialMessageId,
|
||||
initialThreadRootId: initialThreadRootId,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../shared/relay/relay.dart';
|
||||
import 'pending_local_messages_provider.dart';
|
||||
import 'pending_local_aux_events_provider.dart';
|
||||
import 'channel_window.dart';
|
||||
import 'thread_replies_provider.dart';
|
||||
|
||||
@@ -105,6 +106,7 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
final history = await _fetchNewestHistory(session);
|
||||
if (!_isCurrentInit(initVersion)) return;
|
||||
_confirmLocalMessages(history.map((event) => event.id));
|
||||
_confirmLocalAuxEvents(history.map((event) => event.id));
|
||||
|
||||
final existing = state.value ?? const <NostrEvent>[];
|
||||
final existingIds = existing.map((event) => event.id).toSet();
|
||||
@@ -213,6 +215,9 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
if (authoritative && event.threadReference.parentId == null) {
|
||||
_confirmLocalMessages([event.id]);
|
||||
}
|
||||
if (authoritative && EventKind.channelAuxEventKinds.contains(event.kind)) {
|
||||
_confirmLocalAuxEvents([event.id]);
|
||||
}
|
||||
if (_usingChannelWindow) {
|
||||
_handleWindowLiveEvent(event);
|
||||
} else {
|
||||
@@ -282,6 +287,12 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
.confirm(eventIds);
|
||||
}
|
||||
|
||||
void _confirmLocalAuxEvents(Iterable<String> eventIds) {
|
||||
ref
|
||||
.read(pendingLocalAuxEventsProvider(channelId).notifier)
|
||||
.confirm(eventIds);
|
||||
}
|
||||
|
||||
/// Adds a just-signed outgoing message before the relay acknowledges it.
|
||||
/// The live relay echo is deduplicated by event id.
|
||||
void addLocalMessage(NostrEvent event) {
|
||||
@@ -327,11 +338,16 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
'Expected a channel auxiliary event.',
|
||||
);
|
||||
}
|
||||
ref.read(pendingLocalAuxEventsProvider(channelId).notifier).add(event);
|
||||
_handleLiveEvent(event, authoritative: false);
|
||||
}
|
||||
|
||||
/// Removes an optimistic auxiliary event when its publish is rejected.
|
||||
void removeLocalAuxEvent(String eventId) {
|
||||
final pending = ref
|
||||
.read(pendingLocalAuxEventsProvider(channelId).notifier)
|
||||
.take(eventId);
|
||||
if (pending == null) return;
|
||||
final nextAux = _windowStore.liveAux
|
||||
.where((event) => event.id != eventId)
|
||||
.toList();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../shared/relay/relay.dart';
|
||||
|
||||
/// Signed local auxiliary events whose publish has not yet been corroborated
|
||||
/// by an authoritative relay EVENT or query result.
|
||||
class PendingLocalAuxEventsNotifier extends Notifier<Map<String, NostrEvent>> {
|
||||
final String channelId;
|
||||
|
||||
PendingLocalAuxEventsNotifier(this.channelId);
|
||||
|
||||
@override
|
||||
Map<String, NostrEvent> build() => const {};
|
||||
|
||||
void add(NostrEvent event) {
|
||||
state = {...state, event.id: event};
|
||||
}
|
||||
|
||||
NostrEvent? take(String eventId) {
|
||||
final event = state[eventId];
|
||||
if (event == null) return null;
|
||||
final next = {...state}..remove(eventId);
|
||||
state = next;
|
||||
return event;
|
||||
}
|
||||
|
||||
void confirm(Iterable<String> eventIds) {
|
||||
final confirmed = eventIds.toSet();
|
||||
if (!state.keys.any(confirmed.contains)) return;
|
||||
state = {
|
||||
for (final entry in state.entries)
|
||||
if (!confirmed.contains(entry.key)) entry.key: entry.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final pendingLocalAuxEventsProvider =
|
||||
NotifierProvider.family<
|
||||
PendingLocalAuxEventsNotifier,
|
||||
Map<String, NostrEvent>,
|
||||
String
|
||||
>(PendingLocalAuxEventsNotifier.new);
|
||||
@@ -745,17 +745,23 @@ class _WideChannelDestination extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _WideChannelContent extends StatelessWidget {
|
||||
class _WideChannelContent extends HookWidget {
|
||||
final Channel channel;
|
||||
|
||||
const _WideChannelContent({required this.channel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
key: ValueKey('wide-channel-detail-${channel.id}'),
|
||||
onGenerateRoute: (_) => MaterialPageRoute<void>(
|
||||
builder: (_) => ChannelDetailPage(channel: channel),
|
||||
final navigatorKey = useMemoized(GlobalKey<NavigatorState>.new, [
|
||||
channel.id,
|
||||
]);
|
||||
return NavigatorPopHandler(
|
||||
onPopWithResult: (_) => navigatorKey.currentState?.maybePop(),
|
||||
child: Navigator(
|
||||
key: navigatorKey,
|
||||
onGenerateRoute: (_) => MaterialPageRoute<void>(
|
||||
builder: (_) => ChannelDetailPage(channel: channel),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:buzz/features/channels/channel_messages_provider.dart';
|
||||
import 'package:buzz/features/channels/pending_local_aux_events_provider.dart';
|
||||
import 'package:buzz/features/channels/pending_local_messages_provider.dart';
|
||||
import 'package:buzz/features/channels/thread_replies_provider.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
@@ -248,6 +249,48 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'authoritative auxiliary echo prevents a later publish rollback',
|
||||
() async {
|
||||
final relaySession = _RecordingRelaySessionNotifier(
|
||||
queryResults: [
|
||||
[_event(id: 'message', createdAt: 10), _bounds()],
|
||||
],
|
||||
);
|
||||
final container = _buildContainer(relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(channelMessagesProvider(_channelId));
|
||||
await relaySession.subscribed;
|
||||
await _pumpEventQueue();
|
||||
final notifier = container.read(
|
||||
channelMessagesProvider(_channelId).notifier,
|
||||
);
|
||||
final reaction = _auxEvent(
|
||||
id: 'reaction',
|
||||
kind: EventKind.reaction,
|
||||
createdAt: 20,
|
||||
);
|
||||
|
||||
notifier.addLocalAuxEvent(reaction);
|
||||
relaySession.emit(reaction);
|
||||
await _pumpEventQueue();
|
||||
|
||||
expect(
|
||||
container.read(pendingLocalAuxEventsProvider(_channelId)),
|
||||
isEmpty,
|
||||
);
|
||||
notifier.removeLocalAuxEvent(reaction.id);
|
||||
expect(
|
||||
container
|
||||
.read(channelMessagesProvider(_channelId))
|
||||
.value
|
||||
?.map((event) => event.id),
|
||||
['message', 'reaction'],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('adds and rolls back a local message in the channel window', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier(
|
||||
queryResults: [
|
||||
@@ -663,6 +706,25 @@ NostrEvent _summary({
|
||||
);
|
||||
}
|
||||
|
||||
NostrEvent _auxEvent({
|
||||
required String id,
|
||||
required int kind,
|
||||
required int createdAt,
|
||||
}) {
|
||||
return NostrEvent(
|
||||
id: id,
|
||||
pubkey: 'alice',
|
||||
createdAt: createdAt,
|
||||
kind: kind,
|
||||
tags: const [
|
||||
['h', _channelId],
|
||||
['e', 'message'],
|
||||
],
|
||||
content: '+',
|
||||
sig: 'sig',
|
||||
);
|
||||
}
|
||||
|
||||
NostrEvent _bounds({
|
||||
bool hasMore = false,
|
||||
int? cursorCreatedAt,
|
||||
|
||||
Reference in New Issue
Block a user