mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
fix(mobile): check mentions against the route's channel
A DM opened on the fast path is on screen before the channel list has it, but the composer's mention check looked the destination up in that list, so a selected mention failed or waited on the full reload. The channel page now hands the composer the channel it resolved, and the check uses it before falling back to the list. Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -888,6 +888,7 @@ class _ResolvedChannelDetailPage extends HookConsumerWidget {
|
||||
?assistant.pendingNotice,
|
||||
ComposeBar(
|
||||
channelId: channel.id,
|
||||
channel: resolvedChannel,
|
||||
focusNode: composerFocusNode,
|
||||
onFocusRestorerChanged: (restoreFocus) =>
|
||||
restoreComposerFocus.value = restoreFocus,
|
||||
|
||||
@@ -18,9 +18,14 @@ class ComposeBar extends HookConsumerWidget {
|
||||
/// Optional thread IDs for thread-scoped typing indicators.
|
||||
final String? threadHeadId;
|
||||
final String? rootId;
|
||||
|
||||
/// The destination as the route resolved it. Sends check mentions against
|
||||
/// it, so a DM opened before the channel list reloads can still send them.
|
||||
final Channel? channel;
|
||||
const ComposeBar({
|
||||
super.key,
|
||||
required this.channelId,
|
||||
this.channel,
|
||||
this.channelName = '',
|
||||
this.hintText,
|
||||
this.threadHeadId,
|
||||
@@ -575,6 +580,7 @@ class ComposeBar extends HookConsumerWidget {
|
||||
scan = await _scanNonMemberMentions(
|
||||
ref,
|
||||
channelId: channelId,
|
||||
routeChannel: channel,
|
||||
selectedMentions: selectedMentions,
|
||||
currentPubkey: currentPubkey,
|
||||
);
|
||||
|
||||
@@ -510,10 +510,13 @@ class _NonMemberMentionScan {
|
||||
|
||||
/// Resolves which mentioned identities are non-members, and whether this
|
||||
/// identity may add them (see [Channel.canAddMembers]). A DM's participant set
|
||||
/// is fixed at creation, so nobody outside it can be added.
|
||||
/// is fixed at creation, so nobody outside it can be added. [routeChannel],
|
||||
/// when given, stands in for the channel list, which may not have a just
|
||||
/// opened DM yet.
|
||||
Future<_NonMemberMentionScan> _scanNonMemberMentions(
|
||||
WidgetRef ref, {
|
||||
required String channelId,
|
||||
Channel? routeChannel,
|
||||
required List<MentionCandidate> selectedMentions,
|
||||
required String? currentPubkey,
|
||||
}) async {
|
||||
@@ -525,9 +528,11 @@ Future<_NonMemberMentionScan> _scanNonMemberMentions(
|
||||
);
|
||||
if (selectedMentions.isEmpty) return none;
|
||||
|
||||
final channel = (await ref.read(
|
||||
channelsProvider.future,
|
||||
)).firstWhere((candidate) => candidate.id == channelId);
|
||||
final channel =
|
||||
routeChannel ??
|
||||
(await ref.read(
|
||||
channelsProvider.future,
|
||||
)).firstWhere((candidate) => candidate.id == channelId);
|
||||
|
||||
// A DM decides who is inside it the way its send does: by current
|
||||
// membership, or by the channel metadata when membership is unavailable.
|
||||
|
||||
@@ -187,6 +187,8 @@ Widget _buildComposeBar({
|
||||
Future<List<ChannelMember>> Function()? loadMembers,
|
||||
List<AgentDirectoryEntry> relayAgents = const <AgentDirectoryEntry>[],
|
||||
List<Channel> channels = const <Channel>[],
|
||||
bool stallChannels = false,
|
||||
Channel? channel,
|
||||
List<ChannelMember> cachedMembers = const <ChannelMember>[],
|
||||
String? currentPubkey,
|
||||
bool? supportsShowingSystemContextMenu,
|
||||
@@ -240,7 +242,11 @@ Widget _buildComposeBar({
|
||||
if (appLifecycle != null) appLifecycleProvider.overrideWith(appLifecycle),
|
||||
savedPrefsProvider.overrideWithValue(_testPrefs),
|
||||
channelsProvider.overrideWith(
|
||||
() => _FakeChannelsNotifier(channels, cachedMembers: cachedMembers),
|
||||
() => _FakeChannelsNotifier(
|
||||
channels,
|
||||
cachedMembers: cachedMembers,
|
||||
stall: stallChannels,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
@@ -272,6 +278,7 @@ Widget _buildComposeBar({
|
||||
final composeBar = ComposeBar(
|
||||
key: ValueKey(composeBarKey),
|
||||
channelId: 'channel-1',
|
||||
channel: channel,
|
||||
threadHeadId: threadHeadId,
|
||||
focusNode: focusNode,
|
||||
onFocusRestorerChanged: onFocusRestorerChanged,
|
||||
@@ -638,17 +645,23 @@ class _FakeChannelsNotifier extends ChannelsNotifier {
|
||||
final List<Channel> _channels;
|
||||
final List<ChannelMember> _cachedMembers;
|
||||
|
||||
/// Keeps the channel list loading, like a slow full reload.
|
||||
final bool _stall;
|
||||
|
||||
_FakeChannelsNotifier(
|
||||
this._channels, {
|
||||
List<ChannelMember> cachedMembers = const [],
|
||||
}) : _cachedMembers = cachedMembers;
|
||||
bool stall = false,
|
||||
}) : _cachedMembers = cachedMembers,
|
||||
_stall = stall;
|
||||
|
||||
@override
|
||||
List<ChannelMember> cachedMembersForChannel(String channelId) =>
|
||||
channelId == 'channel-1' ? _cachedMembers : const [];
|
||||
|
||||
@override
|
||||
Future<List<Channel>> build() async => _channels;
|
||||
Future<List<Channel>> build() =>
|
||||
_stall ? Completer<List<Channel>>().future : Future.value(_channels);
|
||||
|
||||
@override
|
||||
Future<void> refresh({bool fetchDirectory = false}) async {
|
||||
@@ -4698,6 +4711,57 @@ void main() {
|
||||
expect(sentTags, contains(orderedEquals(['mention', maryJane.pubkey])));
|
||||
});
|
||||
|
||||
testWidgets('a DM opened before the channel list loads sends mentions', (
|
||||
tester,
|
||||
) async {
|
||||
final signer = nostr.Keys.generate();
|
||||
final pat = '1' * 64;
|
||||
List<String>? sentRecipients;
|
||||
List<List<String>>? sentTags;
|
||||
await tester.pumpWidget(
|
||||
_buildComposeBar(
|
||||
uploadService: _testUploadService(signer.nsec),
|
||||
currentPubkey: signer.public,
|
||||
members: [
|
||||
member(signer.public),
|
||||
ChannelMember(
|
||||
pubkey: pat,
|
||||
role: 'member',
|
||||
joinedAt: DateTime.fromMillisecondsSinceEpoch(1000),
|
||||
displayName: 'Pat Peer',
|
||||
),
|
||||
],
|
||||
// The route opened the DM from its own metadata and roster while
|
||||
// the full channel list is still loading.
|
||||
stallChannels: true,
|
||||
channel: _makeCurrentChannel(
|
||||
channelType: 'dm',
|
||||
participantPubkeys: [signer.public, pat],
|
||||
),
|
||||
onSend:
|
||||
(_, recipients, {mediaTags = const <List<String>>[]}) async {
|
||||
sentRecipients = recipients;
|
||||
sentTags = mediaTags;
|
||||
},
|
||||
),
|
||||
);
|
||||
await _expandComposer(tester);
|
||||
await tester.enterText(find.byType(TextField), '@Pat');
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Pat Peer'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(BuzzIcons.arrowUp));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Message not sent: could not check who is in this channel'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(sentRecipients, contains(pat));
|
||||
expect(sentTags, isNot(contains(orderedEquals(['mention', pat]))));
|
||||
});
|
||||
|
||||
testWidgets('a failed member check keeps the draft and Retry sends', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user