mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(mobile): browse and join open channels
Co-authored-by: Tom Brow <tomb@block.xyz> Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
co-authored by
Tom Brow
parent
acdb264e65
commit
62f20d2fda
@@ -79,6 +79,8 @@ class Channel {
|
||||
bool get isPrivate => visibility == 'private';
|
||||
bool get isArchived => archivedAt != null;
|
||||
|
||||
bool get canJoin => visibility == 'open' && !isArchived && !isMember && !isDm;
|
||||
|
||||
String displayLabel({String? currentPubkey}) {
|
||||
if (!isDm || participants.isEmpty) {
|
||||
return name;
|
||||
|
||||
@@ -54,7 +54,7 @@ part 'channels_page/community.dart';
|
||||
part 'channels_page/quick_actions.dart';
|
||||
part 'channels_page/quick_actions_launcher.dart';
|
||||
|
||||
enum _QuickAction { createChannel, newDm }
|
||||
enum _QuickAction { createChannel, newDm, browseChannels }
|
||||
|
||||
const double _kChannelSectionInset = Grid.gutter;
|
||||
const double _kChannelLeadingWidth = 22.0;
|
||||
|
||||
@@ -196,7 +196,9 @@ class _SliverChannelsList extends HookConsumerWidget {
|
||||
sliver: SliverList.list(
|
||||
children: [
|
||||
if (visibleChannels.isEmpty)
|
||||
const _EmptyState()
|
||||
_EmptyState(
|
||||
channels: channels.where((channel) => channel.canJoin).toList(),
|
||||
)
|
||||
else ...[
|
||||
// Starred channels (exclusive — pinned above all sections).
|
||||
if (starredStreamChannels.isNotEmpty)
|
||||
|
||||
@@ -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 = 160;
|
||||
const double _kMorphOpenHeight = 216;
|
||||
const double _kMorphOpenRadius = 20;
|
||||
const double _kMorphSlide = 40;
|
||||
const double _kMorphScale = 0.97;
|
||||
@@ -274,6 +274,13 @@ class _QuickActionsMenu extends StatelessWidget {
|
||||
key: const Key('quick-action-new-dm-card'),
|
||||
onTap: () => onSelected(_QuickAction.newDm),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
_QuickActionItem(
|
||||
icon: LucideIcons.search,
|
||||
title: 'Browse channels',
|
||||
key: const Key('quick-action-browse-channels-card'),
|
||||
onTap: () => onSelected(_QuickAction.browseChannels),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -107,6 +107,8 @@ class ChannelQuickActionsLauncher extends HookConsumerWidget {
|
||||
if (opened != null && context.mounted) {
|
||||
await openChannel(opened);
|
||||
}
|
||||
case _QuickAction.browseChannels:
|
||||
await _showBrowseChannelsSheet(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -360,29 +360,48 @@ class _ChannelSection extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
final List<Channel> channels;
|
||||
|
||||
const _EmptyState({required this.channels});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.55,
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.sizeOf(context).height * 0.55,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.messagesSquare,
|
||||
size: Grid.xl,
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
Text(
|
||||
'No conversations yet',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.messagesSquare,
|
||||
size: Grid.xl,
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: Grid.xs),
|
||||
Text(
|
||||
'No conversations yet',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (channels.isNotEmpty) ...[
|
||||
const SizedBox(height: Grid.xs),
|
||||
Text(
|
||||
'Join an open channel to start a conversation.',
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
_JoinableChannelList(channels: channels),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,186 @@ part of '../channels_page.dart';
|
||||
|
||||
const int _defaultCreateChannelTtlSeconds = 7 * 24 * 60 * 60;
|
||||
|
||||
Future<void> _showBrowseChannelsSheet(BuildContext context) =>
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
constraints: _quickActionSheetConstraints(context),
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (_) => const _BrowseChannelsSheet(),
|
||||
);
|
||||
|
||||
class _BrowseChannelsSheet extends StatelessWidget {
|
||||
const _BrowseChannelsSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final channelsAsync = ref.watch(channelsProvider);
|
||||
final channels = channelsAsync.asData?.value
|
||||
.where((channel) => channel.canJoin)
|
||||
.toList();
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Grid.gutter,
|
||||
0,
|
||||
Grid.gutter,
|
||||
Grid.xs,
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Text(
|
||||
'Browse channels',
|
||||
style: context.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.half),
|
||||
Text(
|
||||
'Join an open channel to add it to your conversations.',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
if (channelsAsync.isLoading && channels == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(Grid.sm),
|
||||
child: Center(child: BuzzLoadingIndicator()),
|
||||
)
|
||||
else if (channelsAsync.hasError && channels == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Grid.sm),
|
||||
child: Text(
|
||||
'Could not load open channels.',
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (channels == null || channels.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Grid.sm),
|
||||
child: Text(
|
||||
'No open channels available to join.',
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
_JoinableChannelList(
|
||||
channels: channels,
|
||||
closeAfterJoin: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _JoinableChannelList extends StatelessWidget {
|
||||
final List<Channel> channels;
|
||||
final bool closeAfterJoin;
|
||||
|
||||
const _JoinableChannelList({
|
||||
required this.channels,
|
||||
this.closeAfterJoin = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sortedChannels = List<Channel>.of(channels)
|
||||
..sort((left, right) => left.name.compareTo(right.name));
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final channel in sortedChannels)
|
||||
_JoinableChannelTile(
|
||||
channel: channel,
|
||||
closeAfterJoin: closeAfterJoin,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _JoinableChannelTile extends HookConsumerWidget {
|
||||
final Channel channel;
|
||||
final bool closeAfterJoin;
|
||||
|
||||
const _JoinableChannelTile({
|
||||
required this.channel,
|
||||
required this.closeAfterJoin,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isJoining = useState(false);
|
||||
final actionError = useState<String?>(null);
|
||||
|
||||
Future<void> join() async {
|
||||
if (isJoining.value) return;
|
||||
isJoining.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await ref.read(channelActionsProvider).joinChannel(channel.id);
|
||||
if (closeAfterJoin && context.mounted) Navigator.of(context).pop();
|
||||
} catch (error) {
|
||||
actionError.value = error.toString();
|
||||
} finally {
|
||||
isJoining.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
key: Key('browse-channel-${channel.id}'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(channelIcon(channel)),
|
||||
title: Text(channel.name),
|
||||
subtitle: channel.description.trim().isEmpty
|
||||
? null
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
key: Key('browse-channel-join-${channel.id}'),
|
||||
onPressed: isJoining.value ? null : () => unawaited(join()),
|
||||
child: Text(isJoining.value ? 'Joining\u2026' : 'Join'),
|
||||
),
|
||||
),
|
||||
if (actionError.value case final error?)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
error,
|
||||
key: Key('browse-channel-error-${channel.id}'),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateChannelMenuOption<T> {
|
||||
final Key? key;
|
||||
final String label;
|
||||
|
||||
@@ -34,11 +34,7 @@ class ManageChannelSheet extends HookConsumerWidget {
|
||||
final mutesState = ref.watch(channelMutesProvider);
|
||||
final isMuted = mutesState.store.channels[channel.id]?.muted == true;
|
||||
|
||||
final canJoin =
|
||||
channel.visibility == 'open' &&
|
||||
!channel.isArchived &&
|
||||
!channel.isMember &&
|
||||
!channel.isDm;
|
||||
final canJoin = channel.canJoin;
|
||||
final canLeave = channel.isMember && !channel.isArchived && !channel.isDm;
|
||||
final canEditCanvas = channel.isMember && !channel.isArchived;
|
||||
|
||||
|
||||
@@ -723,8 +723,8 @@ void main() {
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(largestHeight, greaterThan(160));
|
||||
expect(tester.getSize(surface).height, closeTo(160, 0.01));
|
||||
expect(largestHeight, greaterThan(216));
|
||||
expect(tester.getSize(surface).height, closeTo(216, 0.01));
|
||||
final screenWidth = MediaQuery.sizeOf(tester.element(surface)).width;
|
||||
final surfaceRect = tester.getRect(surface);
|
||||
expect(surfaceRect.left, closeTo(20, 0.01));
|
||||
@@ -737,15 +737,23 @@ 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.width, createRect.width);
|
||||
expect(browseRect.width, createRect.width);
|
||||
expect(dmRect.width, closeTo(menuRect.width - 16, 0.01));
|
||||
|
||||
final cardScheme = Theme.of(tester.element(createCard)).colorScheme;
|
||||
@@ -759,8 +767,12 @@ 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.borderRadius as BorderRadius).topLeft.x,
|
||||
closeTo(12, 0.01),
|
||||
@@ -779,9 +791,98 @@ void main() {
|
||||
tester.widget<Text>(find.text('New direct message')).style?.fontSize,
|
||||
16,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Browse channels')).style?.fontSize,
|
||||
16,
|
||||
);
|
||||
expect(find.text('Message one or more people'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('browse action lists only channels eligible to join', (
|
||||
tester,
|
||||
) async {
|
||||
final channels = [
|
||||
...testChannels,
|
||||
Channel(
|
||||
id: 'open-to-join',
|
||||
name: 'announcements',
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: 'Community announcements',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 8,
|
||||
),
|
||||
Channel(
|
||||
id: 'private-channel',
|
||||
name: 'private-planning',
|
||||
channelType: 'stream',
|
||||
visibility: 'private',
|
||||
description: 'Private planning',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 4,
|
||||
),
|
||||
Channel(
|
||||
id: 'archived-channel',
|
||||
name: 'old-announcements',
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: 'Archived announcements',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 3,
|
||||
archivedAt: DateTime(2025, 1, 2),
|
||||
),
|
||||
Channel(
|
||||
id: 'unjoined-dm',
|
||||
name: 'Hidden DM',
|
||||
channelType: 'dm',
|
||||
visibility: 'open',
|
||||
description: 'Direct message',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 2,
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
disableAnimations: true,
|
||||
overrides: [
|
||||
channelsProvider.overrideWith(() => _FakeNotifier(channels)),
|
||||
],
|
||||
),
|
||||
);
|
||||
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.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-open-to-join')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-join-open-to-join')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const Key('browse-channel-1')), findsNothing);
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-private-channel')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-archived-channel')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byKey(const Key('browse-channel-unjoined-dm')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('create channel sheet lists type and visibility radio options', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -1173,6 +1274,127 @@ void main() {
|
||||
expect(find.text('archived-stream'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('empty state lets stuck users join an open channel', (
|
||||
tester,
|
||||
) async {
|
||||
final discoveredChannel = Channel(
|
||||
id: 'recovery-channel',
|
||||
name: 'community-help',
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: 'Get help from the community',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 7,
|
||||
);
|
||||
final channelsNotifier = _FakeNotifier([discoveredChannel]);
|
||||
final joinedChannelIds = <String>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
channelsProvider.overrideWith(() => channelsNotifier),
|
||||
channelActionsProvider.overrideWith(
|
||||
(ref) => _FakeChannelActions(
|
||||
ref,
|
||||
onJoinChannel: (channelId) async {
|
||||
joinedChannelIds.add(channelId);
|
||||
channelsNotifier.setChannels([
|
||||
discoveredChannel.copyWith(isMember: true),
|
||||
]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No conversations yet'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-recovery-channel')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('browse-channel-join-recovery-channel')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('community-help'), findsOneWidget);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('browse-channel-join-recovery-channel')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(joinedChannelIds, ['recovery-channel']);
|
||||
expect(find.text('No conversations yet'), findsNothing);
|
||||
expect(find.text('community-help'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('empty state lists every joinable channel without overflow', (
|
||||
tester,
|
||||
) async {
|
||||
final discoveredChannels = List.generate(
|
||||
6,
|
||||
(index) => Channel(
|
||||
id: 'recovery-channel-$index',
|
||||
name: 'community-channel-$index',
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: 'Community channel $index',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: index + 1,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
channelsProvider.overrideWith(
|
||||
() => _FakeNotifier(discoveredChannels),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No conversations yet'), findsOneWidget);
|
||||
for (final channel in discoveredChannels) {
|
||||
expect(find.byKey(Key('browse-channel-${channel.id}')), findsOneWidget);
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('empty state omits browse CTA without joinable channels', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
channelsProvider.overrideWith(
|
||||
() => _FakeNotifier([
|
||||
Channel(
|
||||
id: 'private-only',
|
||||
name: 'private-only',
|
||||
channelType: 'stream',
|
||||
visibility: 'private',
|
||||
description: 'Private channel',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 2,
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No conversations yet'), findsOneWidget);
|
||||
expect(find.byKey(const Key('browse-channel-private-only')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows empty state when no channels', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
@@ -1414,7 +1636,7 @@ Widget _buildSettingsPage(BuildContext context) =>
|
||||
const Scaffold(body: Text('Injected settings'));
|
||||
|
||||
class _FakeNotifier extends ChannelsNotifier {
|
||||
final List<Channel> _channels;
|
||||
List<Channel> _channels;
|
||||
final Map<String, Map<String, ObservedUnreadEvent>> _observedEventsByChannel;
|
||||
|
||||
_FakeNotifier(
|
||||
@@ -1428,6 +1650,11 @@ class _FakeNotifier extends ChannelsNotifier {
|
||||
@override
|
||||
Future<List<Channel>> build() async => _channels;
|
||||
|
||||
void setChannels(List<Channel> channels) {
|
||||
_channels = channels;
|
||||
state = AsyncData(channels);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, int> get latestObservedByChannel => {
|
||||
for (final entry in _observedEventsByChannel.entries)
|
||||
@@ -1442,6 +1669,26 @@ class _FakeNotifier extends ChannelsNotifier {
|
||||
get observedUnreadEventsByChannel => _observedEventsByChannel;
|
||||
}
|
||||
|
||||
class _FakeChannelActions extends ChannelActions {
|
||||
final Future<void> Function(String channelId)? onJoinChannel;
|
||||
|
||||
_FakeChannelActions(Ref ref, {this.onJoinChannel})
|
||||
: super(
|
||||
ref: ref,
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
signedEventRelay: SignedEventRelay(
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
nsec: null,
|
||||
),
|
||||
currentPubkey: 'aabb',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> joinChannel(String channelId) async {
|
||||
await onJoinChannel?.call(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeChannelSectionsNotifier extends ChannelSectionsNotifier {
|
||||
_FakeChannelSectionsNotifier(this._store);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user