feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持

This commit is contained in:
qzl
2026-03-27 14:05:03 +08:00
parent b1f0eb8921
commit c592cc7854
178 changed files with 10748 additions and 5764 deletions
@@ -0,0 +1,449 @@
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 '../../../../core/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.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';
import '../../../calendar/data/calendar_api.dart';
import '../../../contacts/data/users/users_api.dart';
import '../../data/inbox_api.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 InboxApi _inboxApi;
late final CalendarApi _calendarApi;
late final UsersApi _usersApi;
InboxMessageResponse? _message;
String? _calendarTitle;
String? _senderName;
bool _loading = true;
bool _submitting = false;
String? _error;
bool get _isPending => _message?.status == InboxMessageStatus.pending;
@override
void initState() {
super.initState();
_inboxApi = sl<InboxApi>();
_calendarApi = sl<CalendarApi>();
_usersApi = sl<UsersApi>();
_loadDetail();
}
Future<void> _loadDetail() async {
setState(() {
_loading = true;
_error = null;
});
try {
final results = await Future.wait([
_inboxApi.getMessages(isRead: false),
_inboxApi.getMessages(isRead: true),
]);
final messages = [...results[0], ...results[1]];
InboxMessageResponse? 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 _calendarApi.getById(message.scheduleItemId!);
calendarTitle = event.title;
} catch (_) {
calendarTitle = null;
}
}
String? senderName;
if (message.senderId != null) {
try {
final sender = await _usersApi.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 _calendarApi.acceptSubscription(itemId);
await _inboxApi.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 _calendarApi.rejectSubscription(itemId);
await _inboxApi.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: AppColors.messageBg,
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: const TextStyle(
fontSize: 12,
color: AppColors.feedbackErrorText,
),
),
],
],
),
),
),
],
),
),
);
}
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: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.messageCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.messagesInviteDetailTitle,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteEvent(
_calendarTitle ?? context.l10n.messagesInviteUnnamedEvent,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.slate700,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteSender(
_senderName ?? context.l10n.messagesInviteUnknownUser,
),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteTime(createdAtText),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteStatus(statusText),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
const SizedBox(height: 10),
Text(
context.l10n.messagesInviteId(widget.inviteId),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
],
),
);
}
Widget _buildCalendarTip() {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: AppColors.messageTipBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageTipBorder),
),
child: Row(
children: [
const Icon(Icons.info_outline, size: 14, color: AppColors.slate500),
const SizedBox(width: 8),
Expanded(
child: Text(
context.l10n.messagesInviteTip,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
),
],
),
);
}
Widget _buildActionRow() {
if (!_isPending) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageCardBorder),
),
child: Text(
context.l10n.messagesInviteAlreadyHandled,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.slate600,
),
textAlign: TextAlign.center,
),
);
}
return SizedBox(
height: 46,
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: _submitting ? null : _rejectInvite,
child: Container(
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageRejectBorder),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'×',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.red400,
),
),
const SizedBox(width: 6),
Text(
context.l10n.messagesReject,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.red400,
),
),
],
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: _submitting ? null : _acceptInvite,
child: Container(
decoration: BoxDecoration(
color: AppColors.messageTagBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageAcceptBorder),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.blue600,
),
),
const SizedBox(width: 6),
Text(
context.l10n.messagesAccept,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.blue600,
),
),
],
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,591 @@
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/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/app_pull_refresh_feedback.dart';
import '../../../../shared/widgets/page_header.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../contacts/data/friends_api.dart';
import '../../data/inbox_api.dart';
import '../../presentation/widgets/message_action_sheet.dart';
class MessageWithFriend {
final InboxMessageResponse message;
final FriendRequestResponse? friendRequest;
const MessageWithFriend({required this.message, this.friendRequest});
}
class MessageInviteListScreen extends StatefulWidget {
const MessageInviteListScreen({super.key});
@override
State<MessageInviteListScreen> createState() =>
_MessageInviteListScreenState();
}
class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
late final InboxApi _inboxApi;
late final FriendsApi _friendsApi;
List<MessageWithFriend> _unreadMessages = [];
List<MessageWithFriend> _readMessages = [];
bool _isLoading = false;
bool _isPullRefreshing = false;
int _activeTabIndex = 0;
@override
void initState() {
super.initState();
_inboxApi = sl<InboxApi>();
_friendsApi = sl<FriendsApi>();
_loadMessages();
}
Future<void> _loadMessages({bool showPageLoader = true}) async {
if (_isLoading || _isPullRefreshing) {
return;
}
if (mounted) {
setState(() {
_isLoading = showPageLoader;
_isPullRefreshing = !showPageLoader;
});
}
try {
final results = await Future.wait([
_inboxApi.getMessages(isRead: false),
_inboxApi.getMessages(isRead: true),
]);
final unreadRaw = results[0];
final readRaw = results[1];
final allMessages = [...unreadRaw, ...readRaw];
final friendshipIds = allMessages
.where(
(m) =>
m.messageType == InboxMessageType.friendRequest &&
m.friendshipId != null,
)
.map((m) => m.friendshipId!)
.toSet()
.toList();
final requestMap = <String, FriendRequestResponse?>{};
if (friendshipIds.isNotEmpty) {
final fetched = await Future.wait(
friendshipIds.map((id) async {
try {
final req = await _friendsApi.getRequestById(id);
return (id, req as FriendRequestResponse?);
} catch (_) {
return (id, null as FriendRequestResponse?);
}
}),
);
for (final pair in fetched) {
requestMap[pair.$1] = pair.$2;
}
}
final unread = _mapMessagesWithFriend(unreadRaw, requestMap);
final read = _mapMessagesWithFriend(readRaw, requestMap);
if (!mounted) return;
setState(() {
_unreadMessages = unread;
_readMessages = read;
_isLoading = false;
_isPullRefreshing = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_isPullRefreshing = false;
});
Toast.show(
context,
context.l10n.messagesLoadFailed,
type: ToastType.error,
);
}
}
Future<void> _onRefreshMessages() async {
await _loadMessages(showPageLoader: false);
}
List<MessageWithFriend> _mapMessagesWithFriend(
List<InboxMessageResponse> messages,
Map<String, FriendRequestResponse?> requestMap,
) {
return messages.map((message) {
final friendRequest = message.friendshipId == null
? null
: requestMap[message.friendshipId!];
return MessageWithFriend(message: message, friendRequest: friendRequest);
}).toList();
}
Future<void> _handleMessageTap(MessageWithFriend item) async {
final message = item.message;
switch (message.messageType) {
case InboxMessageType.calendar:
final content = _parseCalendarContent(message.content);
if (content == null) 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!),
);
}
}
return;
case InboxMessageType.friendRequest:
if (item.friendRequest == null) {
Toast.show(
context,
context.l10n.messagesSenderLoadFailed,
type: ToastType.error,
);
return;
}
_showFriendRequestSheet(item, isReadOnly: message.isRead);
return;
case InboxMessageType.system:
case InboxMessageType.group:
return;
}
}
Map<String, dynamic>? _parseCalendarContent(Map<String, dynamic>? content) {
return content;
}
void _showFriendRequestSheet(
MessageWithFriend item, {
bool isReadOnly = false,
}) {
final message = item.message;
final friendRequest = item.friendRequest;
if (friendRequest == null) return;
final title = context.l10n.messagesFriendRequestTitle(
friendRequest.sender.username,
);
final description = message.content?['message'] as String?;
final statusText = isReadOnly
? (friendRequest.status == 'accepted'
? context.l10n.messagesInviteStatusAccepted
: friendRequest.status == 'rejected'
? context.l10n.messagesInviteStatusRejected
: context.l10n.messagesInviteStatusHandled)
: null;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
statusText: statusText,
isReadOnly: isReadOnly,
icon: Icons.person_add_outlined,
iconColor: AppColors.emerald500,
onAccept: isReadOnly
? null
: () async {
await _processFriendRequest(item, accept: true);
},
onDecline: isReadOnly
? null
: () async {
await _processFriendRequest(item, accept: false);
},
),
);
}
Future<void> _processFriendRequest(
MessageWithFriend item, {
required bool accept,
}) async {
final message = item.message;
final friendshipId = message.friendshipId;
if (friendshipId == null) {
Toast.show(
context,
context.l10n.messagesFriendRequestMissing,
type: ToastType.error,
);
return;
}
try {
if (accept) {
await _friendsApi.acceptRequest(friendshipId);
if (mounted) {
Toast.show(
context,
context.l10n.messagesAcceptedFriendRequest,
type: ToastType.success,
);
}
} else {
await _friendsApi.declineRequest(friendshipId);
if (mounted) {
Toast.show(
context,
context.l10n.messagesRejectedFriendRequest,
type: ToastType.success,
);
}
}
await _inboxApi.markAsRead(message.id);
await _loadMessages();
} catch (e) {
if (mounted) {
Toast.show(
context,
context.l10n.messagesActionFailed,
type: ToastType.error,
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: SafeArea(
child: Column(
children: [
_buildHeader(context),
Expanded(
child: _isLoading
? const Center(
child: AppLoadingIndicator(
size: 22,
color: AppColors.blue500,
trackColor: AppColors.blue100,
withContainer: false,
),
)
: _activeTabIndex == 0
? _buildMessageList(_unreadMessages, isUnread: true)
: _buildMessageList(_readMessages, isUnread: false),
),
],
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 20, 8),
child: Row(
children: [
BackButton(onPressed: () => context.pop()),
const SizedBox(width: 12),
Expanded(child: _buildTabs()),
const SizedBox(width: 56),
],
),
);
}
Widget _buildTabs() {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Expanded(
child: _buildTab(
0,
context.l10n.messagesTabUnread,
Icons.mark_email_unread_outlined,
),
),
const SizedBox(width: 4),
Expanded(
child: _buildTab(
1,
context.l10n.messagesTabRead,
Icons.mark_email_read_outlined,
),
),
],
),
);
}
Widget _buildTab(int index, String label, IconData icon) {
final isSelected = _activeTabIndex == index;
return GestureDetector(
onTap: () {
if (_activeTabIndex != index) {
setState(() => _activeTabIndex = index);
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColors.white : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 16,
color: isSelected ? AppColors.slate900 : AppColors.slate500,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? AppColors.slate900 : AppColors.slate500,
),
),
if (index == 0 && _unreadMessages.isNotEmpty) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: AppColors.red500,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_unreadMessages.length > 99
? '99+'
: _unreadMessages.length.toString(),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
),
),
],
],
),
),
);
}
Widget _buildMessageList(
List<MessageWithFriend> messages, {
required bool isUnread,
}) {
return Stack(
children: [
RefreshIndicator.noSpinner(
onRefresh: _onRefreshMessages,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: messages.isEmpty ? 1 : messages.length,
itemBuilder: (context, index) {
if (messages.isEmpty) {
return SizedBox(
height: MediaQuery.sizeOf(context).height * 0.6,
child: _buildEmptyState(isUnread: isUnread),
);
}
final item = messages[index];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _MessageCard(
item: item,
onTap: () => _handleMessageTap(item),
),
);
},
),
),
Align(
alignment: Alignment.topCenter,
child: AppPullRefreshFeedback(visible: _isPullRefreshing),
),
],
);
}
Widget _buildEmptyState({required bool isUnread}) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.slate100,
shape: BoxShape.circle,
),
child: Icon(
isUnread ? Icons.notifications_none : Icons.inbox_outlined,
size: 36,
color: AppColors.slate400,
),
),
const SizedBox(height: 16),
Text(
isUnread
? context.l10n.messagesEmptyUnreadTitle
: context.l10n.messagesEmptyReadTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.slate500,
),
),
const SizedBox(height: 8),
Text(
isUnread
? context.l10n.messagesEmptyUnreadDesc
: context.l10n.messagesEmptyReadDesc,
style: const TextStyle(fontSize: 13, color: AppColors.slate400),
),
],
),
);
}
}
class _MessageCard extends StatelessWidget {
final MessageWithFriend item;
final VoidCallback onTap;
const _MessageCard({required this.item, required this.onTap});
InboxMessageResponse get message => item.message;
FriendRequestResponse? get friendRequest => item.friendRequest;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: message.isRead
? AppColors.borderSecondary
: AppColors.blue100,
width: message.isRead ? 1 : 1.5,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.emerald500.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.person_add_outlined,
size: 22,
color: AppColors.emerald500,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_title(),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
const SizedBox(height: 6),
Text(
_content(),
style: const TextStyle(
fontSize: 13,
color: AppColors.slate500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
String _title() {
if (message.messageType == InboxMessageType.friendRequest) {
if (friendRequest == null) {
return L10n.current.messagesFriendRequestLoadFailed;
}
return L10n.current.messagesFriendRequestTitle(
friendRequest!.sender.username,
);
}
if (message.messageType == InboxMessageType.calendar) {
final data = message.content;
return data?['title'] as String? ?? L10n.current.messagesCalendarInvite;
}
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 type = data['type'] as String?;
if (type == 'invite') {
final status = message.status.value;
if (status == 'pending') {
return L10n.current.messagesInviteJoinCalendar;
} else if (status == 'accepted') {
return L10n.current.messagesInviteAccepted;
} else if (status == 'rejected') {
return L10n.current.messagesInviteRejected;
}
} else if (type == 'update') {
return L10n.current.messagesCalendarUpdated;
}
return L10n.current.messagesTapToView;
}
return message.content?['message'] as String? ??
L10n.current.messagesTapToView;
}
}
@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:social_app/core/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../data/inbox_api.dart';
class CalendarInviteCard extends StatelessWidget {
final InboxMessageResponse message;
final VoidCallback onAccept;
final VoidCallback onReject;
const CalendarInviteCard({
super.key,
required this.message,
required this.onAccept,
required this.onReject,
});
String? get eventTitle {
final data = message.content;
return data?['title'] as String?;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.blue100,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: const Icon(
Icons.calendar_today,
color: AppColors.blue600,
size: 20,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
l10n.messagesCalendarCardInviteTitle,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
eventTitle != null
? l10n.messagesCalendarCardInviteWithTitle(eventTitle!)
: l10n.messagesCalendarCardInviteWithoutTitle,
style: const TextStyle(fontSize: 14, color: AppColors.slate700),
),
const SizedBox(height: AppSpacing.md),
Row(
children: [
Expanded(
child: AppButton(
text: l10n.messagesReject,
isOutlined: true,
onPressed: onReject,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: AppButton(
text: l10n.messagesAccept,
onPressed: onAccept,
),
),
],
),
],
),
);
}
}
class CalendarUpdateCard extends StatelessWidget {
final InboxMessageResponse message;
final VoidCallback? onTap;
const CalendarUpdateCard({super.key, required this.message, this.onTap});
String? get eventTitle {
final data = message.content;
return data?['title'] as String?;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: AppColors.border),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.blue100,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: const Icon(
Icons.calendar_today,
color: AppColors.blue600,
size: 20,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
eventTitle != null
? l10n.messagesCalendarCardUpdatedWithTitle(eventTitle!)
: l10n.messagesCalendarCardUpdatedWithoutTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
_formatTime(context, message.createdAt),
style: const TextStyle(
fontSize: 12,
color: AppColors.slate500,
),
),
],
),
),
const Icon(Icons.chevron_right, color: AppColors.slate400),
],
),
),
);
}
String _formatTime(BuildContext context, DateTime time) {
final l10n = context.l10n;
final now = DateTime.now();
final diff = now.difference(time);
if (diff.inMinutes < 60) {
return l10n.messagesCalendarCardTimeMinutesAgo(diff.inMinutes);
} else if (diff.inHours < 24) {
return l10n.messagesCalendarCardTimeHoursAgo(diff.inHours);
} else if (diff.inDays < 7) {
return l10n.messagesCalendarCardTimeDaysAgo(diff.inDays);
} else {
return l10n.messagesCalendarCardTimeDate(time.month, time.day);
}
}
}
class CalendarDeleteCard extends StatelessWidget {
final InboxMessageResponse message;
const CalendarDeleteCard({super.key, required this.message});
String? get eventTitle {
final data = message.content;
return data?['title'] as String?;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: AppColors.slate50,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: AppColors.slate200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.slate200,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: const Icon(
Icons.calendar_today,
color: AppColors.slate500,
size: 20,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
eventTitle != null
? l10n.messagesCalendarCardDeletedWithTitle(eventTitle!)
: l10n.messagesCalendarCardDeletedWithoutTitle,
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
),
),
],
),
);
}
}
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../shared/widgets/app_button.dart';
class MessageActionSheet extends StatelessWidget {
final String title;
final String? description;
final String? statusText;
final bool isReadOnly;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final IconData? icon;
final Color? iconColor;
const MessageActionSheet({
super.key,
required this.title,
this.description,
this.statusText,
this.isReadOnly = false,
this.onAccept,
this.onDecline,
this.icon,
this.iconColor,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
decoration: const BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.slate300,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
if (icon != null) ...[
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: (iconColor ?? AppColors.blue500).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 32,
color: iconColor ?? AppColors.blue500,
),
),
const SizedBox(height: 16),
],
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
textAlign: TextAlign.center,
),
if (description != null && description!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
description!,
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
textAlign: TextAlign.center,
),
],
if (statusText != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(16),
),
child: Text(
statusText!,
style: const TextStyle(fontSize: 14, color: AppColors.slate600),
),
),
],
const SizedBox(height: 24),
if (!isReadOnly) ...[
Row(
children: [
Expanded(
child: AppButton(
text: context.l10n.messagesReject,
isOutlined: true,
onPressed: () {
Navigator.pop(context);
onDecline?.call();
},
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: AppButton(
text: context.l10n.messagesAccept,
onPressed: () {
Navigator.pop(context);
onAccept?.call();
},
),
),
],
),
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 12),
],
),
);
}
}