feat: 重构 Reminder Notification 系统并更新应用包名

This commit is contained in:
qzl
2026-03-30 18:36:57 +08:00
parent 9fb2a6857b
commit 91bf3c3f96
90 changed files with 5133 additions and 3017 deletions
@@ -1,443 +0,0 @@
import 'package:flutter/material.dart' hide BackButton;
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../../app/di/injection.dart';
import '../../../../features/calendar/data/repositories/calendar_repository.dart';
import '../../../../features/messages/data/models/inbox_message.dart';
import '../../../../features/messages/data/repositories/inbox_repository.dart';
import '../../../../features/contacts/data/repositories/user_repository.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/page_header.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
class MessageInviteDetailScreen extends StatefulWidget {
final String inviteId;
const MessageInviteDetailScreen({super.key, required this.inviteId});
@override
State<MessageInviteDetailScreen> createState() =>
_MessageInviteDetailScreenState();
}
class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
late final InboxRepository _inboxRepository;
late final CalendarRepository _calendarRepository;
late final UserRepository _userRepository;
InboxMessage? _message;
String? _calendarTitle;
String? _senderName;
bool _loading = true;
bool _submitting = false;
String? _error;
ColorScheme get _colorScheme => Theme.of(context).colorScheme;
bool get _isPending => _message?.status == InboxMessageStatus.pending;
@override
void initState() {
super.initState();
_inboxRepository = sl<InboxRepository>();
_calendarRepository = sl<CalendarRepository>();
_userRepository = sl<UserRepository>();
_loadDetail();
}
Future<void> _loadDetail() async {
setState(() {
_loading = true;
_error = null;
});
try {
final results = await Future.wait([
_inboxRepository.getMessages(isRead: false),
_inboxRepository.getMessages(isRead: true),
]);
final messages = [...results[0], ...results[1]];
InboxMessage? message;
for (final item in messages) {
if (item.id == widget.inviteId) {
message = item;
break;
}
}
if (message == null) {
throw StateError(L10n.current.messagesInviteDetailNotFound);
}
String? calendarTitle;
if (message.scheduleItemId != null) {
try {
final event = await _calendarRepository.getById(
message.scheduleItemId!,
);
calendarTitle = event.title;
} catch (_) {
calendarTitle = null;
}
}
String? senderName;
if (message.senderId != null) {
try {
final sender = await _userRepository.getById(message.senderId!);
senderName = sender.username;
} catch (_) {
senderName = null;
}
}
if (!mounted) {
return;
}
setState(() {
_message = message;
_calendarTitle = calendarTitle;
_senderName = senderName;
_loading = false;
});
} catch (e) {
if (!mounted) {
return;
}
setState(() {
_error = e.toString().replaceFirst('Bad state: ', '');
_loading = false;
});
}
}
Future<void> _acceptInvite() async {
final message = _message;
final itemId = message?.scheduleItemId;
if (message == null || itemId == null || _submitting) {
return;
}
setState(() => _submitting = true);
try {
await _calendarRepository.acceptSubscription(itemId);
await _inboxRepository.markAsRead(message.id);
if (!mounted) {
return;
}
Toast.show(
context,
context.l10n.messagesInviteAcceptedToast,
type: ToastType.success,
);
await _loadDetail();
} catch (_) {
if (!mounted) {
return;
}
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
} finally {
if (mounted) {
setState(() => _submitting = false);
}
}
}
Future<void> _rejectInvite() async {
final message = _message;
final itemId = message?.scheduleItemId;
if (message == null || itemId == null || _submitting) {
return;
}
setState(() => _submitting = true);
try {
await _calendarRepository.rejectSubscription(itemId);
await _inboxRepository.markAsRead(message.id);
if (!mounted) {
return;
}
Toast.show(
context,
context.l10n.messagesInviteRejectedToast,
type: ToastType.success,
);
await _loadDetail();
} catch (_) {
if (!mounted) {
return;
}
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
} finally {
if (mounted) {
setState(() => _submitting = false);
}
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))),
);
}
return Scaffold(
backgroundColor: _colorScheme.surface,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageHeader(leading: BackButton(onPressed: () => context.pop())),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummaryCard(),
const SizedBox(height: 14),
_buildCalendarTip(),
const SizedBox(height: 14),
_buildActionRow(),
if (_error != null) ...[
const SizedBox(height: 14),
Text(
_error!,
style: TextStyle(
fontSize: 12,
color: _colorScheme.error,
),
),
],
],
),
),
),
],
),
),
);
}
Widget _buildSummaryCard() {
final message = _message;
final statusText = message == null
? context.l10n.commonUnknown
: switch (message.status) {
InboxMessageStatus.pending => context.l10n.messagesStatusPending,
InboxMessageStatus.accepted =>
context.l10n.messagesInviteStatusAccepted,
InboxMessageStatus.rejected =>
context.l10n.messagesInviteStatusRejected,
InboxMessageStatus.dismissed =>
context.l10n.messagesInviteStatusHandled,
};
final createdAt = message?.createdAt;
final createdAtText = createdAt == null
? context.l10n.commonUnknown
: DateFormat.yMd(context.l10n.localeName).add_Hm().format(createdAt);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: _colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _colorScheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.messagesInviteDetailTitle,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _colorScheme.onSurface,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteEvent(
_calendarTitle ?? context.l10n.messagesInviteUnnamedEvent,
),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _colorScheme.onSurface,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteSender(
_senderName ?? context.l10n.messagesInviteUnknownUser,
),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: _colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteTime(createdAtText),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: _colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteStatus(statusText),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: _colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteId(widget.inviteId),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: _colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildCalendarTip() {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: _colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _colorScheme.outlineVariant),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 14,
color: _colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
context.l10n.messagesInviteTip,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: _colorScheme.onSurfaceVariant,
),
),
),
],
),
);
}
Widget _buildActionRow() {
if (!_isPending) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: _colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _colorScheme.outlineVariant),
),
child: Text(
context.l10n.messagesInviteAlreadyHandled,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
);
}
return SizedBox(
height: 46,
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: _submitting ? null : _rejectInvite,
child: Container(
decoration: BoxDecoration(
color: _colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _colorScheme.error),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.close, size: 15, color: _colorScheme.error),
const SizedBox(width: 6),
Text(
context.l10n.messagesReject,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _colorScheme.error,
),
),
],
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: _submitting ? null : _acceptInvite,
child: Container(
decoration: BoxDecoration(
color: _colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _colorScheme.primary),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check, size: 15, color: _colorScheme.primary),
const SizedBox(width: 6),
Text(
context.l10n.messagesAccept,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _colorScheme.primary,
),
),
],
),
),
),
),
],
),
);
}
}
@@ -1,8 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart' hide BackButton;
import 'package:go_router/go_router.dart';
import '../../../../app/di/injection.dart';
import '../../../../app/router/app_routes.dart';
import '../../../../core/inbox/inbox_sync_store.dart';
import '../../../../features/calendar/data/repositories/calendar_repository.dart';
import '../../../../features/contacts/data/repositories/friend_repository.dart';
import '../../../../features/messages/data/repositories/inbox_repository.dart';
import '../../../../features/contacts/data/models/friend_request.dart';
@@ -33,11 +36,15 @@ class MessageInviteListScreen extends StatefulWidget {
class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
late final InboxRepository _inboxRepository;
late final FriendRepository _friendRepository;
late final CalendarRepository _calendarRepository;
late final InboxSyncStore _inboxSyncStore;
List<MessageWithFriend> _unreadMessages = [];
List<MessageWithFriend> _readMessages = [];
bool _isLoading = false;
bool _isInitialLoading = true;
bool _isPullRefreshing = false;
bool _isHydrating = false;
bool _pendingStoreSync = false;
int _activeTabIndex = 0;
ColorScheme get _colorScheme => Theme.of(context).colorScheme;
@@ -47,26 +54,65 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
super.initState();
_inboxRepository = sl<InboxRepository>();
_friendRepository = sl<FriendRepository>();
_loadMessages();
_calendarRepository = sl<CalendarRepository>();
_inboxSyncStore = sl<InboxSyncStore>();
_inboxSyncStore.addListener(_handleInboxStoreChanged);
unawaited(_bootstrapInbox());
}
Future<void> _loadMessages({bool showPageLoader = true}) async {
if (_isLoading || _isPullRefreshing) {
@override
void dispose() {
_inboxSyncStore.removeListener(_handleInboxStoreChanged);
super.dispose();
}
void _handleInboxStoreChanged() {
if (_isInitialLoading || _isPullRefreshing || _isHydrating) {
_pendingStoreSync = true;
return;
}
if (mounted) {
unawaited(
_syncMessagesFromStore(forceSnapshot: false, fromPullRefresh: false),
);
}
Future<void> _bootstrapInbox() async {
if (!mounted) {
return;
}
setState(() {
_isInitialLoading = true;
});
await _inboxSyncStore.ensureStarted();
await _syncMessagesFromStore(forceSnapshot: false, fromPullRefresh: false);
if (!mounted) {
return;
}
setState(() {
_isInitialLoading = false;
});
}
Future<void> _syncMessagesFromStore({
required bool forceSnapshot,
required bool fromPullRefresh,
}) async {
if (_isHydrating) {
_pendingStoreSync = true;
return;
}
_isHydrating = true;
if (mounted && fromPullRefresh) {
setState(() {
_isLoading = showPageLoader;
_isPullRefreshing = !showPageLoader;
_isPullRefreshing = true;
});
}
try {
final results = await Future.wait([
_inboxRepository.getMessages(isRead: false),
_inboxRepository.getMessages(isRead: true),
]);
final unreadRaw = results[0];
final readRaw = results[1];
if (forceSnapshot) {
await _inboxSyncStore.refreshSnapshot();
}
final unreadRaw = _inboxSyncStore.unreadMessages;
final readRaw = _inboxSyncStore.readMessages;
final allMessages = [...unreadRaw, ...readRaw];
final friendshipIds = allMessages
@@ -90,15 +136,23 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
setState(() {
_unreadMessages = unread;
_readMessages = read;
_isLoading = false;
_isPullRefreshing = false;
});
_isHydrating = false;
if (_pendingStoreSync) {
_pendingStoreSync = false;
await _syncMessagesFromStore(
forceSnapshot: false,
fromPullRefresh: false,
);
}
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_isPullRefreshing = false;
});
_isHydrating = false;
_pendingStoreSync = false;
Toast.show(
context,
context.l10n.messagesLoadFailed,
@@ -108,7 +162,7 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
}
Future<void> _onRefreshMessages() async {
await _loadMessages(showPageLoader: false);
await _syncMessagesFromStore(forceSnapshot: true, fromPullRefresh: true);
}
List<MessageWithFriend> _mapMessagesWithFriend(
@@ -128,17 +182,19 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
switch (message.messageType) {
case InboxMessageType.calendar:
final content = _parseCalendarContent(message.content);
if (content == null) return;
if (content == null) {
_showProtocolErrorToast();
return;
}
final type = content['type'] as String?;
if (type == 'invite') {
context.push(AppRoutes.messageInviteDetail(message.id));
} else if (type == 'update') {
if (message.scheduleItemId != null) {
context.push(
AppRoutes.calendarEventDetail(message.scheduleItemId!),
);
}
final isHandled = message.status != InboxMessageStatus.pending;
_showCalendarInviteSheet(item, isReadOnly: isHandled);
} else if (type == 'updated' || type == 'deleted') {
_showCalendarChangeSheet(item);
} else {
_showProtocolErrorToast();
}
return;
case InboxMessageType.friendRequest:
@@ -159,9 +215,127 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
}
Map<String, dynamic>? _parseCalendarContent(Map<String, dynamic>? content) {
if (content == null) {
return null;
}
final type = content['type'];
final schemaVersion = content['schema_version'];
final item = content['item'];
final actor = content['actor'];
final summary = content['summary'];
if (type is! String || schemaVersion is! int || schemaVersion != 2) {
return null;
}
if (item is! Map<String, dynamic> || actor is! Map<String, dynamic>) {
return null;
}
final itemId = item['id'];
final itemTitle = item['title'];
final actorId = actor['user_id'];
final actorName = actor['username'];
if (itemId is! String || itemTitle is! String || itemTitle.trim().isEmpty) {
return null;
}
if (actorId is! String ||
actorName is! String ||
actorName.trim().isEmpty) {
return null;
}
if (summary is! String || summary.trim().isEmpty) {
return null;
}
if (type == 'updated') {
final changes = content['changes'];
if (changes is! List) {
return null;
}
}
return content;
}
void _showProtocolErrorToast() {
Toast.show(
context,
context.l10n.messagesProtocolInvalid,
type: ToastType.error,
);
}
void _showCalendarChangeSheet(MessageWithFriend item) {
final message = item.message;
final content = _parseCalendarContent(message.content);
if (content == null) {
_showProtocolErrorToast();
return;
}
final actor = content['actor'] as Map<String, dynamic>;
final actorName =
actor['username'] as String? ?? context.l10n.messagesUnknownActor;
final summary = content['summary'] as String;
final type = content['type'] as String;
final changes =
(content['changes'] as List?)
?.whereType<Map<String, dynamic>>()
.toList() ??
const [];
final details = <String>[];
if (changes.isNotEmpty) {
for (final entry in changes) {
final label =
entry['label'] as String? ?? (entry['field'] as String? ?? '-');
final before = entry['display_before'] as String? ?? '-';
final after = entry['display_after'] as String? ?? '-';
details.add('$label: $before -> $after');
}
}
final description = details.isEmpty
? summary
: ([summary, ...details].join('\n'));
showModalBottomSheet<void>(
context: context,
backgroundColor: _colorScheme.surface.withValues(alpha: 0),
isScrollControlled: true,
builder: (ctx) => MessageActionSheet(
title: type == 'deleted'
? context.l10n.messagesCalendarDeletedBy(actorName)
: context.l10n.messagesCalendarUpdatedBy(actorName),
description: description,
isReadOnly: true,
icon: type == 'deleted'
? Icons.delete_outline
: Icons.edit_calendar_outlined,
iconColor: type == 'deleted'
? _colorScheme.error
: _colorScheme.primary,
primaryActionText: context.l10n.messagesAcknowledge,
onPrimaryAction: () async {
await _markMessageAsRead(message);
},
),
);
}
Future<void> _markMessageAsRead(InboxMessage message) async {
if (message.isRead) {
return;
}
try {
await _inboxRepository.markAsRead(message.id);
await _syncMessagesFromStore(forceSnapshot: true, fromPullRefresh: false);
} catch (_) {
if (!mounted) {
return;
}
Toast.show(
context,
context.l10n.messagesActionFailed,
type: ToastType.error,
);
}
}
void _showFriendRequestSheet(
MessageWithFriend item, {
bool isReadOnly = false,
@@ -207,6 +381,98 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
);
}
void _showCalendarInviteSheet(
MessageWithFriend item, {
bool isReadOnly = false,
}) {
final message = item.message;
final parsed = _parseCalendarContent(message.content);
if (parsed == null) {
_showProtocolErrorToast();
return;
}
final itemMap = parsed['item'] as Map<String, dynamic>;
final actorMap = parsed['actor'] as Map<String, dynamic>;
final title = (itemMap['title'] as String).trim();
final resolvedTitle = context.l10n.messagesInviteEvent(title);
final statusText = isReadOnly ? _calendarStatusLabel(message.status) : null;
final actorName =
actorMap['username'] as String? ?? context.l10n.messagesUnknownActor;
final actorPhone = actorMap['phone'] as String?;
final summary = parsed['summary'] as String;
final detailLines = <String>[
summary,
'${context.l10n.messagesCalendarInviteActorLabel}: $actorName${(actorPhone != null && actorPhone.isNotEmpty) ? ' / $actorPhone' : ''}',
];
final startAt = _formatInviteDateTime(itemMap['start_at']);
final endAt = _formatInviteDateTime(itemMap['end_at']);
final timezone = itemMap['timezone'] as String?;
if (startAt != null) {
detailLines.add(
'${context.l10n.messagesCalendarInviteTimeLabel}: ${endAt != null ? '$startAt - $endAt' : startAt}${timezone != null && timezone.isNotEmpty ? ' ($timezone)' : ''}',
);
}
final descriptionText = itemMap['description'] as String?;
if (descriptionText != null && descriptionText.trim().isNotEmpty) {
detailLines.add(
'${context.l10n.messagesCalendarInviteDescriptionLabel}: ${descriptionText.trim()}',
);
}
if (isReadOnly) {
detailLines.add(context.l10n.messagesInviteAlreadyHandled);
}
final description = detailLines.join('\n');
showModalBottomSheet<void>(
context: context,
backgroundColor: _colorScheme.surface.withValues(alpha: 0),
isScrollControlled: true,
builder: (ctx) => MessageActionSheet(
title: resolvedTitle,
description: description,
statusText: statusText,
isReadOnly: isReadOnly,
icon: Icons.event_outlined,
iconColor: _colorScheme.primary,
onAccept: isReadOnly
? null
: () async {
await _processCalendarInvite(item, accept: true);
},
onDecline: isReadOnly
? null
: () async {
await _processCalendarInvite(item, accept: false);
},
),
);
}
String? _formatInviteDateTime(Object? raw) {
if (raw is! String || raw.isEmpty) {
return null;
}
final dt = DateTime.tryParse(raw);
if (dt == null) {
return null;
}
final local = dt.toLocal();
final month = local.month.toString().padLeft(2, '0');
final day = local.day.toString().padLeft(2, '0');
final hour = local.hour.toString().padLeft(2, '0');
final minute = local.minute.toString().padLeft(2, '0');
return '${local.year}-$month-$day $hour:$minute';
}
String _calendarStatusLabel(InboxMessageStatus status) {
return switch (status) {
InboxMessageStatus.pending => context.l10n.messagesStatusPending,
InboxMessageStatus.accepted => context.l10n.messagesInviteStatusAccepted,
InboxMessageStatus.rejected => context.l10n.messagesInviteStatusRejected,
InboxMessageStatus.dismissed => context.l10n.messagesInviteStatusHandled,
};
}
Future<void> _processFriendRequest(
MessageWithFriend item, {
required bool accept,
@@ -243,7 +509,7 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
}
}
await _inboxRepository.markAsRead(message.id);
await _loadMessages();
await _syncMessagesFromStore(forceSnapshot: true, fromPullRefresh: false);
} catch (e) {
if (mounted) {
Toast.show(
@@ -255,6 +521,54 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
}
}
Future<void> _processCalendarInvite(
MessageWithFriend item, {
required bool accept,
}) async {
final message = item.message;
final scheduleItemId = message.scheduleItemId;
if (scheduleItemId == null) {
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
return;
}
try {
if (accept) {
await _calendarRepository.acceptSubscription(scheduleItemId);
if (mounted) {
Toast.show(
context,
context.l10n.messagesInviteAcceptedToast,
type: ToastType.success,
);
}
} else {
await _calendarRepository.rejectSubscription(scheduleItemId);
if (mounted) {
Toast.show(
context,
context.l10n.messagesInviteRejectedToast,
type: ToastType.success,
);
}
}
await _inboxRepository.markAsRead(message.id);
await _syncMessagesFromStore(forceSnapshot: true, fromPullRefresh: false);
} catch (_) {
if (mounted) {
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -264,7 +578,7 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
children: [
_buildHeader(context),
Expanded(
child: _isLoading
child: _isInitialLoading
? const Center(
child: AppLoadingIndicator(
size: 22,
@@ -479,6 +793,42 @@ class _MessageCard extends StatelessWidget {
InboxMessage get message => item.message;
FriendRequest? get friendRequest => item.friendRequest;
Map<String, dynamic>? _parseCalendarContent(Map<String, dynamic>? content) {
if (content == null) {
return null;
}
final type = content['type'];
final schemaVersion = content['schema_version'];
final item = content['item'];
final actor = content['actor'];
final summary = content['summary'];
if (type is! String || schemaVersion is! int || schemaVersion != 2) {
return null;
}
if (item is! Map<String, dynamic> || actor is! Map<String, dynamic>) {
return null;
}
final itemId = item['id'];
final itemTitle = item['title'];
final actorId = actor['user_id'];
final actorName = actor['username'];
if (itemId is! String || itemTitle is! String || itemTitle.trim().isEmpty) {
return null;
}
if (actorId is! String ||
actorName is! String ||
actorName.trim().isEmpty) {
return null;
}
if (summary is! String || summary.trim().isEmpty) {
return null;
}
if (type == 'updated' && content['changes'] is! List) {
return null;
}
return content;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -555,19 +905,39 @@ class _MessageCard extends StatelessWidget {
);
}
if (message.messageType == InboxMessageType.calendar) {
final data = message.content;
return data?['title'] as String? ?? L10n.current.messagesCalendarInvite;
final data = _parseCalendarContent(message.content);
if (data == null) {
return L10n.current.messagesProtocolInvalidCardTitle;
}
final type = data['type'];
final item = data['item'];
final itemTitle = item is Map<String, dynamic>
? item['title'] as String?
: null;
if (type == 'invite' &&
itemTitle != null &&
itemTitle.trim().isNotEmpty) {
return L10n.current.messagesInviteEvent(itemTitle);
}
if (type == 'updated' &&
itemTitle != null &&
itemTitle.trim().isNotEmpty) {
return L10n.current.messagesCalendarCardUpdatedWithTitle(itemTitle);
}
if (type == 'deleted' &&
itemTitle != null &&
itemTitle.trim().isNotEmpty) {
return L10n.current.messagesCalendarCardDeletedWithTitle(itemTitle);
}
return L10n.current.messagesProtocolInvalidCardTitle;
}
return L10n.current.messagesSystemMessage;
}
String _content() {
if (message.messageType == InboxMessageType.calendar) {
Map<String, dynamic>? data;
if (message.content != null) {
data = message.content;
}
if (data == null) return L10n.current.messagesTapToView;
final data = _parseCalendarContent(message.content);
if (data == null) return L10n.current.messagesProtocolInvalidCardDesc;
final type = data['type'] as String?;
if (type == 'invite') {
@@ -579,10 +949,12 @@ class _MessageCard extends StatelessWidget {
} else if (status == InboxMessageStatus.rejected) {
return L10n.current.messagesInviteRejected;
}
} else if (type == 'update') {
} else if (type == 'updated') {
return L10n.current.messagesCalendarUpdated;
} else if (type == 'deleted') {
return L10n.current.messagesCalendarDeleted;
}
return L10n.current.messagesTapToView;
return L10n.current.messagesProtocolInvalidCardDesc;
}
return message.content?['message'] as String? ??
L10n.current.messagesTapToView;
@@ -12,6 +12,8 @@ class MessageActionSheet extends StatelessWidget {
final VoidCallback? onDecline;
final IconData? icon;
final Color? iconColor;
final String? primaryActionText;
final VoidCallback? onPrimaryAction;
const MessageActionSheet({
super.key,
@@ -23,6 +25,8 @@ class MessageActionSheet extends StatelessWidget {
this.onDecline,
this.icon,
this.iconColor,
this.primaryActionText,
this.onPrimaryAction,
});
@override
@@ -124,6 +128,17 @@ class MessageActionSheet extends StatelessWidget {
),
],
),
] else if (primaryActionText != null && onPrimaryAction != null) ...[
SizedBox(
width: double.infinity,
child: AppButton(
text: primaryActionText!,
onPressed: () {
Navigator.pop(context);
onPrimaryAction?.call();
},
),
),
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 12),
],