feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user