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,284 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'feed_item.dart';
|
||||
|
||||
/// Inbox filters, mirroring desktop's `InboxFilter` in
|
||||
/// `desktop/src/features/home/lib/inbox.ts`.
|
||||
enum InboxFilter {
|
||||
all,
|
||||
mention,
|
||||
thread,
|
||||
needsAction,
|
||||
activity,
|
||||
agentActivity,
|
||||
reminders,
|
||||
drafts,
|
||||
}
|
||||
|
||||
/// Category ordering used to pick the label for a grouped conversation.
|
||||
/// Mirrors desktop's `categoryPriority`.
|
||||
int categoryPriority(String category) {
|
||||
return switch (category) {
|
||||
'needs_action' => 0,
|
||||
'mention' => 1,
|
||||
'agent_activity' => 2,
|
||||
_ => 3,
|
||||
};
|
||||
}
|
||||
|
||||
/// Thread reference from NIP-10 tags — desktop's `getThreadReference`.
|
||||
({String? parentId, String? rootId}) threadReferenceOf(
|
||||
List<List<String>> tags,
|
||||
) {
|
||||
List<String>? rootTag;
|
||||
List<String>? replyTag;
|
||||
for (final tag in tags) {
|
||||
if (tag.length >= 4 && tag[0] == 'e') {
|
||||
if (tag[3] == 'root') rootTag = tag;
|
||||
if (tag[3] == 'reply') replyTag = tag;
|
||||
}
|
||||
}
|
||||
if (replyTag == null) return (parentId: null, rootId: null);
|
||||
final parentId = replyTag[1];
|
||||
return (parentId: parentId, rootId: rootTag?[1] ?? parentId);
|
||||
}
|
||||
|
||||
/// Broadcast replies surface at the channel top level — desktop's
|
||||
/// `isBroadcastReply`.
|
||||
bool isBroadcastReply(List<List<String>> tags) {
|
||||
return tags.any(
|
||||
(tag) => tag.length >= 2 && tag[0] == 'broadcast' && tag[1] == '1',
|
||||
);
|
||||
}
|
||||
|
||||
/// A thread reply that is not broadcast — desktop's `isThreadReply`.
|
||||
bool isThreadReply(List<List<String>> tags) {
|
||||
return threadReferenceOf(tags).parentId != null && !isBroadcastReply(tags);
|
||||
}
|
||||
|
||||
/// Stable conversation identity: `rootId ?? parentId ?? event.id`, except
|
||||
/// ordinary (non-thread) DM messages, which group by DM channel identity so
|
||||
/// one DM conversation renders as one row. Mirrors desktop's
|
||||
/// `getInboxConversationId`.
|
||||
String inboxConversationId(
|
||||
List<List<String>> tags,
|
||||
String eventId, {
|
||||
String? dmChannelId,
|
||||
}) {
|
||||
final thread = threadReferenceOf(tags);
|
||||
final threadId = thread.rootId ?? thread.parentId;
|
||||
if (threadId != null) return threadId;
|
||||
if (dmChannelId != null) return 'dm:$dmChannelId';
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/// One conversation-grouped inbox row. Mirrors desktop's `InboxItem`.
|
||||
@immutable
|
||||
class InboxItem {
|
||||
/// Stable conversation identity for the thread group.
|
||||
final String conversationId;
|
||||
|
||||
/// Representative (latest) event id.
|
||||
final String id;
|
||||
|
||||
/// Representative (latest) feed item.
|
||||
final FeedItem item;
|
||||
|
||||
/// All events grouped into this conversation, unordered.
|
||||
final List<FeedItem> groupItems;
|
||||
|
||||
/// Distinct categories present in the group, priority-sorted.
|
||||
final List<String> categories;
|
||||
|
||||
final bool isActionRequired;
|
||||
final int latestActivityAt;
|
||||
|
||||
const InboxItem({
|
||||
required this.conversationId,
|
||||
required this.id,
|
||||
required this.item,
|
||||
required this.groupItems,
|
||||
required this.categories,
|
||||
required this.isActionRequired,
|
||||
required this.latestActivityAt,
|
||||
});
|
||||
|
||||
/// Root id if any grouped event carries thread-reply tags.
|
||||
String? get threadRootId {
|
||||
for (final candidate in [item, ...groupItems]) {
|
||||
if (isThreadReply(candidate.tags)) {
|
||||
return threadReferenceOf(candidate.tags).rootId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The event the row should deep-link to: the oldest event in the group
|
||||
/// newer than [readAt] (oldest unread), falling back to the latest event.
|
||||
FeedItem deepLinkTarget(int? readAt) {
|
||||
if (readAt != null) {
|
||||
FeedItem? oldestUnread;
|
||||
for (final candidate in groupItems) {
|
||||
if (candidate.createdAt <= readAt) continue;
|
||||
if (oldestUnread == null ||
|
||||
candidate.createdAt < oldestUnread.createdAt) {
|
||||
oldestUnread = candidate;
|
||||
}
|
||||
}
|
||||
if (oldestUnread != null) return oldestUnread;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// Contextual row label — desktop's `getInboxTypeLabel`:
|
||||
/// "DM from X" / "Mentioned in" / "Needs action in" / "Thread in" / "... in".
|
||||
({String text, String? channelLabel}) inboxTypeLabel(
|
||||
InboxItem item, {
|
||||
required String? channelName,
|
||||
required bool isDm,
|
||||
required String senderLabel,
|
||||
}) {
|
||||
if (isDm) {
|
||||
return (
|
||||
text: senderLabel.isNotEmpty ? 'DM from $senderLabel' : 'DM',
|
||||
channelLabel: null,
|
||||
);
|
||||
}
|
||||
|
||||
final category = item.categories.firstOrNull ?? item.item.category;
|
||||
if (category == 'mention') {
|
||||
return (
|
||||
text: channelName != null ? 'Mentioned in' : 'Mentioned',
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
if (category == 'needs_action') {
|
||||
return (
|
||||
text: channelName != null ? 'Needs action in' : 'Needs action',
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
if (item.threadRootId != null) {
|
||||
return (
|
||||
text: channelName != null ? 'Thread in' : 'Thread',
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
|
||||
final headline = item.item.headline;
|
||||
return (
|
||||
text: channelName != null ? '$headline in' : headline,
|
||||
channelLabel: channelName,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether [item] matches [filter] — desktop's `matchesInboxFilter`.
|
||||
/// `reminders` and `drafts` are separate surfaces and never match here.
|
||||
bool matchesInboxFilter(InboxItem item, InboxFilter filter) {
|
||||
return switch (filter) {
|
||||
InboxFilter.all => true,
|
||||
InboxFilter.thread => [
|
||||
item.item,
|
||||
...item.groupItems,
|
||||
].any((i) => isThreadReply(i.tags)),
|
||||
InboxFilter.mention => item.categories.contains('mention'),
|
||||
InboxFilter.needsAction => item.categories.contains('needs_action'),
|
||||
InboxFilter.activity => item.categories.contains('activity'),
|
||||
InboxFilter.agentActivity => item.categories.contains('agent_activity'),
|
||||
InboxFilter.reminders || InboxFilter.drafts => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Group raw feed items into conversation rows sorted by latest activity.
|
||||
/// [isDmChannel] identifies DM channels so their ordinary top-level messages
|
||||
/// collapse into one row per DM conversation. Mirrors desktop's
|
||||
/// `buildInboxItems`.
|
||||
List<InboxItem> buildInboxItems(
|
||||
Iterable<FeedItem> feedItems, {
|
||||
bool Function(String channelId)? isDmChannel,
|
||||
}) {
|
||||
final groups = <String, List<FeedItem>>{};
|
||||
for (final item in feedItems) {
|
||||
final channelId = item.channelId;
|
||||
final dmChannelId =
|
||||
channelId != null && (isDmChannel?.call(channelId) ?? false)
|
||||
? channelId
|
||||
: null;
|
||||
final key = inboxConversationId(
|
||||
item.tags,
|
||||
item.id,
|
||||
dmChannelId: dmChannelId,
|
||||
);
|
||||
groups.putIfAbsent(key, () => []).add(item);
|
||||
}
|
||||
|
||||
final rows = <InboxItem>[];
|
||||
for (final entry in groups.entries) {
|
||||
final items = entry.value;
|
||||
var latest = items.first;
|
||||
var latestActivityAt = 0;
|
||||
for (final item in items) {
|
||||
if (item.createdAt > latest.createdAt) latest = item;
|
||||
if (item.createdAt > latestActivityAt) latestActivityAt = item.createdAt;
|
||||
}
|
||||
final categories = items.map((i) => i.category).toSet().toList()
|
||||
..sort((a, b) => categoryPriority(a).compareTo(categoryPriority(b)));
|
||||
|
||||
rows.add(
|
||||
InboxItem(
|
||||
conversationId: entry.key,
|
||||
id: latest.id,
|
||||
item: latest,
|
||||
groupItems: List.unmodifiable(items),
|
||||
categories: List.unmodifiable(categories),
|
||||
isActionRequired: categories.contains('needs_action'),
|
||||
latestActivityAt: latestActivityAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
rows.sort((a, b) => b.latestActivityAt.compareTo(a.latestActivityAt));
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// Date-bucket label for section headers — desktop's `groupInboxItems`:
|
||||
/// Today / Yesterday / weekday (within 7 days) / short date.
|
||||
String inboxDayLabel(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) return 'Today';
|
||||
if (dayDiff == 1) return 'Yesterday';
|
||||
if (dayDiff < 7 && dayDiff > 0) {
|
||||
const weekdays = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
return weekdays[date.weekday - 1];
|
||||
}
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
final label = '${months[date.month - 1]} ${date.day}';
|
||||
return date.year == current.year ? label : '$label, ${date.year}';
|
||||
}
|
||||
Reference in New Issue
Block a user