feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
part of '../activity_page.dart';
|
||||
|
||||
String _filterLabel(BuildContext context, InboxFilter filter) =>
|
||||
switch (filter) {
|
||||
InboxFilter.all => context.l10n.filterAll,
|
||||
InboxFilter.mention => context.l10n.filterMentions,
|
||||
InboxFilter.thread => context.l10n.filterThreads,
|
||||
InboxFilter.needsAction => context.l10n.filterNeedsAction,
|
||||
InboxFilter.activity => context.l10n.filterActivity,
|
||||
InboxFilter.agentActivity => context.l10n.filterAgents,
|
||||
InboxFilter.reminders => context.l10n.filterReminders,
|
||||
InboxFilter.drafts => context.l10n.filterDrafts,
|
||||
};
|
||||
|
||||
class _ActivityActionsPill extends StatelessWidget {
|
||||
final InboxFilter filter;
|
||||
final int dueReminderCount;
|
||||
final int draftCount;
|
||||
final bool unreadOnly;
|
||||
final int unreadCount;
|
||||
final ValueChanged<InboxFilter> onFilterChanged;
|
||||
final ValueChanged<bool> onUnreadOnlyChanged;
|
||||
final VoidCallback onMarkAllRead;
|
||||
|
||||
const _ActivityActionsPill({
|
||||
required this.filter,
|
||||
required this.dueReminderCount,
|
||||
required this.draftCount,
|
||||
required this.unreadOnly,
|
||||
required this.unreadCount,
|
||||
required this.onFilterChanged,
|
||||
required this.onUnreadOnlyChanged,
|
||||
required this.onMarkAllRead,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(Radii.full),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(Radii.full),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.quarter),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_FilterMenuButton(
|
||||
filter: filter,
|
||||
dueReminderCount: dueReminderCount,
|
||||
draftCount: draftCount,
|
||||
onChanged: onFilterChanged,
|
||||
),
|
||||
_InboxOptionsButton(
|
||||
unreadOnly: unreadOnly,
|
||||
unreadCount: unreadCount,
|
||||
onUnreadOnlyChanged: onUnreadOnlyChanged,
|
||||
onMarkAllRead: onMarkAllRead,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Compact filter dropdown replacing the old chip rail — mirrors desktop's
|
||||
/// inbox filter menu (`FILTER_OPTIONS`).
|
||||
class _FilterMenuButton extends StatelessWidget {
|
||||
final InboxFilter filter;
|
||||
final int dueReminderCount;
|
||||
final int draftCount;
|
||||
final ValueChanged<InboxFilter> onChanged;
|
||||
|
||||
const _FilterMenuButton({
|
||||
required this.filter,
|
||||
required this.dueReminderCount,
|
||||
required this.draftCount,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
builder: (buttonContext) => InkWell(
|
||||
key: const ValueKey('activity-filter-menu'),
|
||||
borderRadius: BorderRadius.circular(Radii.md),
|
||||
onTap: () async {
|
||||
final selected = await showAnchoredPopover<InboxFilter>(
|
||||
context: buttonContext,
|
||||
width: 240,
|
||||
alignment: AnchoredPopoverAlignment.start,
|
||||
offset: const Offset(0, Grid.half),
|
||||
menuPadding: const EdgeInsets.symmetric(vertical: Grid.half),
|
||||
surfaceKey: const ValueKey('activity-filter-popover'),
|
||||
items: [
|
||||
for (final entry in InboxFilter.values)
|
||||
PopupMenuItem(
|
||||
value: entry,
|
||||
height: Grid.xl,
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.twelve),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Grid.sm,
|
||||
child: entry == filter
|
||||
? Icon(
|
||||
LucideIcons.check,
|
||||
size: 16,
|
||||
color: context.colors.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_filterLabel(context, entry),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (entry == InboxFilter.reminders &&
|
||||
dueReminderCount > 0)
|
||||
_CountBadge(count: dueReminderCount)
|
||||
else if (entry == InboxFilter.drafts &&
|
||||
draftCount > 0)
|
||||
_CountBadge(count: draftCount),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (buttonContext.mounted && selected != null) onChanged(selected);
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: Grid.xl),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_filterLabel(context, filter),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: navigationPrimaryForeground(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.quarter),
|
||||
Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 16,
|
||||
color: navigationPrimaryForeground(context),
|
||||
),
|
||||
if (dueReminderCount > 0 || draftCount > 0) ...[
|
||||
const SizedBox(width: Grid.quarter),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.colors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountBadge extends StatelessWidget {
|
||||
final int count;
|
||||
|
||||
const _CountBadge({required this.count});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Grid.half + Grid.quarter,
|
||||
vertical: Grid.quarter,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: navigationPrimaryForeground(context),
|
||||
borderRadius: BorderRadius.circular(Grid.xxs),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Overflow menu with the unread-only toggle and mark-all-read, mirroring
|
||||
/// desktop's inbox options popover.
|
||||
class _InboxOptionsButton extends StatelessWidget {
|
||||
final bool unreadOnly;
|
||||
final int unreadCount;
|
||||
final ValueChanged<bool> onUnreadOnlyChanged;
|
||||
final VoidCallback onMarkAllRead;
|
||||
|
||||
const _InboxOptionsButton({
|
||||
required this.unreadOnly,
|
||||
required this.unreadCount,
|
||||
required this.onUnreadOnlyChanged,
|
||||
required this.onMarkAllRead,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
builder: (buttonContext) => IconButton(
|
||||
key: const ValueKey('activity-options-menu'),
|
||||
tooltip: context.l10n.activityOptions,
|
||||
color: navigationPrimaryForeground(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: Grid.xl,
|
||||
height: Grid.xl,
|
||||
),
|
||||
icon: const Icon(LucideIcons.ellipsis, size: 20),
|
||||
onPressed: () async {
|
||||
final selected = await showAnchoredPopover<String>(
|
||||
context: buttonContext,
|
||||
width: 216,
|
||||
alignment: AnchoredPopoverAlignment.end,
|
||||
surfaceKey: const ValueKey('activity-options-popover'),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: 'unread-only',
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
unreadOnly
|
||||
? context.l10n.showAll
|
||||
: context.l10n.showUnread,
|
||||
),
|
||||
),
|
||||
if (unreadOnly)
|
||||
Icon(
|
||||
LucideIcons.check,
|
||||
size: 16,
|
||||
color: context.colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'mark-all-read',
|
||||
enabled: unreadCount > 0,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.markAllRead)),
|
||||
if (unreadCount > 0)
|
||||
Text(
|
||||
'$unreadCount',
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!buttonContext.mounted || selected == null) return;
|
||||
if (selected == 'unread-only') onUnreadOnlyChanged(!unreadOnly);
|
||||
if (selected == 'mark-all-read') onMarkAllRead();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
part of '../activity_page.dart';
|
||||
|
||||
const _inboxSwipeActionInset = Grid.half;
|
||||
const _inboxSwipeActionLabelMinWidth = Grid.xxl;
|
||||
const _inboxSwipeLabelRevealWidth =
|
||||
_inboxSwipeActionLabelMinWidth + (_inboxSwipeActionInset * 2);
|
||||
|
||||
String _localizedInboxHeadline(BuildContext context, InboxItem item) {
|
||||
final l10n = context.l10n;
|
||||
return switch (item.item.kind) {
|
||||
45001 => l10n.forumPost,
|
||||
45003 => l10n.forumReply,
|
||||
46010 => l10n.approvalRequested,
|
||||
43001 => l10n.jobRequested,
|
||||
43002 => l10n.jobAccepted,
|
||||
43003 => l10n.progressUpdate,
|
||||
43004 => l10n.jobResult,
|
||||
43005 => l10n.jobCancelled,
|
||||
43006 => l10n.jobFailed,
|
||||
_ => switch (item.item.category) {
|
||||
'mention' => l10n.mention,
|
||||
'agent_activity' => l10n.agentUpdate,
|
||||
_ => l10n.channelUpdate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
({String text, String? channelLabel}) _localizedInboxTypeLabel(
|
||||
BuildContext context,
|
||||
InboxItem item, {
|
||||
required String? channelName,
|
||||
required bool isDm,
|
||||
required String senderLabel,
|
||||
}) {
|
||||
final l10n = context.l10n;
|
||||
if (isDm) {
|
||||
return (
|
||||
text: senderLabel.isNotEmpty ? l10n.dmFrom(senderLabel) : l10n.directMessage,
|
||||
channelLabel: null,
|
||||
);
|
||||
}
|
||||
|
||||
final category = item.categories.firstOrNull ?? item.item.category;
|
||||
if (category == 'mention') {
|
||||
return (
|
||||
text: channelName != null ? l10n.mentionedIn : l10n.mentioned,
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
if (category == 'needs_action') {
|
||||
return (
|
||||
text: channelName != null ? l10n.needsActionIn : l10n.needsAction,
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
if (item.threadRootId != null) {
|
||||
return (
|
||||
text: channelName != null ? l10n.threadInLabel : l10n.threadTitle,
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
|
||||
final headline = _localizedInboxHeadline(context, item);
|
||||
return (
|
||||
text: channelName != null ? l10n.headlineIn(headline) : headline,
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
|
||||
String _localizedInboxPreview(BuildContext context, InboxItem item) {
|
||||
final content = item.item.content.trim();
|
||||
if (content.isNotEmpty) return content;
|
||||
return item.item.kind == 46010
|
||||
? context.l10n.workflowWaitingApproval
|
||||
: context.l10n.noAdditionalDetails;
|
||||
}
|
||||
|
||||
/// "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(
|
||||
context.l10n.newActivity,
|
||||
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.
|
||||
///
|
||||
/// Swiping left reveals the row's read-state action. Opening remains a tap or
|
||||
/// long-press action, so the swipe has one clear, easily recoverable outcome.
|
||||
class _InboxRow extends HookConsumerWidget {
|
||||
final InboxItem item;
|
||||
final Channel? channel;
|
||||
final String? currentPubkey;
|
||||
final bool isDone;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onMarkRead;
|
||||
final VoidCallback onMarkUnread;
|
||||
|
||||
const _InboxRow({
|
||||
super.key,
|
||||
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) {
|
||||
const restingRevealWidth = 132.0;
|
||||
const commitThreshold = 0.58;
|
||||
final revealAmount = useState(0.0);
|
||||
final isDragging = useState(false);
|
||||
final labelHapticFired = useRef(false);
|
||||
final userCache = ref.watch(userCacheProvider);
|
||||
final profile = userCache[item.item.pubkey.toLowerCase()];
|
||||
final senderLabel = profile?.displayName ?? shortPubkey(item.item.pubkey);
|
||||
final profileMentionNames = {
|
||||
for (final pubkey in mentionedPubkeysFromTags(item.item.tags))
|
||||
if (userCache[pubkey]?.displayName?.trim().isNotEmpty == true)
|
||||
pubkey: userCache[pubkey]!.displayName!.trim(),
|
||||
};
|
||||
final mentionPubkeys = mentionedPubkeysFromTags(item.item.tags);
|
||||
final knownAgentPubkeys = channel == null
|
||||
? ref.watch(knownAgentPubkeysProvider)
|
||||
: ref.watch(agentMentionPubkeysProvider(channel!.id));
|
||||
final agentMentionPubkeys = agentPubkeysWithProfileOwners(
|
||||
knownAgentPubkeys: knownAgentPubkeys,
|
||||
profileOwnedAgentPubkeys: [
|
||||
for (final profile in userCache.values)
|
||||
if (profile.ownerPubkey != null) profile.pubkey,
|
||||
],
|
||||
);
|
||||
final mentionNames = mentionNamesWithDirectoryLabels(
|
||||
mentionPubkeys: mentionPubkeys,
|
||||
profileMentionNames: profileMentionNames,
|
||||
directoryDisplayNames: ref.watch(agentDirectoryDisplayNamesProvider),
|
||||
agentMentionPubkeys: agentMentionPubkeys,
|
||||
);
|
||||
|
||||
final isDm = channel?.isDm ?? false;
|
||||
final channelName = channel != null && !isDm
|
||||
? (channel!.name.isNotEmpty ? channel!.name : null)
|
||||
: null;
|
||||
final label = _localizedInboxTypeLabel(
|
||||
context,
|
||||
item,
|
||||
channelName: channelName,
|
||||
isDm: isDm,
|
||||
senderLabel: senderLabel,
|
||||
);
|
||||
|
||||
final mutedColor = context.colors.onSurfaceVariant;
|
||||
final labelColor = item.isActionRequired && !isDone
|
||||
? context.colors.tertiary
|
||||
: mutedColor;
|
||||
|
||||
final reducedMotion = MediaQuery.of(context).disableAnimations;
|
||||
final swipeDirection = Directionality.of(context) == TextDirection.ltr
|
||||
? 1.0
|
||||
: -1.0;
|
||||
void closeActions() => revealAmount.value = 0;
|
||||
void toggleReadState() {
|
||||
closeActions();
|
||||
isDone ? onMarkUnread() : onMarkRead();
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final actionExtent = constraints.maxWidth;
|
||||
final revealedWidth = revealAmount.value
|
||||
.clamp(0, actionExtent)
|
||||
.toDouble();
|
||||
final actionColor = isDone
|
||||
? context.colors.primary
|
||||
: context.appColors.success;
|
||||
return ClipRect(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (revealedWidth > 0)
|
||||
PositionedDirectional(
|
||||
top: 0,
|
||||
end: 0,
|
||||
bottom: 0,
|
||||
width: revealedWidth,
|
||||
child: Padding(
|
||||
key: ValueKey('inbox-swipe-background-${item.id}'),
|
||||
padding: const EdgeInsets.all(_inboxSwipeActionInset),
|
||||
child: _InboxSwipeAction(
|
||||
key: ValueKey('inbox-swipe-read-${item.id}'),
|
||||
color: actionColor,
|
||||
foregroundColor: contrastForeground(actionColor),
|
||||
icon: isDone ? LucideIcons.mail : LucideIcons.mailOpen,
|
||||
label: isDone ? context.l10n.markUnread : context.l10n.markRead,
|
||||
onTap: toggleReadState,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSlide(
|
||||
offset: Offset(
|
||||
-swipeDirection * revealAmount.value / constraints.maxWidth,
|
||||
0,
|
||||
),
|
||||
duration: reducedMotion || isDragging.value
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 160),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: (_) {
|
||||
isDragging.value = true;
|
||||
labelHapticFired.value = false;
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
isDragging.value = true;
|
||||
final previous = revealAmount.value;
|
||||
final next =
|
||||
(previous - (details.delta.dx * swipeDirection))
|
||||
.clamp(0, actionExtent)
|
||||
.toDouble();
|
||||
if (!labelHapticFired.value &&
|
||||
previous < _inboxSwipeLabelRevealWidth &&
|
||||
next >= _inboxSwipeLabelRevealWidth) {
|
||||
labelHapticFired.value = true;
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
}
|
||||
revealAmount.value = next;
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
isDragging.value = false;
|
||||
final velocity = details.primaryVelocity ?? 0;
|
||||
final shouldCommit =
|
||||
(velocity * swipeDirection) < -900 ||
|
||||
revealAmount.value >= actionExtent * commitThreshold;
|
||||
if (shouldCommit) {
|
||||
toggleReadState();
|
||||
} else {
|
||||
revealAmount.value =
|
||||
(velocity * swipeDirection) < -200 ||
|
||||
revealAmount.value >= restingRevealWidth / 2
|
||||
? restingRevealWidth
|
||||
: 0;
|
||||
}
|
||||
},
|
||||
child: Material(
|
||||
color: context.colors.surface,
|
||||
child: 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(
|
||||
context,
|
||||
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: _localizedInboxPreview(context, item),
|
||||
mentionNames: mentionNames,
|
||||
agentMentionPubkeys: agentMentionPubkeys,
|
||||
tags: item.item.tags,
|
||||
maxLines: 2,
|
||||
baseStyle: activityPreviewTextStyle
|
||||
.copyWith(
|
||||
color: context.colors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showRowActions(BuildContext context) {
|
||||
showBuzzModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Grid.gutter,
|
||||
0,
|
||||
Grid.gutter,
|
||||
Grid.xs,
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isDone ? LucideIcons.mail : LucideIcons.mailOpen,
|
||||
),
|
||||
title: Text(
|
||||
isDone ? context.l10n.markUnread : context.l10n.markRead,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
isDone ? onMarkUnread() : onMarkRead();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.externalLink),
|
||||
title: Text(context.l10n.openConversation),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
onTap();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InboxSwipeAction extends StatelessWidget {
|
||||
final Color color;
|
||||
final Color foregroundColor;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _InboxSwipeAction({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.foregroundColor,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Material(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(Radii.full),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(Radii.full),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: label,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < Grid.lg) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final showLabel =
|
||||
constraints.maxWidth >= _inboxSwipeActionLabelMinWidth;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: foregroundColor),
|
||||
if (showLabel) ...[
|
||||
const SizedBox(height: Grid.quarter),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: foregroundColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
part of '../activity_page.dart';
|
||||
|
||||
/// Reminders surface for the Reminders filter — due/pending NIP-ER
|
||||
/// reminders that deep-link to their target message.
|
||||
class _RemindersList extends ConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final void Function(Reminder reminder) onOpen;
|
||||
final Future<void> Function() onRefresh;
|
||||
|
||||
const _RemindersList({
|
||||
required this.scrollController,
|
||||
required this.onOpen,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final remindersAsync = ref.watch(remindersProvider);
|
||||
final reminders = [
|
||||
for (final r in remindersAsync.value ?? const <Reminder>[])
|
||||
if (r.status == 'pending') r,
|
||||
];
|
||||
|
||||
if (remindersAsync.isLoading && reminders.isEmpty) {
|
||||
return Center(
|
||||
child: BuzzLoadingIndicator(
|
||||
size: 44,
|
||||
semanticLabel: context.l10n.loadingReminders,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (reminders.isEmpty) {
|
||||
return _EmptySurface(
|
||||
icon: LucideIcons.clock,
|
||||
message: context.l10n.noReminders,
|
||||
detail: context.l10n.remindersHint,
|
||||
);
|
||||
}
|
||||
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
return BeeRefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: _activityScrollPadding(context),
|
||||
itemCount: reminders.length,
|
||||
itemBuilder: (context, index) {
|
||||
final reminder = reminders[index];
|
||||
final due = reminder.isDue(now);
|
||||
final title = reminder.note?.trim().isNotEmpty == true
|
||||
? reminder.note!.trim()
|
||||
: reminder.target?.preview ?? context.l10n.reminderLabel;
|
||||
return ListTile(
|
||||
key: ValueKey('reminder-row-${reminder.id}'),
|
||||
leading: Icon(
|
||||
due ? LucideIcons.bellRing : LucideIcons.clock,
|
||||
size: 20,
|
||||
color: due
|
||||
? context.colors.primary
|
||||
: context.colors.onSurfaceVariant,
|
||||
),
|
||||
title: Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: reminder.notBefore != null
|
||||
? Text(
|
||||
due
|
||||
? context.l10n.dueNow
|
||||
: context.l10n.dueAt(
|
||||
_inboxTimestamp(context, reminder.notBefore!),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => onOpen(reminder),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drafts surface for the Drafts filter — locally saved unsent composer
|
||||
/// text that reopens the target composer.
|
||||
class _DraftsList extends StatelessWidget {
|
||||
final List<ComposeDraft> drafts;
|
||||
final ScrollController scrollController;
|
||||
final Map<String, Channel> channelById;
|
||||
final String? myPubkey;
|
||||
final void Function(ComposeDraft draft) onOpen;
|
||||
final void Function(ComposeDraft draft) onDelete;
|
||||
|
||||
const _DraftsList({
|
||||
required this.drafts,
|
||||
required this.scrollController,
|
||||
required this.channelById,
|
||||
required this.myPubkey,
|
||||
required this.onOpen,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (drafts.isEmpty) {
|
||||
return _EmptySurface(
|
||||
icon: LucideIcons.filePen,
|
||||
message: context.l10n.noDrafts,
|
||||
detail: context.l10n.draftsHint,
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: _activityScrollPadding(context),
|
||||
itemCount: drafts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final draft = drafts[index];
|
||||
final channel = channelById[draft.channelId];
|
||||
final destination = channel == null
|
||||
? context.l10n.unavailableChannel
|
||||
: channel.isDm
|
||||
? resolveDmChannelDisplayLabel(channel, currentPubkey: myPubkey)
|
||||
: '#${channel.name}';
|
||||
return ListTile(
|
||||
key: ValueKey('draft-row-${draft.key}'),
|
||||
leading: Icon(
|
||||
LucideIcons.filePen,
|
||||
size: 20,
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
title: Text(draft.text, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
draft.threadHeadId != null
|
||||
? context.l10n.threadIn(destination)
|
||||
: destination,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(LucideIcons.trash2, size: 18),
|
||||
tooltip: context.l10n.deleteDraft,
|
||||
onPressed: () => onDelete(draft),
|
||||
),
|
||||
onTap: () => onOpen(draft),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbox list timestamp — desktop's `formatInboxTimestamp`:
|
||||
/// time today, "Yesterday", short date this year, dated otherwise.
|
||||
String _inboxTimestamp(
|
||||
BuildContext context,
|
||||
int unixSeconds, {
|
||||
DateTime? now,
|
||||
}) {
|
||||
final current = now ?? DateTime.now();
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000);
|
||||
final today = DateTime(current.year, current.month, current.day);
|
||||
final day = DateTime(date.year, date.month, date.day);
|
||||
final dayDiff = today.difference(day).inDays;
|
||||
|
||||
if (dayDiff == 0) {
|
||||
final hour = date.hour % 12 == 0 ? 12 : date.hour % 12;
|
||||
final minute = date.minute.toString().padLeft(2, '0');
|
||||
final period = date.hour < 12 ? context.l10n.timeAm : context.l10n.timePm;
|
||||
return '$hour:$minute $period';
|
||||
}
|
||||
if (dayDiff == 1) return context.l10n.timeYesterday;
|
||||
|
||||
final months = [
|
||||
context.l10n.monthJan,
|
||||
context.l10n.monthFeb,
|
||||
context.l10n.monthMar,
|
||||
context.l10n.monthApr,
|
||||
context.l10n.monthMay,
|
||||
context.l10n.monthJun,
|
||||
context.l10n.monthJul,
|
||||
context.l10n.monthAug,
|
||||
context.l10n.monthSep,
|
||||
context.l10n.monthOct,
|
||||
context.l10n.monthNov,
|
||||
context.l10n.monthDec,
|
||||
];
|
||||
final month = months[date.month - 1];
|
||||
return date.year == current.year
|
||||
? context.l10n.shortDate(month, date.day)
|
||||
: context.l10n.shortDateYear(month, date.day, date.year);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
part of '../activity_page.dart';
|
||||
|
||||
class _LoadingSkeleton extends StatelessWidget {
|
||||
final ScrollController scrollController;
|
||||
|
||||
const _LoadingSkeleton({required this.scrollController});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
controller: scrollController,
|
||||
padding: _activityScrollPadding(
|
||||
context,
|
||||
horizontal: Grid.gutter,
|
||||
top: Grid.gutter,
|
||||
bottom: Grid.gutter,
|
||||
),
|
||||
itemCount: 8,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: Grid.xs),
|
||||
itemBuilder: (context, _) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.colors.outlineVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.twelve),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 140,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.outlineVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.outlineVariant.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.half),
|
||||
Container(
|
||||
width: 220,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.outlineVariant.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptySurface extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String message;
|
||||
final String? detail;
|
||||
|
||||
const _EmptySurface({required this.icon, required this.message, this.detail});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: Grid.lg, color: context.colors.onSurfaceVariant),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
message,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (detail != null) ...[
|
||||
const SizedBox(height: Grid.half),
|
||||
Text(
|
||||
detail!,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyFilterState extends StatelessWidget {
|
||||
final InboxFilter filter;
|
||||
final bool unreadOnly;
|
||||
|
||||
const _EmptyFilterState({required this.filter, required this.unreadOnly});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (unreadOnly) {
|
||||
return _EmptySurface(
|
||||
icon: LucideIcons.mailOpen,
|
||||
message: context.l10n.activityNoUnread,
|
||||
detail: context.l10n.activityUnreadHint,
|
||||
);
|
||||
}
|
||||
final (icon, message) = switch (filter) {
|
||||
InboxFilter.mention => (
|
||||
LucideIcons.atSign,
|
||||
context.l10n.activityNoMentions,
|
||||
),
|
||||
InboxFilter.thread => (
|
||||
LucideIcons.messageSquare,
|
||||
context.l10n.activityNoThreadReplies,
|
||||
),
|
||||
InboxFilter.needsAction => (
|
||||
LucideIcons.circleAlert,
|
||||
context.l10n.activityNothingNeedsAction,
|
||||
),
|
||||
InboxFilter.activity => (
|
||||
LucideIcons.activity,
|
||||
context.l10n.activityNoRecent,
|
||||
),
|
||||
InboxFilter.agentActivity => (
|
||||
LucideIcons.bot,
|
||||
context.l10n.activityNoAgentUpdates,
|
||||
),
|
||||
_ => (LucideIcons.bell, context.l10n.activityNoActivity),
|
||||
};
|
||||
return _EmptySurface(icon: icon, message: message);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorView extends StatelessWidget {
|
||||
final Future<void> Function() onRetry;
|
||||
|
||||
const _ErrorView({required this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.triangleAlert,
|
||||
size: Grid.lg,
|
||||
color: context.colors.error,
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
context.l10n.activityLoadFailed,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
FilledButton.icon(
|
||||
onPressed: () => unawaited(onRetry()),
|
||||
icon: const Icon(LucideIcons.refreshCcw, size: 16),
|
||||
label: Text(context.l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user