Files
buzz/mobile/lib/features/activity/activity_page/inbox_row.dart
klopez4212 6da45ac5cf Polish mobile message and search layouts (#3121)
## Summary

- align message typography, avatars, metadata, and spacing across mobile
surfaces
- improve message follow behavior, touch feedback, and Activity popover
motion
- refine Search motion, gutters, and explicit recent-search history

## Snapshots

### Home


![Home](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--01-home.png)

### Activity


![Activity](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--02-activity.png)

### Search


![Search](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--03-search.png)

## Testing

- `just mobile-check`
- `just mobile-test` (749 passed, 1 skipped)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
2026-07-28 17:20:50 +01:00

248 lines
8.2 KiB
Dart

part of '../activity_page.dart';
/// "New" boundary between unread and previously read rows.
class _NewBoundaryDivider extends StatelessWidget {
const _NewBoundaryDivider();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: Grid.gutter,
vertical: Grid.half,
),
child: Row(
children: [
Expanded(
child: Divider(color: context.colors.error.withValues(alpha: 0.5)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
child: Text(
'New',
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.error,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Divider(color: context.colors.error.withValues(alpha: 0.5)),
),
],
),
);
}
}
/// One conversation row, matching desktop's inbox item hierarchy:
/// avatar | sender + unread dot + time | contextual label | preview.
class _InboxRow extends ConsumerWidget {
final InboxItem item;
final Channel? channel;
final String? currentPubkey;
final bool isDone;
final VoidCallback onTap;
final VoidCallback onMarkRead;
final VoidCallback onMarkUnread;
const _InboxRow({
required this.item,
required this.channel,
required this.currentPubkey,
required this.isDone,
required this.onTap,
required this.onMarkRead,
required this.onMarkUnread,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userCache = ref.watch(userCacheProvider);
final profile = userCache[item.item.pubkey.toLowerCase()];
final senderLabel = profile?.displayName ?? shortPubkey(item.item.pubkey);
final isDm = channel?.isDm ?? false;
final channelName = channel != null && !isDm
? (channel!.name.isNotEmpty ? channel!.name : null)
: null;
final label = inboxTypeLabel(
item,
channelName: channelName,
isDm: isDm,
senderLabel: senderLabel,
);
final mutedColor = context.colors.onSurfaceVariant;
final labelColor = item.isActionRequired && !isDone
? context.colors.tertiary
: mutedColor;
return InkWell(
key: ValueKey('inbox-row-${item.id}'),
onTap: onTap,
onLongPress: () => _showRowActions(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Grid.gutter,
vertical: Grid.twelve,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_RowAvatar(pubkey: item.item.pubkey, profile: profile),
const SizedBox(width: messageAvatarContentGap),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sender + unread dot + timestamp.
Row(
children: [
Expanded(
child: MessageAuthorMeta(
displayName: senderLabel,
username: messageUsernameLabel(profile),
timestamp: _inboxTimestamp(item.latestActivityAt),
nameColor: context.colors.onSurface,
metadataColor: mutedColor,
nameStyle: activityUsernameTextStyle,
metadataStyle: activityTimestampTextStyle,
displayNameKey: ValueKey(
'activity-author-${item.id}',
),
usernameKey: ValueKey('activity-username-${item.id}'),
timestampKey: ValueKey(
'activity-timestamp-${item.id}',
),
),
),
if (!isDone) ...[
const SizedBox(width: Grid.xxs),
Container(
key: ValueKey('inbox-unread-dot-${item.id}'),
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context.colors.primary,
),
),
],
],
),
const SizedBox(height: Grid.quarter),
// Contextual label: "Mentioned in #channel" etc.
Row(
children: [
Flexible(
child: Text(
label.text,
style: activityContextTextStyle.copyWith(
color: labelColor,
),
overflow: TextOverflow.ellipsis,
),
),
if (label.channelLabel != null) ...[
const SizedBox(width: Grid.half),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Grid.half + Grid.quarter,
vertical: Grid.quarter / 2,
),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(Grid.half),
),
child: Text(
'#${label.channelLabel}',
style: activityContextTextStyle.copyWith(
color: mutedColor,
),
overflow: TextOverflow.ellipsis,
),
),
),
],
],
),
const SizedBox(height: Grid.half),
// Message preview.
MessageContent(
content: item.item.displayContent,
tags: item.item.tags,
maxLines: 2,
baseStyle: activityPreviewTextStyle.copyWith(
color: context.colors.onSurface,
),
),
],
),
),
],
),
),
);
}
void _showRowActions(BuildContext context) {
showModalBottomSheet<void>(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
isDone ? LucideIcons.mail : LucideIcons.mailOpen,
size: 20,
),
title: Text(isDone ? 'Mark unread' : 'Mark as read'),
onTap: () {
Navigator.of(sheetContext).pop();
isDone ? onMarkUnread() : onMarkRead();
},
),
ListTile(
leading: const Icon(LucideIcons.externalLink, size: 20),
title: const Text('Open conversation'),
onTap: () {
Navigator.of(sheetContext).pop();
onTap();
},
),
],
),
),
);
}
}
class _RowAvatar extends StatelessWidget {
final String pubkey;
final UserProfile? profile;
const _RowAvatar({required this.pubkey, required this.profile});
@override
Widget build(BuildContext context) {
final initial =
profile?.initial ?? (pubkey.isNotEmpty ? pubkey[0].toUpperCase() : '?');
return AvatarImage(
imageUrl: profile?.avatarUrl,
radius: activityAvatarSize / 2,
backgroundColor: context.colors.primaryContainer,
fallback: Text(
initial,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: context.colors.onPrimaryContainer,
),
),
);
}
}