diff --git a/assets/audio/sfx/launch-done.mp3 b/assets/audio/sfx/launch-done.mp3 new file mode 100644 index 0000000..1f456f7 Binary files /dev/null and b/assets/audio/sfx/launch-done.mp3 differ diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 271a347..bb83128 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -939,5 +939,7 @@ "settingsSoundEffects": "Sound Effects", "settingsSoundEffectsDescription": "Enable the sound effects around the app.", "settingsResetMemorizedWindowSize": "Reset Window Size", - "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size." + "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.", + "chatDirect": "Direct Messages", + "back": "返回" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index c33c6a2..c0fc6fc 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -936,5 +936,7 @@ "settingsSoundEffects": "声音效果", "settingsSoundEffectsDescription": "在一些场合下启用声音特效。", "settingsResetMemorizedWindowSize": "重置窗口大小", - "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。" + "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。", + "chatDirect": "私信", + "back": "返回" } diff --git a/lib/main.dart b/lib/main.dart index cde9f03..6c08bd8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -396,8 +396,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { final cfg = context.read(); if (!cfg.soundEffects) return; - final player = AudioPlayer(playerId: 'launch-intro-player'); - await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5); + final player = AudioPlayer(playerId: 'launch-done-player'); + await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8); player.onPlayerComplete.listen((_) { player.dispose(); }); diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index eba39b9..faacbec 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:surface/types/realm.dart'; class AppNavListItem { final String title; @@ -135,11 +134,4 @@ class NavigationProvider extends ChangeNotifier { _currentIndex = idx; notifyListeners(); } - - SnRealm? focusedRealm; - - void setFocusedRealm(SnRealm? realm) { - focusedRealm = realm; - notifyListeners(); - } } diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index ed9fafe..70b668a 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -1,3 +1,5 @@ +import 'package:animations/animations.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; @@ -6,11 +8,14 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/channel.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/chat.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/app_bar_leading.dart'; @@ -18,6 +23,7 @@ import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/unauthorized_hint.dart'; +import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; class ChatScreen extends StatefulWidget { @@ -35,6 +41,7 @@ class _ChatScreenState extends State { List? _channels; Map? _lastMessages; Map? _unreadCounts; + Map? _unreadCountsGrouped; Future _fetchWhatsNew() async { final sn = context.read(); @@ -42,19 +49,48 @@ class _ChatScreenState extends State { if (resp.data == null) return; final List out = resp.data; setState(() { - _unreadCounts = {for (var v in out) v['channel_id']: v['count']}; + _unreadCounts ??= {}; + _unreadCountsGrouped ??= {}; + for (var v in out) { + _unreadCounts![v['channel_id']] = v['count']; + final channel = + _channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']); + if (channel != null) { + if (channel.realmId != null) { + _unreadCountsGrouped![channel.realmId!] ??= 0; + _unreadCountsGrouped![channel.realmId!] = + (_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt(); + } + if (channel.type == 1) { + _unreadCountsGrouped![0] ??= 0; + _unreadCountsGrouped![0] = + (_unreadCountsGrouped![0]! + v['count']).toInt(); + } + } + } }); } - void _refreshChannels({bool noRemote = false}) { + void _refreshChannels({bool withBoost = false, bool noRemote = false}) { + final ct = context.read(); final ua = context.read(); if (!ua.isAuthorized) { setState(() => _isBusy = false); return; } + if (!withBoost) { + if (!noRemote) { + ct.refreshAvailableChannels(); + } + } else { + setState(() { + _channels = ct.availableChannels; + }); + } + final chan = context.read(); - chan.fetchChannels(noRemote: noRemote).listen((channels) async { + chan.fetchChannels(noRemote: true).listen((channels) async { final lastMessages = await chan.getLastMessages(channels); _lastMessages = {for (final val in lastMessages) val.channelId: val}; channels.sort((a, b) { @@ -130,29 +166,49 @@ class _ChatScreenState extends State { @override void initState() { super.initState(); - _refreshChannels(); + _refreshChannels(withBoost: true); _fetchWhatsNew(); } void _onTapChannel(SnChannel channel) { setState(() => _unreadCounts?[channel.id] = 0); - GoRouter.of(context).pushReplacementNamed( - 'chatRoom', - pathParameters: { - 'scope': channel.realm?.alias ?? 'global', - 'alias': channel.alias, - }, - ).then((value) { - if (mounted) { - setState(() => _unreadCounts?[channel.id] = 0); - _refreshChannels(noRemote: true); - } - }); + if (ResponsiveScaffold.getIsExpand(context)) { + GoRouter.of(context).pushReplacementNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + ).then((value) { + if (mounted) { + setState(() => _unreadCounts?[channel.id] = 0); + _refreshChannels(noRemote: true); + } + }); + } else { + GoRouter.of(context).pushNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + ).then((value) { + if (mounted) { + setState(() => _unreadCounts?[channel.id] = 0); + _refreshChannels(noRemote: true); + } + }); + } } + SnRealm? _focusedRealm; + bool _isDirect = false; + @override Widget build(BuildContext context) { final ua = context.read(); + final sn = context.read(); + final rel = context.read(); if (!ua.isAuthorized) { return AppScaffold( @@ -235,34 +291,178 @@ class _ChatScreenState extends State { body: Column( children: [ LoadingIndicator(isActive: _isBusy), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeTop: true, + if (_channels != null) + Expanded( child: RefreshIndicator( onRefresh: () => Future.wait([ Future.sync(() => _refreshChannels()), _fetchWhatsNew(), ]), - child: ListView.builder( - itemCount: _channels?.length ?? 0, - itemBuilder: (context, idx) { - final channel = _channels![idx]; - final lastMessage = _lastMessages?[channel.id]; + child: Builder(builder: (context) { + final scopeList = ListView( + key: const Key('realm-list-view'), + padding: EdgeInsets.zero, + children: [ + ListTile( + minTileHeight: 48, + leading: + const Icon(Symbols.inbox_text).padding(right: 4), + contentPadding: EdgeInsets.only(left: 24, right: 24), + title: Text('chatDirect').tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_unreadCountsGrouped?[0] != null && + (_unreadCountsGrouped?[0] ?? 0) > 0) + Badge( + label: Text( + _unreadCountsGrouped![0].toString(), + ), + ), + ], + ), + onTap: () { + setState(() => _isDirect = true); + }, + ), + ...rel.availableRealms.map((ele) { + return ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.only(left: 20, right: 24), + leading: AccountImage( + content: ele.avatar, + radius: 16, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_unreadCountsGrouped?[ele.id] != null && + (_unreadCountsGrouped?[ele.id] ?? 0) > 0) + Badge( + label: Text( + _unreadCountsGrouped![ele.id].toString(), + ), + ), + ], + ), + title: Text(ele.name), + onTap: () { + setState(() => _focusedRealm = ele); + }, + ); + }), + ], + ); - return _ChatChannelEntry( - channel: channel, - lastMessage: lastMessage, - unreadCount: _unreadCounts?[channel.id], - onTap: () { - _onTapChannel(channel); - }, - ); - }, - ), + final directChatList = ListView( + key: Key('direct-chat-list-view'), + padding: EdgeInsets.zero, + children: [ + ListTile( + minTileHeight: 48, + leading: const Icon(Symbols.arrow_left_alt), + contentPadding: EdgeInsets.only(left: 24), + title: Text('back').tr(), + onTap: () { + setState(() => _isDirect = false); + }, + ), + const Divider(height: 1), + ..._channels!.where((ele) => ele.type == 1).map( + (ele) { + return _ChatChannelEntry( + channel: ele, + unreadCount: _unreadCounts?[ele.id], + lastMessage: _lastMessages?[ele.id], + isCompact: true, + onTap: () => _onTapChannel(ele), + ); + }, + ) + ], + ); + + final realmScopedChatList = _focusedRealm == null + ? const SizedBox.shrink() + : ListView( + key: ValueKey(_focusedRealm), + padding: EdgeInsets.zero, + children: [ + if (_focusedRealm!.banner != null) + AspectRatio( + aspectRatio: 16 / 9, + child: AutoResizeUniversalImage( + sn.getAttachmentUrl( + _focusedRealm!.banner!, + ), + fit: BoxFit.cover, + ), + ), + ListTile( + minTileHeight: 48, + tileColor: Theme.of(context) + .colorScheme + .surfaceContainer, + leading: AccountImage( + content: _focusedRealm!.avatar, + radius: 16, + ), + contentPadding: EdgeInsets.only( + left: 20, + right: 16, + ), + trailing: IconButton( + icon: const Icon(Symbols.close), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + onPressed: () { + setState(() => _focusedRealm = null); + }, + ), + title: Text(_focusedRealm!.name), + ), + ...(_channels! + .where( + (ele) => ele.realmId == _focusedRealm?.id) + .map( + (ele) { + return _ChatChannelEntry( + channel: ele, + unreadCount: _unreadCounts?[ele.id], + lastMessage: _lastMessages?[ele.id], + onTap: () => _onTapChannel(ele), + isCompact: true, + ); + }, + )) + ], + ); + + return PageTransitionSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, + Animation primaryAnimation, + Animation secondaryAnimation) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: (_focusedRealm == null && !_isDirect) + ? scopeList + : _isDirect + ? directChatList + : realmScopedChatList, + ); + }), ), ), - ), ], ), ); @@ -274,11 +474,13 @@ class _ChatChannelEntry extends StatelessWidget { final int? unreadCount; final SnChatMessage? lastMessage; final Function? onTap; + final bool isCompact; const _ChatChannelEntry({ required this.channel, this.unreadCount, this.lastMessage, this.onTap, + this.isCompact = false, }); @override @@ -297,6 +499,34 @@ class _ChatChannelEntry extends StatelessWidget { ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name : channel.name; + if (isCompact) { + return ListTile( + minTileHeight: 48, + contentPadding: + EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24), + leading: otherMember != null + ? AccountImage( + content: ud.getFromCache(otherMember.accountId)?.avatar, + radius: 16, + ) + : const Icon(Symbols.tag), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (unreadCount != null && (unreadCount ?? 0) > 0) + Badge( + label: Text(unreadCount.toString()), + ), + ], + ), + title: Text(title), + onTap: () { + onTap?.call(); + }, + ); + } + return ListTile( title: Row( children: [ @@ -359,7 +589,7 @@ class _ChatChannelEntry extends StatelessWidget { content: otherMember != null ? ud.getFromCache(otherMember.accountId)?.avatar : channel.realm?.avatar, - fallbackWidget: const Icon(Symbols.chat, size: 20), + fallbackWidget: const Icon(Symbols.tag, size: 20), ), onTap: () => onTap?.call(), ); diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index ab03f07..679ca37 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:animations/animations.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -11,13 +10,9 @@ import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:surface/providers/channel.dart'; import 'package:surface/providers/navigation.dart'; -import 'package:surface/providers/sn_network.dart'; -import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/widgets/account/account_image.dart'; -import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/version_label.dart'; class AppNavigationDrawer extends StatefulWidget { @@ -151,163 +146,3 @@ class _AppNavigationDrawerState extends State { ); } } - -class _DrawerContentList extends StatelessWidget { - const _DrawerContentList(); - - @override - Widget build(BuildContext context) { - final ct = context.read(); - final sn = context.read(); - final nav = context.watch(); - final rel = context.watch(); - - return PageTransitionSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation primaryAnimation, - Animation secondaryAnimation) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - fillColor: Colors.transparent, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, - child: nav.focusedRealm == null - ? ListView( - key: const Key('realm-list-view'), - padding: EdgeInsets.zero, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Solar Network').bold(), - AppVersionLabel(), - ], - ).padding( - horizontal: 32, - vertical: 12, - ), - ...rel.availableRealms.map((ele) { - return ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.symmetric(horizontal: 24), - leading: AccountImage( - content: ele.avatar, - radius: 16, - ), - title: Text(ele.name), - onTap: () { - nav.setFocusedRealm(ele); - }, - ); - }), - ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.only(left: 28, right: 16), - leading: const Icon(Symbols.globe).padding(right: 4), - title: Text('screenRealmDiscovery').tr(), - onTap: () { - GoRouter.of(context).pushNamed('realmDiscovery'); - Scaffold.of(context).closeDrawer(); - }, - ), - ], - ) - : ListView( - key: ValueKey(nav.focusedRealm), - padding: EdgeInsets.zero, - children: [ - if (nav.focusedRealm!.banner != null) - AspectRatio( - aspectRatio: 16 / 9, - child: AutoResizeUniversalImage( - sn.getAttachmentUrl( - nav.focusedRealm!.banner!, - ), - fit: BoxFit.cover, - ), - ), - ListTile( - minTileHeight: 48, - tileColor: Theme.of(context).colorScheme.surfaceContainer, - contentPadding: EdgeInsets.only( - left: 24, - right: 16, - ), - leading: AccountImage( - content: nav.focusedRealm!.avatar, - radius: 16, - ), - trailing: IconButton( - icon: const Icon(Symbols.close), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - onPressed: () { - nav.setFocusedRealm(null); - }, - ), - title: Text(nav.focusedRealm!.name), - onTap: () { - GoRouter.of(context).goNamed( - 'realmDetail', - pathParameters: { - 'alias': nav.focusedRealm!.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ), - ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.only( - left: 28, - right: 8, - ), - leading: const Icon(Symbols.globe), - title: Text('community').tr(), - onTap: () { - GoRouter.of(context).goNamed( - 'realmCommunity', - pathParameters: { - 'alias': nav.focusedRealm!.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ), - if (ct.availableChannels - .where((ele) => ele.realmId == nav.focusedRealm?.id) - .isNotEmpty) - const Divider(height: 1), - ...(ct.availableChannels - .where((ele) => ele.realmId == nav.focusedRealm?.id) - .map((ele) { - return ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.only( - left: 28, - right: 8, - ), - leading: const Icon(Symbols.tag), - title: Text(ele.name), - onTap: () { - GoRouter.of(context).goNamed( - 'chatRoom', - pathParameters: { - 'scope': ele.realm?.alias ?? 'global', - 'alias': ele.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ); - })) - ], - ), - ); - } -}