**Major Features Added:** - **Inline Reply System**: Replace compose screen with inline reply boxes - **Thread Navigation**: Parent/child navigation with jump functionality - **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy - **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions **Frontend Changes:** - **ThreadedCommentWidget**: Complete rewrite with animations and navigation - **ThreadNode Model**: Added parent references and descendant counting - **ThreadedConversationScreen**: Integrated navigation handlers - **PostDetailScreen**: Replaced with threaded conversation view - **ComposeScreen**: Added reply indicators and context - **PostActions**: Fixed visibility checks for chain buttons **Backend Changes:** - **API Route**: Added /posts/:id/thread endpoint - **Post Repository**: Include allow_chain and visibility fields in feed - **Thread Handler**: Support for fetching post chains **UI/UX Improvements:** - **Reply Context**: Clear indication when replying to specific posts - **Character Counting**: 500 character limit with live counter - **Visual Hierarchy**: Depth-based indentation and styling - **Smooth Animations**: SizeTransition, FadeTransition, hover states - **Chain Navigation**: Parent/child buttons with visual feedback **Technical Enhancements:** - **Animation Controllers**: Proper lifecycle management - **State Management**: Clean separation of concerns - **Navigation Callbacks**: Reusable navigation system - **Error Handling**: Graceful fallbacks and user feedback This creates a Reddit-style threaded conversation experience with smooth animations, inline replies, and intuitive navigation between posts in a chain.
137 lines
4 KiB
Dart
137 lines
4 KiB
Dart
import 'profile.dart';
|
|
|
|
/// Types of notifications
|
|
enum NotificationType {
|
|
appreciate, // Someone appreciated your post
|
|
chain, // Someone chained your post
|
|
follow, // Someone followed you
|
|
follow_request, // Someone requested to follow you
|
|
new_follower, // Someone followed you (public or approved)
|
|
request_accepted, // Someone accepted your follow request
|
|
comment, // Someone commented on your post
|
|
mention, // Someone mentioned you
|
|
}
|
|
|
|
/// Notification model
|
|
class AppNotification {
|
|
final String id;
|
|
final NotificationType type;
|
|
final Profile? actor; // User who performed the action
|
|
final String? postId;
|
|
final String? postBody;
|
|
final Map<String, dynamic>? metadata;
|
|
final DateTime createdAt;
|
|
final bool isRead;
|
|
final DateTime? archivedAt;
|
|
|
|
const AppNotification({
|
|
required this.id,
|
|
required this.type,
|
|
this.actor,
|
|
this.postId,
|
|
this.postBody,
|
|
this.metadata,
|
|
required this.createdAt,
|
|
this.isRead = false,
|
|
this.archivedAt,
|
|
});
|
|
|
|
factory AppNotification.fromJson(Map<String, dynamic> json) {
|
|
final metadataValue = json['metadata'];
|
|
Map<String, dynamic>? metadata;
|
|
if (metadataValue is Map) {
|
|
metadata = Map<String, dynamic>.from(metadataValue);
|
|
}
|
|
|
|
// Extract post body from nested post object or direct field
|
|
String? postBody = json['post_body'] as String?;
|
|
if (postBody == null && json['post'] is Map) {
|
|
postBody = (json['post'] as Map)['body'] as String?;
|
|
}
|
|
|
|
return AppNotification(
|
|
id: json['id'] as String,
|
|
type: NotificationType.values.firstWhere(
|
|
(e) => e.name == json['type'],
|
|
orElse: () => NotificationType.appreciate,
|
|
),
|
|
actor: json['actor'] != null
|
|
? Profile.fromJson(json['actor'] as Map<String, dynamic>)
|
|
: null,
|
|
postId: json['post_id'] as String?,
|
|
postBody: postBody,
|
|
metadata: metadata,
|
|
createdAt: DateTime.parse(json['created_at'] as String),
|
|
isRead: json['is_read'] as bool? ?? false,
|
|
archivedAt: json['archived_at'] != null
|
|
? DateTime.parse(json['archived_at'] as String)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'type': type.name,
|
|
'actor': actor?.toJson(),
|
|
'post_id': postId,
|
|
'post_body': postBody,
|
|
'metadata': metadata,
|
|
'created_at': createdAt.toIso8601String(),
|
|
'is_read': isRead,
|
|
'archived_at': archivedAt?.toIso8601String(),
|
|
};
|
|
}
|
|
|
|
AppNotification copyWith({
|
|
String? id,
|
|
NotificationType? type,
|
|
Profile? actor,
|
|
String? postId,
|
|
String? postBody,
|
|
Map<String, dynamic>? metadata,
|
|
DateTime? createdAt,
|
|
bool? isRead,
|
|
DateTime? archivedAt,
|
|
}) {
|
|
return AppNotification(
|
|
id: id ?? this.id,
|
|
type: type ?? this.type,
|
|
actor: actor ?? this.actor,
|
|
postId: postId ?? this.postId,
|
|
postBody: postBody ?? this.postBody,
|
|
metadata: metadata ?? this.metadata,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
isRead: isRead ?? this.isRead,
|
|
archivedAt: archivedAt ?? this.archivedAt,
|
|
);
|
|
}
|
|
|
|
String? get followerIdFromMetadata {
|
|
final value = metadata?['follower_id'];
|
|
return value is String ? value : null;
|
|
}
|
|
|
|
String get message {
|
|
final actorName = actor?.displayName ?? 'Someone';
|
|
switch (type) {
|
|
case NotificationType.appreciate:
|
|
return '$actorName appreciated your post';
|
|
case NotificationType.chain:
|
|
return '$actorName chained your post';
|
|
case NotificationType.follow:
|
|
return '$actorName started following you';
|
|
case NotificationType.follow_request:
|
|
return '$actorName requested to follow you';
|
|
case NotificationType.new_follower:
|
|
return '$actorName followed you';
|
|
case NotificationType.request_accepted:
|
|
return '$actorName accepted your follow request';
|
|
case NotificationType.comment:
|
|
return '$actorName commented on your post';
|
|
case NotificationType.mention:
|
|
return '$actorName mentioned you';
|
|
}
|
|
}
|
|
}
|