mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(mobile): simplify and refine the quick actions menu (#8491)
Keeps the mobile + menu focused on Create channel and New direct message, with 8-point facing corners, a 6-point gap, and the active Home/Solo tab fill on iOS. Moves Browse channels directly above the channel list, including its empty state, preserving open-channel discovery, retry, and Join. Adds a cold-start journey through the real channel loader and join action with relay boundaries faked. Validation: mobile formatting/analysis and full mobile test suite; mutation check confirms the new journey fails when directory loading is removed. The original menu styling was tried on a physical iPhone; the replacement Browse entry has widget-journey coverage and has not yet been tried on device. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -2,6 +2,7 @@ import 'channel_unread.dart';
|
||||
import '../../shared/widgets/collapsible_section_body.dart';
|
||||
import '../../shared/widgets/compose_morph_icon.dart';
|
||||
import '../../shared/me/me_sync.dart';
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' show max, min, pi;
|
||||
@@ -77,7 +78,7 @@ part 'channels_page/quick_actions.dart';
|
||||
part 'channels_page/quick_actions_launcher.dart';
|
||||
part 'channels_page/header_actions.dart';
|
||||
|
||||
enum _QuickAction { createChannel, newDm, browseChannels }
|
||||
enum _QuickAction { createChannel, newDm }
|
||||
|
||||
const double _kChannelSectionInset = Grid.gutter;
|
||||
const double _kChannelLeadingWidth = 22.0;
|
||||
|
||||
@@ -254,6 +254,25 @@ class _SliverChannelsList extends HookConsumerWidget {
|
||||
),
|
||||
sliver: SliverList.list(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
key: const Key('browse-channels-button'),
|
||||
icon: const Icon(BuzzIcons.compass),
|
||||
label: const Text('Browse channels'),
|
||||
onPressed: () => showBuzzModalBottomSheet<void>(
|
||||
context: context,
|
||||
title: 'Browse channels',
|
||||
constraints: _quickActionSheetConstraints(context),
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (_) => const _BrowseChannelsSheet(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (visibleChannels.isEmpty)
|
||||
const _EmptyState()
|
||||
else ...[
|
||||
|
||||
@@ -7,7 +7,7 @@ const _kMorphCloseCurve = Cubic(0.22, 1, 0.36, 1);
|
||||
const double _kMorphOpenBounce = 0.14;
|
||||
const double _kMorphCloseBounce = 0.06;
|
||||
const double _kMorphClosedSize = 56;
|
||||
const double _kMorphOpenHeight = 216;
|
||||
const double _kMorphOpenHeight = 142;
|
||||
const double _kMorphOpenRadius = mobileTabBarHeight / 2;
|
||||
const double _kMorphSlide = 40;
|
||||
const double _kMorphScale = 0.97;
|
||||
@@ -300,23 +300,24 @@ class _QuickActionsMenu extends StatelessWidget {
|
||||
_QuickActionItem(
|
||||
icon: BuzzIcons.hash,
|
||||
title: 'Create channel',
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(_kQuickActionCardRadius),
|
||||
bottom: Radius.circular(8),
|
||||
),
|
||||
key: const Key('quick-action-create-channel-card'),
|
||||
onTap: () => onSelected(_QuickAction.createChannel),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
const SizedBox(height: Grid.xxs - 2),
|
||||
_QuickActionItem(
|
||||
icon: BuzzIcons.messagesSquare,
|
||||
title: 'New direct message',
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(8),
|
||||
bottom: Radius.circular(_kQuickActionCardRadius),
|
||||
),
|
||||
key: const Key('quick-action-new-dm-card'),
|
||||
onTap: () => onSelected(_QuickAction.newDm),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
_QuickActionItem(
|
||||
icon: BuzzIcons.compass,
|
||||
title: 'Browse channels',
|
||||
key: const Key('quick-action-browse-channels-card'),
|
||||
onTap: () => onSelected(_QuickAction.browseChannels),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -327,12 +328,14 @@ class _QuickActionItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
const _QuickActionItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
required this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -340,7 +343,10 @@ class _QuickActionItem extends StatelessWidget {
|
||||
final isIos = defaultTargetPlatform == TargetPlatform.iOS;
|
||||
final foreground = isIos ? context.colors.onSurface : Colors.white;
|
||||
final background = isIos
|
||||
? context.controlSurface
|
||||
? Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.05),
|
||||
context.controlSurface,
|
||||
)
|
||||
: Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.1),
|
||||
const Color(0xFF262626),
|
||||
@@ -349,9 +355,7 @@ class _QuickActionItem extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: Material(
|
||||
color: background,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(_kQuickActionCardRadius),
|
||||
),
|
||||
shape: RoundedSuperellipseBorder(borderRadius: borderRadius),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
|
||||
@@ -121,15 +121,6 @@ class ChannelQuickActionsLauncher extends HookConsumerWidget {
|
||||
if (opened != null && context.mounted) {
|
||||
await openChannel(opened);
|
||||
}
|
||||
case _QuickAction.browseChannels:
|
||||
await showBuzzModalBottomSheet<void>(
|
||||
context: context,
|
||||
title: 'Browse channels',
|
||||
constraints: _quickActionSheetConstraints(context),
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (_) => const _BrowseChannelsSheet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ import 'package:buzz/shared/widgets/skeleton.dart';
|
||||
import '../../helpers/thread_ids.dart';
|
||||
|
||||
part 'channels_page_test/presence_tests.dart';
|
||||
part 'channels_page_test/discovery_journey_tests.dart';
|
||||
part 'channels_page_test/unread_readiness_tests.dart';
|
||||
|
||||
void main() {
|
||||
@@ -157,6 +158,7 @@ void main() {
|
||||
}
|
||||
|
||||
_communityUnreadLandingTests(buildTestable);
|
||||
_discoveryJourneyTests(buildTestable);
|
||||
|
||||
final testChannels = [
|
||||
Channel(
|
||||
@@ -2949,8 +2951,8 @@ void main() {
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(largestHeight, greaterThan(216));
|
||||
expect(tester.getSize(surface).height, closeTo(216, 0.01));
|
||||
expect(largestHeight, greaterThan(142));
|
||||
expect(tester.getSize(surface).height, closeTo(142, 0.01));
|
||||
final screenWidth = MediaQuery.sizeOf(tester.element(surface)).width;
|
||||
final surfaceRect = tester.getRect(surface);
|
||||
expect(surfaceRect.left, closeTo(20, 0.01));
|
||||
@@ -2963,23 +2965,15 @@ void main() {
|
||||
const Key('quick-action-create-channel-card'),
|
||||
);
|
||||
final dmCard = find.byKey(const Key('quick-action-new-dm-card'));
|
||||
final browseCard = find.byKey(
|
||||
const Key('quick-action-browse-channels-card'),
|
||||
);
|
||||
final createRect = tester.getRect(createCard);
|
||||
final dmRect = tester.getRect(dmCard);
|
||||
final browseRect = tester.getRect(browseCard);
|
||||
|
||||
expect(createRect.left - menuRect.left, closeTo(8, 0.01));
|
||||
expect(menuRect.right - createRect.right, closeTo(8, 0.01));
|
||||
expect(dmRect.left - menuRect.left, closeTo(8, 0.01));
|
||||
expect(menuRect.right - dmRect.right, closeTo(8, 0.01));
|
||||
expect(browseRect.left - menuRect.left, closeTo(8, 0.01));
|
||||
expect(menuRect.right - browseRect.right, closeTo(8, 0.01));
|
||||
expect(dmRect.top - createRect.bottom, closeTo(8, 0.01));
|
||||
expect(browseRect.top - dmRect.bottom, closeTo(8, 0.01));
|
||||
expect(dmRect.top - createRect.bottom, closeTo(6, 0.01));
|
||||
expect(dmRect.width, createRect.width);
|
||||
expect(browseRect.width, createRect.width);
|
||||
expect(dmRect.width, closeTo(menuRect.width - 16, 0.01));
|
||||
|
||||
final expectedCardColor = Color.alphaBlend(
|
||||
@@ -2992,25 +2986,21 @@ void main() {
|
||||
final dmMaterial = tester.widget<Material>(
|
||||
find.descendant(of: dmCard, matching: find.byType(Material)).first,
|
||||
);
|
||||
final browseMaterial = tester.widget<Material>(
|
||||
find.descendant(of: browseCard, matching: find.byType(Material)).first,
|
||||
);
|
||||
expect(createMaterial.color, expectedCardColor);
|
||||
expect(dmMaterial.color, expectedCardColor);
|
||||
expect(browseMaterial.color, expectedCardColor);
|
||||
expect(
|
||||
((createMaterial.shape as RoundedSuperellipseBorder).borderRadius
|
||||
as BorderRadius)
|
||||
.topLeft
|
||||
.x,
|
||||
closeTo(20, 0.01),
|
||||
(createMaterial.shape as RoundedSuperellipseBorder).borderRadius,
|
||||
const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
bottom: Radius.circular(8),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
((dmMaterial.shape as RoundedSuperellipseBorder).borderRadius
|
||||
as BorderRadius)
|
||||
.topLeft
|
||||
.x,
|
||||
closeTo(20, 0.01),
|
||||
(dmMaterial.shape as RoundedSuperellipseBorder).borderRadius,
|
||||
const BorderRadius.vertical(
|
||||
top: Radius.circular(8),
|
||||
bottom: Radius.circular(20),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Start a new stream channel'), findsNothing);
|
||||
@@ -3023,8 +3013,8 @@ void main() {
|
||||
16,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Browse channels')).style?.fontSize,
|
||||
16,
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.text('Message one or more people'), findsNothing);
|
||||
});
|
||||
@@ -3087,11 +3077,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Create or start conversation'));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -3123,11 +3109,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Create or start conversation'));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No open channels available to join.'), findsOneWidget);
|
||||
@@ -3162,11 +3144,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Create or start conversation'));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Couldn’t load open channels.'), findsOneWidget);
|
||||
@@ -3213,11 +3191,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Create or start conversation'));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BuzzLoadingIndicator), findsOneWidget);
|
||||
@@ -3269,11 +3243,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Create or start conversation'));
|
||||
await tester.pump();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -4120,105 +4090,11 @@ class _FakeNotifier extends ChannelsNotifier {
|
||||
get observedUnreadEventsByChannel => _observedEventsByChannel;
|
||||
}
|
||||
|
||||
class _RetryingDirectoryNotifier extends ChannelsNotifier {
|
||||
_RetryingDirectoryNotifier({
|
||||
required this.initialChannels,
|
||||
required this.retriedChannels,
|
||||
});
|
||||
|
||||
final List<Channel> initialChannels;
|
||||
final List<Channel> retriedChannels;
|
||||
int retryCount = 0;
|
||||
|
||||
@override
|
||||
Future<List<Channel>> build() async => initialChannels;
|
||||
|
||||
@override
|
||||
Future<void> ensureDirectoryLoaded() async {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markError(_activeDirectoryScope(ref));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> retryDirectory() async {
|
||||
retryCount++;
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoading(_activeDirectoryScope(ref));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
state = AsyncData(retriedChannels);
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoaded(_activeDirectoryScope(ref));
|
||||
}
|
||||
}
|
||||
|
||||
class _SupersededDirectoryNotifier extends ChannelsNotifier {
|
||||
_SupersededDirectoryNotifier({
|
||||
required this.initialChannels,
|
||||
required this.retriedChannels,
|
||||
});
|
||||
|
||||
final List<Channel> initialChannels;
|
||||
final List<Channel> retriedChannels;
|
||||
final _directoryCompletion = Completer<void>();
|
||||
int retryCount = 0;
|
||||
|
||||
@override
|
||||
Future<List<Channel>> build() async => initialChannels;
|
||||
|
||||
@override
|
||||
Future<void> ensureDirectoryLoaded() async {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoading(_activeDirectoryScope(ref));
|
||||
await _directoryCompletion.future;
|
||||
}
|
||||
|
||||
/// Mirrors an ordinary refresh invalidating the active directory request.
|
||||
void supersedeLoadingDirectory() {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markError(_activeDirectoryScope(ref));
|
||||
_directoryCompletion.complete();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> retryDirectory() async {
|
||||
retryCount++;
|
||||
state = AsyncData(retriedChannels);
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoaded(_activeDirectoryScope(ref));
|
||||
}
|
||||
}
|
||||
|
||||
String _activeDirectoryScope(Ref ref) => channelDirectoryScope(
|
||||
ref.read(relayConfigProvider).baseUrl,
|
||||
ref.read(myPubkeyProvider),
|
||||
);
|
||||
|
||||
class _RecordingChannelActions extends ChannelActions {
|
||||
_RecordingChannelActions(Ref ref)
|
||||
: super(
|
||||
ref: ref,
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
signedEventRelay: SignedEventRelay(
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
nsec: null,
|
||||
),
|
||||
currentPubkey: 'self',
|
||||
);
|
||||
|
||||
final List<String> joinedChannelIds = [];
|
||||
|
||||
@override
|
||||
Future<void> joinChannel(String channelId) async {
|
||||
joinedChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeChannelSectionsNotifier extends ChannelSectionsNotifier {
|
||||
_FakeChannelSectionsNotifier(this._store);
|
||||
|
||||
@@ -4424,3 +4300,97 @@ class _CompletedPairingNotifier extends PairingNotifier {
|
||||
@override
|
||||
PairingState build() => const PairingState(status: PairingStatus.success);
|
||||
}
|
||||
|
||||
class _RetryingDirectoryNotifier extends ChannelsNotifier {
|
||||
_RetryingDirectoryNotifier({
|
||||
required this.initialChannels,
|
||||
required this.retriedChannels,
|
||||
});
|
||||
|
||||
final List<Channel> initialChannels;
|
||||
final List<Channel> retriedChannels;
|
||||
int retryCount = 0;
|
||||
|
||||
@override
|
||||
Future<List<Channel>> build() async => initialChannels;
|
||||
|
||||
@override
|
||||
Future<void> ensureDirectoryLoaded() async {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markError(_activeDirectoryScope(ref));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> retryDirectory() async {
|
||||
retryCount++;
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoading(_activeDirectoryScope(ref));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
state = AsyncData(retriedChannels);
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoaded(_activeDirectoryScope(ref));
|
||||
}
|
||||
}
|
||||
|
||||
class _SupersededDirectoryNotifier extends ChannelsNotifier {
|
||||
_SupersededDirectoryNotifier({
|
||||
required this.initialChannels,
|
||||
required this.retriedChannels,
|
||||
});
|
||||
|
||||
final List<Channel> initialChannels;
|
||||
final List<Channel> retriedChannels;
|
||||
final _directoryCompletion = Completer<void>();
|
||||
int retryCount = 0;
|
||||
|
||||
@override
|
||||
Future<List<Channel>> build() async => initialChannels;
|
||||
|
||||
@override
|
||||
Future<void> ensureDirectoryLoaded() async {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoading(_activeDirectoryScope(ref));
|
||||
await _directoryCompletion.future;
|
||||
}
|
||||
|
||||
/// Mirrors an ordinary refresh invalidating the active directory request.
|
||||
void supersedeLoadingDirectory() {
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markError(_activeDirectoryScope(ref));
|
||||
_directoryCompletion.complete();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> retryDirectory() async {
|
||||
retryCount++;
|
||||
state = AsyncData(retriedChannels);
|
||||
ref
|
||||
.read(channelDirectoryLoadStatusProvider.notifier)
|
||||
.markLoaded(_activeDirectoryScope(ref));
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingChannelActions extends ChannelActions {
|
||||
_RecordingChannelActions(Ref ref)
|
||||
: super(
|
||||
ref: ref,
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
signedEventRelay: SignedEventRelay(
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
nsec: null,
|
||||
),
|
||||
currentPubkey: 'self',
|
||||
);
|
||||
|
||||
final List<String> joinedChannelIds = [];
|
||||
|
||||
@override
|
||||
Future<void> joinChannel(String channelId) async {
|
||||
joinedChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
part of '../channels_page_test.dart';
|
||||
|
||||
void _discoveryJourneyTests(
|
||||
Widget Function({required List<Override> overrides}) buildTestable,
|
||||
) {
|
||||
testWidgets(
|
||||
'zero-membership user retries discovery and joins a conversation',
|
||||
(tester) async {
|
||||
final session = _DiscoveryJourneySession();
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
relaySessionProvider.overrideWith(() => session),
|
||||
myPubkeyProvider.overrideWith((ref) => 'self'),
|
||||
channelActionsProvider.overrideWith(
|
||||
(ref) => ChannelActions(
|
||||
ref: ref,
|
||||
session: session,
|
||||
signedEventRelay: _DiscoveryJourneyRelay(session),
|
||||
currentPubkey: 'self',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final container = ProviderScope.containerOf(
|
||||
tester.element(find.byType(ChannelsPage)),
|
||||
);
|
||||
expect(container.read(channelsProvider).requireValue, isEmpty);
|
||||
expect(session.directoryRequests, 0);
|
||||
|
||||
await tester.tap(find.byKey(const Key('browse-channels-button')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Couldn’t load open channels.'), findsOneWidget);
|
||||
await tester.tap(find.byKey(const Key('browse-channels-retry')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('community-help'), findsOneWidget);
|
||||
expect(find.text('private-planning'), findsNothing);
|
||||
expect(find.text('old-channel'), findsNothing);
|
||||
expect(find.text('hidden-dm'), findsNothing);
|
||||
expect(
|
||||
container.read(channelsProvider).requireValue.every((c) => !c.isMember),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('browse-channel-join-$_discoveryId')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(session.joined, isTrue);
|
||||
expect(find.byType(BottomSheet), findsNothing);
|
||||
expect(find.text('community-help'), findsOneWidget);
|
||||
expect(
|
||||
container
|
||||
.read(channelsProvider)
|
||||
.requireValue
|
||||
.singleWhere((channel) => channel.id == _discoveryId)
|
||||
.isMember,
|
||||
isTrue,
|
||||
);
|
||||
// Discovery remains available after acquiring a membership.
|
||||
expect(find.byKey(const Key('browse-channels-button')), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const _discoveryId = '11111111-1111-4111-8111-111111111111';
|
||||
|
||||
class _DiscoveryJourneySession extends _ReconnectingRelaySession {
|
||||
_DiscoveryJourneySession() : super(initialStatus: SessionStatus.connected);
|
||||
|
||||
bool joined = false;
|
||||
int directoryRequests = 0;
|
||||
|
||||
NostrEvent metadata(
|
||||
String id,
|
||||
String name, {
|
||||
String type = 'stream',
|
||||
bool private = false,
|
||||
bool archived = false,
|
||||
}) => NostrEvent(
|
||||
id: 'meta-$id',
|
||||
pubkey: 'creator',
|
||||
createdAt: 1,
|
||||
kind: 39000,
|
||||
tags: [
|
||||
['d', id],
|
||||
['name', name],
|
||||
['t', type],
|
||||
[private ? 'private' : 'public'],
|
||||
if (archived) ['archived', 'true'],
|
||||
],
|
||||
content: '',
|
||||
sig: 'sig',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> queryRelay(
|
||||
List<NostrFilter> filters, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
final result = <NostrEvent>[];
|
||||
for (final filter in filters) {
|
||||
if (filter.until != null) continue;
|
||||
if (filter.kinds.contains(39000) && !filter.tags.containsKey('#d')) {
|
||||
directoryRequests++;
|
||||
if (directoryRequests == 1) throw Exception('Directory unavailable');
|
||||
result.addAll([
|
||||
metadata(_discoveryId, 'community-help'),
|
||||
metadata(
|
||||
'22222222-2222-4222-8222-222222222222',
|
||||
'private-planning',
|
||||
private: true,
|
||||
),
|
||||
metadata(
|
||||
'33333333-3333-4333-8333-333333333333',
|
||||
'old-channel',
|
||||
archived: true,
|
||||
),
|
||||
metadata(
|
||||
'44444444-4444-4444-8444-444444444444',
|
||||
'hidden-dm',
|
||||
type: 'dm',
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
result.addAll(await fetchHistory(filter));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> fetchHistory(
|
||||
NostrFilter filter, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
if (filter.kinds.contains(39002) && joined) {
|
||||
return [
|
||||
NostrEvent(
|
||||
id: 'membership',
|
||||
pubkey: 'creator',
|
||||
createdAt: 2,
|
||||
kind: 39002,
|
||||
tags: [
|
||||
['d', _discoveryId],
|
||||
['p', 'self'],
|
||||
],
|
||||
content: '',
|
||||
sig: 'sig',
|
||||
),
|
||||
];
|
||||
}
|
||||
if (filter.kinds.contains(39000) &&
|
||||
(filter.tags['#d'] ?? []).contains(_discoveryId)) {
|
||||
return [metadata(_discoveryId, 'community-help')];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Fake only the signed network boundary; the widget, ChannelsNotifier and
|
||||
// ChannelActions.joinChannel/refresh are the production implementations.
|
||||
class _DiscoveryJourneyRelay extends SignedEventRelay {
|
||||
_DiscoveryJourneyRelay(this.session) : super(session: session, nsec: null);
|
||||
final _DiscoveryJourneySession session;
|
||||
|
||||
@override
|
||||
Future<NostrEvent> submit({
|
||||
required int kind,
|
||||
required String content,
|
||||
required List<List<String>> tags,
|
||||
int? createdAt,
|
||||
void Function(NostrEvent)? onSigned,
|
||||
}) async {
|
||||
expect(kind, 9021);
|
||||
expect(tags, [
|
||||
['h', _discoveryId],
|
||||
]);
|
||||
session.joined = true;
|
||||
return NostrEvent(
|
||||
id: 'join',
|
||||
pubkey: 'self',
|
||||
createdAt: 2,
|
||||
kind: kind,
|
||||
tags: tags,
|
||||
content: '',
|
||||
sig: 'sig',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,10 @@ void main() {
|
||||
expect(tester.getSize(launcherGlass).width, greaterThan(56));
|
||||
expect(find.text('Create channel').hitTestable(), findsOneWidget);
|
||||
expect(find.text('New direct message').hitTestable(), findsOneWidget);
|
||||
expect(find.text('Browse channels').hitTestable(), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
findsNothing,
|
||||
);
|
||||
await tester.tapAt(const Offset(8, 8));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
@@ -270,7 +273,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Create channel'), findsOneWidget);
|
||||
expect(find.text('New direct message'), findsOneWidget);
|
||||
expect(find.text('Browse channels'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('quick-action-browse-channels-card')),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('keeps the Buzz backdrop behind the scalable Home screen', (
|
||||
|
||||
Reference in New Issue
Block a user