Skip to content
559 changes: 559 additions & 0 deletions mobile/lib/features/channels/channel_actions_sheet.dart

Large diffs are not rendered by default.

43 changes: 21 additions & 22 deletions mobile/lib/features/channels/channel_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import '../profile/user_cache_provider.dart';
import '../profile/user_profile.dart';
import '../forum/forum_posts_view.dart';
import 'channel.dart';
import 'channel_actions_sheet.dart';
import 'channel_link_navigation.dart';
import 'agent_activity/working_bots_provider.dart';
import 'channel_management_provider.dart';
import 'channel_sections/channel_sections_provider.dart';
import 'channel_messages_provider.dart';
import 'channel_typing_provider.dart';
import 'channel_typing_indicator.dart';
Expand All @@ -38,7 +40,6 @@ import 'date_formatters.dart';
import 'day_divider.dart';
import 'dm_channel_labels.dart';
import 'ephemeral_channel_display.dart';
import 'manage_channel_sheet.dart';
import 'members_sheet.dart';
import 'message_actions.dart';
import 'message_content.dart';
Expand Down Expand Up @@ -277,27 +278,25 @@ class ChannelDetailPage extends HookConsumerWidget {
channel: resolvedChannel,
currentPubkey: currentPubkey,
),
if (!resolvedChannel.isDm)
IconButton(
color: context.colors.primary,
onPressed: () async {
final shouldClose = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
showDragHandle: true,
constraints: BoxConstraints(
maxWidth: 640,
maxHeight: MediaQuery.sizeOf(context).height * 0.9,
),
builder: (_) => ManageChannelSheet(channel: resolvedChannel),
);
if (shouldClose == true && context.mounted) {
Navigator.of(context).pop();
}
},
tooltip: 'Manage channel',
icon: const Icon(LucideIcons.ellipsisVertical, size: 22),
),
IconButton(
color: context.colors.primary,
onPressed: () async {
final shouldClose = await showChannelActionsSheet(
context: context,
channel: resolvedChannel,
isUnread: false,
sectionId: ref
.read(channelSectionsProvider)
.store
.assignments[resolvedChannel.id],
);
if (shouldClose == true && context.mounted) {
Navigator.of(context).pop();
}
},
tooltip: 'Channel actions',
icon: const Icon(LucideIcons.ellipsisVertical, size: 22),
),
],
),
body: Stack(
Expand Down
44 changes: 44 additions & 0 deletions mobile/lib/features/channels/channel_management_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,20 @@ List<List<String>> buildCreateChannelTags({
];
}

/// Builds the relay tags for setting the archived state of [channelId].
List<List<String>> buildSetChannelArchivedTags(
String channelId, {
required bool archived,
}) => [
['h', channelId],
['archived', archived.toString()],
];

/// Builds the relay tags for deleting [channelId].
List<List<String>> buildDeleteChannelTags(String channelId) => [
['h', channelId],
];

class ChannelActions {
final Ref _ref;
final RelaySessionNotifier _session;
Expand Down Expand Up @@ -584,6 +598,36 @@ class ChannelActions {
await _refreshChannelState(channelId);
}

/// Archives the channel and refreshes its cached state.
Future<void> archiveChannel(String channelId) =>
_setChannelArchived(channelId, archived: true);

/// Unarchives the channel and refreshes its cached state.
Future<void> unarchiveChannel(String channelId) =>
_setChannelArchived(channelId, archived: false);

Future<void> _setChannelArchived(
String channelId, {
required bool archived,
}) async {
await _signedEventRelay.submit(
kind: 9002,
content: '',
tags: buildSetChannelArchivedTags(channelId, archived: archived),
);
await _refreshChannelState(channelId);
}

/// Deletes the channel and refreshes its cached state.
Future<void> deleteChannel(String channelId) async {
await _signedEventRelay.submit(
kind: 9008,
content: '',
tags: buildDeleteChannelTags(channelId),
);
await _refreshChannelState(channelId);
}

Future<void> setCanvas({
required String channelId,
required String content,
Expand Down
1 change: 1 addition & 0 deletions mobile/lib/features/channels/channels_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import '../profile/user_cache_provider.dart';
import '../pairing/pairing_page.dart';
import '../pairing/pairing_provider.dart';
import 'channel.dart';
import 'channel_actions_sheet.dart';
import 'channel_detail_page.dart';
import 'channel_management_provider.dart';
import 'dm_channel_labels.dart';
Expand Down
210 changes: 5 additions & 205 deletions mobile/lib/features/channels/channels_page/channel_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,212 +118,12 @@ class _ChannelTile extends ConsumerWidget {
}

void _showChannelActions(BuildContext context, WidgetRef ref) {
showModalBottomSheet<void>(
showChannelActionsSheet(
context: context,
showDragHandle: true,
builder: (sheetContext) {
final sections = ref.read(channelSectionsProvider).store.sections
..sort((a, b) => a.order.compareTo(b.order));
final isStarred =
ref
.read(channelStarsProvider)
.store
.channels[channel.id]
?.starred ==
true;

return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(
Grid.gutter,
0,
Grid.gutter,
Grid.xs,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
isStarred ? LucideIcons.starOff : LucideIcons.star,
),
title: Text(isStarred ? 'Unstar channel' : 'Star channel'),
onTap: () {
Navigator.of(sheetContext).pop();
if (isStarred) {
ref
.read(channelStarsProvider.notifier)
.unstarChannel(channel.id);
} else {
ref
.read(channelStarsProvider.notifier)
.starChannel(channel.id);
}
},
),
ListTile(
leading: const Icon(LucideIcons.folderInput),
title: const Text('Move to section'),
onTap: () async {
Navigator.of(sheetContext).pop();
await _showMoveSectionSheet(context, ref, sections);
},
),
ListTile(
leading: Icon(
isMuted ? LucideIcons.bell : LucideIcons.bellOff,
),
title: Text(isMuted ? 'Unmute channel' : 'Mute channel'),
onTap: () {
Navigator.of(sheetContext).pop();
if (isMuted) {
ref
.read(channelMutesProvider.notifier)
.unmuteChannel(channel.id);
} else {
ref
.read(channelMutesProvider.notifier)
.muteChannel(channel.id);
}
},
),
ListTile(
leading: Icon(
isUnread ? LucideIcons.checkCheck : LucideIcons.circleDot,
),
title: Text(isUnread ? 'Mark as read' : 'Mark as unread'),
onTap: () {
Navigator.of(sheetContext).pop();
final ts = dateTimeToUnixSeconds(channel.lastMessageAt);
if (ts != null) {
if (isUnread) {
onMarkRead?.call();
ref
.read(readStateProvider.notifier)
.markContextRead(
channel.id,
ts,
clearForcedMessages: true,
);
ref
.read(channelsProvider.notifier)
.clearObservedUnreadCoveredByRead(channel.id, ts);
} else {
ref
.read(readStateProvider.notifier)
.markContextUnread(
channel.id,
channelId: channel.id,
);
}
}
},
),
],
),
),
);
},
);
}

