From 044fb983d6e21e4fe52d0f192a41d2fa36f2b237 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 10 Jun 2025 01:09:28 +0800 Subject: [PATCH] :sparkles: Chat break and notify level --- assets/i18n/en-US.json | 18 ++- assets/i18n/zh-CN.json | 18 ++- assets/i18n/zh-TW.json | 20 ++- lib/models/chat.dart | 3 + lib/models/chat.freezed.dart | 32 ++-- lib/models/chat.g.dart | 10 ++ lib/screens/chat/room_detail.dart | 259 ++++++++++++++++++++++++++++-- lib/screens/explore.g.dart | 2 +- lib/widgets/content/sheet.dart | 4 +- 9 files changed, 336 insertions(+), 30 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index b64161f..2bee161 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -282,6 +282,7 @@ "one": "{} unread message", "other": "{} unread messages" }, + "chatBreakNone": "None", "settingsRealmCompactView": "Compact Realm View", "settingsMixedFeed": "Mixed Feed", "settingsAutoTranslate": "Auto Translate", @@ -396,5 +397,20 @@ "contactMethodPrimary": "Primary", "contactMethodSetPrimary": "Set as Primary", "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", - "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone." + "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.", + "chatNotifyLevel": "Notify Level", + "chatNotifyLevelDescription": "Decide how many notifications you will receive.", + "chatNotifyLevelAll": "All", + "chatNotifyLevelMention": "Mentions", + "chatNotifyLevelNone": "None", + "chatNotifyLevelUpdated": "The notify level has been updated to {}.", + "chatBreak": "Take a Break", + "chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.", + "chatBreakClear": "Clear the break time", + "chatBreakHour": "{} break", + "chatBreakDay": "{} day break", + "chatBreakSet": "Break set for {}", + "chatBreakCleared": "Chat break has been cleared.", + "chatBreakCustom": "Custom duration", + "chatBreakEnterMinutes": "Enter minutes" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index d1cd541..47b8401 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -291,5 +291,21 @@ "postVisibilityPublic": "公开", "postVisibilityFriends": "仅好友可见", "postVisibilityUnlisted": "不公开", - "postVisibilityPrivate": "私密" + "postVisibilityPrivate": "私密", + "chatNotifyLevel": "通知级别", + "chatNotifyLevelDescription": "决定您将收到多少通知。", + "chatNotifyLevelAll": "全部", + "chatNotifyLevelMention": "提及", + "chatNotifyLevelNone": "无", + "chatNotifyLevelUpdated": "通知级别已更新为 {}。", + "chatBreak": "暂停聊天", + "chatBreakDescription": "设置一个时间,在该时间之前,您的通知级别将仅为提及,以暂时休息当前讨论的话题。", + "chatBreakClear": "清除暂停时间", + "chatBreakHour": "暂停 {} 分钟", + "chatBreakDay": "暂停 {} 天", + "chatBreakSet": "已设置暂停 {}", + "chatBreakCleared": "聊天暂停已清除。", + "chatBreakCustom": "自定义时长", + "chatBreakEnterMinutes": "输入分钟数", + "chatBreakNone": "无" } \ No newline at end of file diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 9c59d57..2f8e144 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -68,7 +68,7 @@ "createRealmHint": "結識志同道合的朋友、建立社群等等。", "editRealm": "編輯領域", "deleteRealm": "刪除領域", - "deleteRealmHint": "確定要刪除此領域嗎?這也將刪除此領域下的所有頻道、發佈者和貼文。", + "deleteRealmHint": "確定要刪除此領域嗎?這也將刪除該領域下的所有頻道、發佈者和貼文。", "explore": "探索", "account": "帳號", "name": "名稱", @@ -291,5 +291,21 @@ "postVisibilityPublic": "公開", "postVisibilityFriends": "僅好友可見", "postVisibilityUnlisted": "不公開", - "postVisibilityPrivate": "私密" + "postVisibilityPrivate": "私密", + "chatNotifyLevel": "通知等級", + "chatNotifyLevelDescription": "決定您將收到多少通知。", + "chatNotifyLevelAll": "全部", + "chatNotifyLevelMention": "提及", + "chatNotifyLevelNone": "無", + "chatNotifyLevelUpdated": "通知等級已更新為 {}。", + "chatBreak": "暫停聊天", + "chatBreakDescription": "設定一個時間,在該時間之前,您的通知等級將僅為提及,以暫時休息當前討論的話題。", + "chatBreakClear": "清除暫停時間", + "chatBreakHour": "暫停 {} 分鐘", + "chatBreakDay": "暫停 {} 天", + "chatBreakSet": "已設定暫停 {}", + "chatBreakCleared": "聊天暫停已清除。", + "chatBreakCustom": "自訂時長", + "chatBreakEnterMinutes": "輸入分鐘數", + "chatBreakNone": "無" } \ No newline at end of file diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 09e7fa3..febc06e 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -90,7 +90,10 @@ sealed class SnChatMember with _$SnChatMember { required int role, required int notify, required DateTime? joinedAt, + required DateTime? breakUntil, + required DateTime? timeoutUntil, required bool isBot, + // Frontend data DateTime? lastTyped, }) = _SnChatMember; diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index d58798a..86597ae 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -663,7 +663,8 @@ $SnChatMemberCopyWith<$Res> get sender { /// @nodoc mixin _$SnChatMember { - DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; DateTime? get lastTyped; + DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; DateTime? get breakUntil; DateTime? get timeoutUntil; bool get isBot;// Frontend data + DateTime? get lastTyped; /// Create a copy of SnChatMember /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -676,16 +677,16 @@ $SnChatMemberCopyWith get copyWith => _$SnChatMemberCopyWithImpl Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped); +int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,breakUntil,timeoutUntil,isBot,lastTyped); @override String toString() { - return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)'; + return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, breakUntil: $breakUntil, timeoutUntil: $timeoutUntil, isBot: $isBot, lastTyped: $lastTyped)'; } @@ -696,7 +697,7 @@ abstract mixin class $SnChatMemberCopyWith<$Res> { factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl; @useResult $Res call({ - DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped + DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, DateTime? breakUntil, DateTime? timeoutUntil, bool isBot, DateTime? lastTyped }); @@ -713,7 +714,7 @@ class _$SnChatMemberCopyWithImpl<$Res> /// Create a copy of SnChatMember /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? breakUntil = freezed,Object? timeoutUntil = freezed,Object? isBot = null,Object? lastTyped = freezed,}) { return _then(_self.copyWith( createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable @@ -727,6 +728,8 @@ as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,breakUntil: freezed == breakUntil ? _self.breakUntil : breakUntil // ignore: cast_nullable_to_non_nullable +as DateTime?,timeoutUntil: freezed == timeoutUntil ? _self.timeoutUntil : timeoutUntil // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable as DateTime?, @@ -761,7 +764,7 @@ $SnAccountCopyWith<$Res> get account { @JsonSerializable() class _SnChatMember implements SnChatMember { - const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot, this.lastTyped}); + const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.breakUntil, required this.timeoutUntil, required this.isBot, this.lastTyped}); factory _SnChatMember.fromJson(Map json) => _$SnChatMemberFromJson(json); @override final DateTime createdAt; @@ -776,7 +779,10 @@ class _SnChatMember implements SnChatMember { @override final int role; @override final int notify; @override final DateTime? joinedAt; +@override final DateTime? breakUntil; +@override final DateTime? timeoutUntil; @override final bool isBot; +// Frontend data @override final DateTime? lastTyped; /// Create a copy of SnChatMember @@ -792,16 +798,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.breakUntil, breakUntil) || other.breakUntil == breakUntil)&&(identical(other.timeoutUntil, timeoutUntil) || other.timeoutUntil == timeoutUntil)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped); +int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,breakUntil,timeoutUntil,isBot,lastTyped); @override String toString() { - return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)'; + return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, breakUntil: $breakUntil, timeoutUntil: $timeoutUntil, isBot: $isBot, lastTyped: $lastTyped)'; } @@ -812,7 +818,7 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl; @override @useResult $Res call({ - DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped + DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, DateTime? breakUntil, DateTime? timeoutUntil, bool isBot, DateTime? lastTyped }); @@ -829,7 +835,7 @@ class __$SnChatMemberCopyWithImpl<$Res> /// Create a copy of SnChatMember /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? breakUntil = freezed,Object? timeoutUntil = freezed,Object? isBot = null,Object? lastTyped = freezed,}) { return _then(_SnChatMember( createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable @@ -843,6 +849,8 @@ as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,breakUntil: freezed == breakUntil ? _self.breakUntil : breakUntil // ignore: cast_nullable_to_non_nullable +as DateTime?,timeoutUntil: freezed == timeoutUntil ? _self.timeoutUntil : timeoutUntil // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable as DateTime?, diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 91bafea..b55befd 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -166,6 +166,14 @@ _SnChatMember _$SnChatMemberFromJson(Map json) => json['joined_at'] == null ? null : DateTime.parse(json['joined_at'] as String), + breakUntil: + json['break_until'] == null + ? null + : DateTime.parse(json['break_until'] as String), + timeoutUntil: + json['timeout_until'] == null + ? null + : DateTime.parse(json['timeout_until'] as String), isBot: json['is_bot'] as bool, lastTyped: json['last_typed'] == null @@ -187,6 +195,8 @@ Map _$SnChatMemberToJson(_SnChatMember instance) => 'role': instance.role, 'notify': instance.notify, 'joined_at': instance.joinedAt?.toIso8601String(), + 'break_until': instance.breakUntil?.toIso8601String(), + 'timeout_until': instance.timeoutUntil?.toIso8601String(), 'is_bot': instance.isBot, 'last_typed': instance.lastTyped?.toIso8601String(), }; diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 0e9220a..e595d73 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:island/models/chat.dart'; import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; @@ -14,7 +15,7 @@ import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; -import 'package:island/widgets/content/paging_helper_ext.dart'; +import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -31,6 +32,206 @@ class ChatDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final roomState = ref.watch(chatroomProvider(id)); + final roomIdentity = ref.watch(chatroomIdentityProvider(id)); + + const kNotifyLevelText = [ + 'chatNotifyLevelAll', + 'chatNotifyLevelMention', + 'chatNotifyLevelNone', + ]; + + void setNotifyLevel(int level) async { + try { + final client = ref.watch(apiClientProvider); + await client.patch( + '/chat/$id/members/me/notify', + data: {'notify_level': level}, + ); + ref.invalidate(chatroomIdentityProvider(id)); + if (context.mounted) { + showSnackBar( + context, + 'chatNotifyLevelUpdated'.tr(args: [kNotifyLevelText[level].tr()]), + ); + } + } catch (err) { + showErrorAlert(err); + } + } + + void setChatBreak(DateTime until) async { + try { + final client = ref.watch(apiClientProvider); + await client.patch( + '/chat/$id/members/me/notify', + data: {'break_until': until.toUtc().toIso8601String()}, + ); + ref.invalidate(chatroomProvider(id)); + } catch (err) { + showErrorAlert(err); + } + } + + void showNotifyLevelBottomSheet(SnChatMember identity) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: + (context) => SheetScaffold( + height: 320, + titleText: 'chatNotifyLevel'.tr(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('chatNotifyLevelAll').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.notifications_active), + selected: identity.notify == 0, + onTap: () { + setNotifyLevel(0); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('chatNotifyLevelMention').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.alternate_email), + selected: identity.notify == 1, + onTap: () { + setNotifyLevel(1); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('chatNotifyLevelNone').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.notifications_off), + selected: identity.notify == 2, + onTap: () { + setNotifyLevel(2); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + } + + void showChatBreakDialog() { + final now = DateTime.now(); + final durationController = TextEditingController(); + + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('chatBreak').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('chatBreakDescription').tr(), + const Gap(16), + ListTile( + title: const Text('Clear').tr(), + subtitle: const Text('chatBreakClear').tr(), + leading: const Icon(Icons.notifications_active), + onTap: () { + setChatBreak(now); + Navigator.pop(context); + if (context.mounted) { + showSnackBar(context, 'chatBreakCleared'.tr()); + } + }, + ), + ListTile( + title: const Text('5m'), + subtitle: const Text('chatBreakHour').tr(args: ['5m']), + leading: const Icon(Symbols.circle), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 5))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar(context, 'chatBreakSet'.tr(args: ['5m'])); + } + }, + ), + ListTile( + title: const Text('10m'), + subtitle: const Text('chatBreakHour').tr(args: ['10m']), + leading: const Icon(Symbols.circle), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 10))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar(context, 'chatBreakSet'.tr(args: ['10m'])); + } + }, + ), + ListTile( + title: const Text('15m'), + subtitle: const Text('chatBreakHour').tr(args: ['15m']), + leading: const Icon(Symbols.timer_3), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 15))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar(context, 'chatBreakSet'.tr(args: ['15m'])); + } + }, + ), + ListTile( + title: const Text('30m'), + subtitle: const Text('chatBreakHour').tr(args: ['30m']), + leading: const Icon(Symbols.timer), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 30))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar(context, 'chatBreakSet'.tr(args: ['30m'])); + } + }, + ), + const Gap(8), + TextField( + controller: durationController, + decoration: InputDecoration( + labelText: 'Custom (minutes)'.tr(), + hintText: 'Enter minutes'.tr(), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.check), + onPressed: () { + final minutes = int.tryParse(durationController.text); + if (minutes != null && minutes > 0) { + setChatBreak(now.add(Duration(minutes: minutes))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar( + context, + 'chatBreakSet'.tr(args: ['${minutes}m']), + ); + } + } + }, + ), + ), + keyboardType: TextInputType.number, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('cancel').tr(), + ), + ], + ), + ); + } const iconShadow = Shadow( color: Colors.black54, @@ -114,17 +315,51 @@ class ChatDetailScreen extends HookConsumerWidget { ], ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - currentRoom.description ?? 'descriptionNone'.tr(), - style: const TextStyle(fontSize: 16), - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentRoom.description ?? 'descriptionNone'.tr(), + style: const TextStyle(fontSize: 16), + ).padding(all: 24), + const Divider(height: 1), + roomIdentity.when( + data: + (identity) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Symbols.notifications), + trailing: const Icon(Symbols.chevron_right), + title: const Text('chatNotifyLevel').tr(), + subtitle: Text( + kNotifyLevelText[identity!.notify].tr(), + ), + onTap: + () => + showNotifyLevelBottomSheet(identity), + ), + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Icons.timer), + trailing: const Icon(Symbols.chevron_right), + title: const Text('chatBreak').tr(), + subtitle: identity!.breakUntil != null && identity.breakUntil!.isAfter(DateTime.now()) + ? Text(DateFormat('yyyy-MM-dd HH:mm').format(identity.breakUntil!)) + : const Text('chatBreakNone').tr(), + onTap: () => showChatBreakDialog(), + ), + ], + ), + error: (_, _) => const SizedBox.shrink(), + loading: () => const SizedBox.shrink(), + ), + ], ), ), ], diff --git a/lib/screens/explore.g.dart b/lib/screens/explore.g.dart index 11b31ec..1020078 100644 --- a/lib/screens/explore.g.dart +++ b/lib/screens/explore.g.dart @@ -7,7 +7,7 @@ part of 'explore.dart'; // ************************************************************************** String _$activityListNotifierHash() => - r'1baf0bb961bc02bfc8a5b5f515981072c6ce1750'; + r'2ca8fe14686d7f4fb09ab26f2978eb2de7184565'; /// See also [ActivityListNotifier]. @ProviderFor(ActivityListNotifier) diff --git a/lib/widgets/content/sheet.dart b/lib/widgets/content/sheet.dart index 2902239..13fa725 100644 --- a/lib/widgets/content/sheet.dart +++ b/lib/widgets/content/sheet.dart @@ -7,6 +7,7 @@ class SheetScaffold extends StatelessWidget { final List actions; final Widget child; final double heightFactor; + final double? height; const SheetScaffold({ super.key, this.title, @@ -14,6 +15,7 @@ class SheetScaffold extends StatelessWidget { required this.child, this.actions = const [], this.heightFactor = 0.8, + this.height, }); @override @@ -32,7 +34,7 @@ class SheetScaffold extends StatelessWidget { return Container( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * heightFactor, + maxHeight: height ?? MediaQuery.of(context).size.height * heightFactor, ), child: Column( children: [