sojorn/sojorn_app/lib/models/notification.dart
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**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.
2026-01-30 07:40:19 -06:00

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';
}
}
}