Future<void> _showMoveSectionSheet(
BuildContext context,
WidgetRef ref,
List<ChannelSection> sections,
) async {
await showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (sheetContext) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(
Grid.gutter,
0,
Grid.gutter,
Grid.xs,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final section in sections)
ListTile(
leading: Icon(
LucideIcons.folder,
color: sectionId == section.id
? sheetContext.colors.primary
: null,
),
title: Text(section.name),
trailing: sectionId == section.id
? Icon(
LucideIcons.check,
color: sheetContext.colors.primary,
)
: null,
onTap: () {
Navigator.of(sheetContext).pop();
ref
.read(channelSectionsProvider.notifier)
.assignChannel(channel.id, section.id);
},
),
ListTile(
leading: const Icon(LucideIcons.folderPlus),
title: const Text('New section…'),
onTap: () async {
Navigator.of(sheetContext).pop();
if (!context.mounted) return;
final name = await showDialog<String>(
context: context,
builder: (_) => const _SectionNameDialog(
title: 'New Section',
confirmLabel: 'Create',
),
);
if (name != null && name.isNotEmpty) {
ref
.read(channelSectionsProvider.notifier)
.createSection(name);
// Assign after create — sections list has been mutated,
// re-read to find the new section by name.
final newSection = ref
.read(channelSectionsProvider)
.store
.sections
.lastWhere(
(s) => s.name == name.trim(),
orElse: () => const ChannelSection(
id: '',
name: '',
order: -1,
),
);
if (newSection.id.isNotEmpty) {
ref
.read(channelSectionsProvider.notifier)
.assignChannel(channel.id, newSection.id);
}
}
},
),
if (sectionId != null)
ListTile(
leading: const Icon(LucideIcons.folderMinus),
title: const Text('Remove from section'),
onTap: () {
Navigator.of(sheetContext).pop();
ref
.read(channelSectionsProvider.notifier)
.unassignChannel(channel.id);
},
),
],
),
),
);
},
channel: channel,
isUnread: isUnread,
onMarkRead: onMarkRead,
sectionId: sectionId,
);
}
}
Expand Down
Loading