Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 20 additions & 32 deletions mobile/lib/features/channels/channel_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import 'manage_channel_sheet.dart';
import 'members_sheet.dart';
import 'message_actions.dart';
import 'message_content.dart';
import 'mentions/mention_candidates_provider.dart';
import 'read_state/deferred_read_state_update.dart';
import 'read_state/read_state_provider.dart';
import 'read_state/read_state_time.dart';
Expand Down Expand Up @@ -183,53 +184,39 @@ class ChannelDetailPage extends HookConsumerWidget {

return FrostedScaffold(
appBar: FrostedAppBar(
iconColor: context.colors.primary,
title: resolvedChannel.isDm
? _DmAppBarTitle(
channel: resolvedChannel,
currentPubkey: currentPubkey,
)
: Row(
children: [
Icon(
channelIcon(resolvedChannel),
size: 18,
color: context.colors.onSurfaceVariant,
SizedBox.square(
dimension: 22,
child: Center(
child: Icon(channelIcon(resolvedChannel), size: 18),
),
),
const SizedBox(width: Grid.half),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
resolveDmChannelDisplayLabel(
resolvedChannel,
currentPubkey: currentPubkey,
),
overflow: TextOverflow.ellipsis,
),
),
if (resolvedChannel.isEphemeral) ...[
const SizedBox(width: Grid.quarter),
_HeaderEphemeralBadge(channel: resolvedChannel),
],
],
),
if (resolvedChannel.isStream)
Text(
resolvedChannel.description.isNotEmpty
? resolvedChannel.description
: '${resolvedChannel.memberCount} member${resolvedChannel.memberCount == 1 ? '' : 's'}',
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
Flexible(
child: Text(
resolveDmChannelDisplayLabel(
resolvedChannel,
currentPubkey: currentPubkey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (resolvedChannel.isEphemeral) ...[
const SizedBox(width: Grid.quarter),
_HeaderEphemeralBadge(channel: resolvedChannel),
],
],
),
),
Expand All @@ -243,6 +230,7 @@ class ChannelDetailPage extends HookConsumerWidget {
),
if (!resolvedChannel.isDm)
IconButton(
color: context.colors.primary,
onPressed: () async {
final shouldClose = await showModalBottomSheet<bool>(
context: context,
Expand All @@ -259,7 +247,7 @@ class ChannelDetailPage extends HookConsumerWidget {
}
},
tooltip: 'Manage channel',
icon: const Icon(LucideIcons.ellipsis),
icon: const Icon(LucideIcons.ellipsisVertical, size: 22),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class _MembersButton extends ConsumerWidget {
.isNotEmpty;

return IconButton(
color: context.colors.primary,
onPressed: () {
showModalBottomSheet<void>(
context: context,
Expand All @@ -95,7 +96,7 @@ class _MembersButton extends ConsumerWidget {
icon: Stack(
clipBehavior: Clip.none,
children: [
const Icon(LucideIcons.users),
const Icon(LucideIcons.users, size: 22),
if (hasWorkingBot)
Positioned(
top: -2,
Expand Down
161 changes: 97 additions & 64 deletions mobile/lib/features/channels/channel_detail_page/message_bubble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ class _MessageBubble extends ConsumerWidget {

// Build mention names map from event p-tags.
final userCache = ref.watch(userCacheProvider);
final knownAgentPubkeys = ref.watch(
mentionAgentPubkeysProvider(currentChannelId),
);
final mentionNames = <String, String>{};
final agentMentionPubkeys = <String>{};
for (final mpk in message.mentionPubkeys) {
final p = userCache[mpk.toLowerCase()];
final normalizedPubkey = mpk.toLowerCase();
final p = userCache[normalizedPubkey];
if (p?.displayName != null) {
mentionNames[mpk.toLowerCase()] = p!.displayName!;
mentionNames[normalizedPubkey] = p!.displayName!;
}
if (knownAgentPubkeys.contains(normalizedPubkey)) {
agentMentionPubkeys.add(normalizedPubkey);
}
}

Expand Down Expand Up @@ -67,70 +75,74 @@ class _MessageBubble extends ConsumerWidget {
child: _UserAvatar(profile: profile, pubkey: message.pubkey),
)
else
const SizedBox(width: 28),
const SizedBox(width: 36),
const SizedBox(width: Grid.xxs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showAuthor)
Padding(
padding: const EdgeInsets.only(bottom: Grid.quarter),
child: Row(
children: [
GestureDetector(
onTap: () =>
showUserProfileSheet(context, message.pubkey),
child: Text(
displayName,
style: context.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colors.onSurface,
child: Transform.translate(
offset: Offset(0, showAuthor ? -Grid.quarter : 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showAuthor)
Padding(
padding: const EdgeInsets.only(bottom: Grid.quarter),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () =>
showUserProfileSheet(context, message.pubkey),
child: Text(
displayName,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: context.colors.onSurface,
),
),
),
),
const SizedBox(width: Grid.xxs),
Text(
formatMessageTime(message.createdAt),
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
if (message.edited) ...[
const SizedBox(width: Grid.half),
Text(
'(edited)',
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
fontStyle: FontStyle.italic,
const SizedBox(width: Grid.xxs),
_messageTimestamp(context, message.createdAt),
if (message.edited) ...[
const SizedBox(width: Grid.half),
Text(
'(edited)',
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
),
],
],
],
),
),
MessageContent(
content: message.content,
mentionNames: mentionNames,
agentMentionPubkeys: agentMentionPubkeys,
channelNames: channelNames,
tags: message.tags,
baseStyle: context.textTheme.bodyLarge?.copyWith(
color: context.colors.onSurface,
),
onChannelTap: (channelId) {
openChannelLink(
context: context,
ref: ref,
channelId: channelId,
currentChannelId: currentChannelId,
);
},
onMentionTap: (pubkey) =>
showUserProfileSheet(context, pubkey),
),
MessageContent(
content: message.content,
mentionNames: mentionNames,
channelNames: channelNames,
tags: message.tags,
onChannelTap: (channelId) {
openChannelLink(
context: context,
ref: ref,
channelId: channelId,
currentChannelId: currentChannelId,
);
},
onMentionTap: (pubkey) =>
showUserProfileSheet(context, pubkey),
),
if (message.reactions.isNotEmpty)
ReactionRow(
reactions: message.reactions,
onToggle: (emoji) => toggleReaction(ref, message, emoji),
),
],
if (message.reactions.isNotEmpty)
ReactionRow(
reactions: message.reactions,
onToggle: (emoji) =>
toggleReaction(ref, message, emoji),
),
],
),
),
),
],
Expand All @@ -140,11 +152,28 @@ class _MessageBubble extends ConsumerWidget {
}
}

Widget _messageTimestamp(BuildContext context, int createdAt) {
return Text(
formatMessageTime(createdAt),
style: context.textTheme.labelSmall?.copyWith(
fontSize: 14,
height: 22 / 14,
letterSpacing: context.textTheme.titleSmall?.letterSpacing,
color: context.colors.onSurfaceVariant,
),
);
}

class _UserAvatar extends StatelessWidget {
final UserProfile? profile;
final String pubkey;
final double size;

const _UserAvatar({required this.profile, required this.pubkey});
const _UserAvatar({
required this.profile,
required this.pubkey,
this.size = 36,
});

@override
Widget build(BuildContext context) {
Expand All @@ -154,14 +183,18 @@ class _UserAvatar extends StatelessWidget {

return AvatarImage(
imageUrl: avatarUrl,
radius: 14,
radius: size / 2,
backgroundColor: context.colors.primaryContainer,
fallback: Text(
initial,
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
style:
(size > 28
? context.textTheme.labelMedium
: context.textTheme.labelSmall)
?.copyWith(
color: context.colors.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class _MessageList extends HookConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final displayEntries = groupMembershipTimelineEntries(entries);
final itemScrollController = useMemoized(ItemScrollController.new);
final itemPositionsListener = useMemoized(ItemPositionsListener.create);
final isLoadingOlder = useState(false);
Expand All @@ -35,12 +36,12 @@ class _MessageList extends HookConsumerWidget {

int? reversedIndexOf(String? messageId) {
if (messageId == null) return null;
final chronologicalIndex = entries.indexWhere(
(entry) => entry.message.id == messageId,
final chronologicalIndex = displayEntries.indexWhere(
(group) => group.any((entry) => entry.message.id == messageId),
);
return chronologicalIndex < 0
? null
: entries.length - 1 - chronologicalIndex;
: displayEntries.length - 1 - chronologicalIndex;
}

Future<void> scrollToLatest() async {
Expand Down Expand Up @@ -68,7 +69,7 @@ class _MessageList extends HookConsumerWidget {
.map((position) => position.index)
.reduce((a, b) => a > b ? a : b);
if (!hasUserScrolled.value ||
oldestVisible < entries.length - 3 ||
oldestVisible < displayEntries.length - 3 ||
isLoadingOlder.value) {
return;
}
Expand Down Expand Up @@ -201,10 +202,10 @@ class _MessageList extends HookConsumerWidget {
top: frostedAppBarHeight(context),
bottom: Grid.xxs,
),
itemCount: entries.length + (isLoadingOlder.value ? 1 : 0),
itemCount: displayEntries.length + (isLoadingOlder.value ? 1 : 0),
itemBuilder: (context, index) {
// Loading indicator at the top (last index in reversed list).
if (index >= entries.length) {
if (index >= displayEntries.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: Grid.xs),
child: Center(
Expand All @@ -218,12 +219,15 @@ class _MessageList extends HookConsumerWidget {
}

// Reversed list: index 0 = newest (bottom of screen).
final chronIdx = entries.length - 1 - index;
final entry = entries[chronIdx];
final chronIdx = displayEntries.length - 1 - index;
final entryGroup = displayEntries[chronIdx];
final entry = entryGroup.first;
final message = entry.message;

// Day boundary check — applies to all messages including system.
final prevEntry = chronIdx > 0 ? entries[chronIdx - 1] : null;
final prevEntry = chronIdx > 0
? displayEntries[chronIdx - 1].last
: null;
final prevMessage = prevEntry?.message;
final showDayDivider =
prevMessage == null ||
Expand All @@ -246,6 +250,9 @@ class _MessageList extends HookConsumerWidget {
if (message.isSystem)
_SystemMessageRow(
message: message,
groupedMessages: entryGroup.length > 1
? entryGroup.map((entry) => entry.message).toList()
: null,
channelId: channelId,
currentPubkey: currentPubkey,
allMessages: null,
Expand Down
Loading
Loading