mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## The bug When you tap an item in the Activity inbox that refers to a message inside a thread (for example, someone replied to you or mentioned you in a thread reply), the app opened the channel at its top level. It did not open the thread, and it did not show you the message the notification was about. You had to hunt for the reply manually. ## The fix Activity items now keep track of two things: the root message of the thread and the specific message that triggered the notification. Tapping the item now: 1. Opens the thread detail view for that thread (instead of the channel's top level). 2. Scrolls to the specific message that triggered the notification. 3. Briefly highlights that message so it is easy to spot. This works for both direct replies and replies nested deeper in a thread, and it fetches the thread from the relay if it is not already loaded (for example, right after app launch). ## Testing - Flutter analyzer - 64 focused mobile tests covering direct and nested thread markers plus Activity navigation - Full pre-push suite (mobile, desktop, Rust, and Tauri tests) ## Manual verification Open Activity, tap a mention for a reply inside a thread, and confirm Buzz opens that thread, scrolls to the reply, and highlights it. --------- Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz> Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
138 lines
3.8 KiB
Dart
138 lines
3.8 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
@immutable
|
|
class FeedItem {
|
|
final String id;
|
|
final int kind;
|
|
final String pubkey;
|
|
final String content;
|
|
final int createdAt;
|
|
final String? channelId;
|
|
final String channelName;
|
|
final List<List<String>> tags;
|
|
final String
|
|
category; // "mention", "needs_action", "activity", "agent_activity"
|
|
|
|
const FeedItem({
|
|
required this.id,
|
|
required this.kind,
|
|
required this.pubkey,
|
|
required this.content,
|
|
required this.createdAt,
|
|
required this.channelId,
|
|
required this.channelName,
|
|
required this.tags,
|
|
required this.category,
|
|
});
|
|
|
|
factory FeedItem.fromJson(Map<String, dynamic> json) => FeedItem(
|
|
id: json['id'] as String,
|
|
kind: json['kind'] as int,
|
|
pubkey: json['pubkey'] as String,
|
|
content: json['content'] as String,
|
|
createdAt: json['created_at'] as int,
|
|
channelId: json['channel_id'] as String?,
|
|
channelName: (json['channel_name'] as String?) ?? '',
|
|
tags:
|
|
(json['tags'] as List<dynamic>?)
|
|
?.map((t) => (t as List<dynamic>).map((e) => e as String).toList())
|
|
.toList() ??
|
|
const [],
|
|
category: json['category'] as String,
|
|
);
|
|
|
|
/// The message whose thread directly contains this item, when this event is
|
|
/// a reply. For nested replies this is the direct parent, not the outer root.
|
|
String? get threadHeadId {
|
|
for (final tag in tags) {
|
|
if (tag.length >= 4 && tag[0] == 'e' && tag[3] == 'reply') {
|
|
return tag[1];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Human-readable headline based on event kind and category.
|
|
String get headline {
|
|
switch (kind) {
|
|
case 45001:
|
|
return 'Forum post';
|
|
case 45003:
|
|
return 'Forum reply';
|
|
case 46010:
|
|
return 'Approval requested';
|
|
case 43001:
|
|
return 'Job requested';
|
|
case 43002:
|
|
return 'Job accepted';
|
|
case 43003:
|
|
return 'Progress update';
|
|
case 43004:
|
|
return 'Job result';
|
|
case 43005:
|
|
return 'Job cancelled';
|
|
case 43006:
|
|
return 'Job failed';
|
|
default:
|
|
if (category == 'mention') return 'Mention';
|
|
if (category == 'agent_activity') return 'Agent update';
|
|
return 'Channel update';
|
|
}
|
|
}
|
|
|
|
/// Trimmed content, with a fallback for empty events.
|
|
String get displayContent {
|
|
final trimmed = content.trim();
|
|
if (trimmed.isNotEmpty) return trimmed;
|
|
if (kind == 46010) return 'A workflow is waiting for approval.';
|
|
return 'No additional details.';
|
|
}
|
|
}
|
|
|
|
/// Parsed response from GET /api/feed.
|
|
@immutable
|
|
class HomeFeedResponse {
|
|
final List<FeedItem> mentions;
|
|
final List<FeedItem> needsAction;
|
|
final List<FeedItem> activity;
|
|
final List<FeedItem> agentActivity;
|
|
|
|
HomeFeedResponse({
|
|
required this.mentions,
|
|
required this.needsAction,
|
|
required this.activity,
|
|
required this.agentActivity,
|
|
});
|
|
|
|
factory HomeFeedResponse.fromJson(Map<String, dynamic> json) {
|
|
final feed = json['feed'] as Map<String, dynamic>;
|
|
return HomeFeedResponse(
|
|
mentions: _parseItems(feed['mentions']),
|
|
needsAction: _parseItems(feed['needs_action']),
|
|
activity: _parseItems(feed['activity']),
|
|
agentActivity: _parseItems(feed['agent_activity']),
|
|
);
|
|
}
|
|
|
|
/// All items merged into a single list, sorted newest-first.
|
|
late final List<FeedItem> all = () {
|
|
final items = [...mentions, ...needsAction, ...activity, ...agentActivity];
|
|
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
return List<FeedItem>.unmodifiable(items);
|
|
}();
|
|
|
|
bool get isEmpty =>
|
|
mentions.isEmpty &&
|
|
needsAction.isEmpty &&
|
|
activity.isEmpty &&
|
|
agentActivity.isEmpty;
|
|
}
|
|
|
|
List<FeedItem> _parseItems(dynamic json) {
|
|
if (json == null) return const [];
|
|
return (json as List<dynamic>)
|
|
.cast<Map<String, dynamic>>()
|
|
.map(FeedItem.fromJson)
|
|
.toList();
|
|
}
|