diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 8c84dac..5ccfc2c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -248,5 +248,9 @@ "openLinkConfirm": "Leaving the Solar Network", "openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.", "brokenLink": "Unable open link {}... It might be broken or missing uri parts...", - "copyToClipboard": "Copy to clipboard" + "copyToClipboard": "Copy to clipboard", + "leaveChatRoom": "Leave Chat Room", + "leaveChatRoomHint": "Are you sure to leave this chat room?", + "leaveRealm": "Leave Realm", + "leaveRealmHint": "Are you sure to leave this realm?" } diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index 0616295..c44c97d 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:island/route.gr.dart'; +import 'package:island/services/responsive.dart'; import 'package:material_symbols_icons/symbols.dart'; @RoutePage() @@ -10,6 +11,9 @@ class TabsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final useHorizontalLayout = + MediaQuery.of(context).size.width > kWideScreenWidth; + return AutoTabsRouter.pageView( routes: const [ ExploreRoute(), @@ -17,6 +21,8 @@ class TabsScreen extends StatelessWidget { RealmListRoute(), AccountRoute(), ], + scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), builder: (context, child, _) { final tabsRouter = AutoTabsRouter.of(context); return Scaffold( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 1e294a6..ae6d343 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -759,9 +759,13 @@ class _ChatInput extends StatelessWidget { controller: messageController, decoration: InputDecoration( hintText: - chatRoom.type == 1 + (chatRoom.type == 1 && chatRoom.name == null) ? 'chatDirectMessageHint'.tr( - args: [chatRoom.members!.first.account.nick], + args: [ + chatRoom.members! + .map((e) => e.account.nick) + .join(', '), + ], ) : 'chatMessageHint'.tr(args: [chatRoom.name!]), border: InputBorder.none, diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 4fde271..98a0e55 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -27,13 +27,6 @@ class ChatDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final roomState = ref.watch(chatroomProvider(id)); - final roomIdentity = ref.watch(chatroomIdentityProvider(id)); - - final isModerator = roomIdentity.when( - loading: () => false, - error: (error, _) => false, - data: (identity) => (identity?.role ?? 0) >= 50, - ); const iconShadow = Shadow( color: Colors.black54, @@ -110,8 +103,7 @@ class ChatDetailScreen extends HookConsumerWidget { ); }, ), - if (isModerator) - _ChatRoomActionMenu(id: id, iconShadow: iconShadow), + _ChatRoomActionMenu(id: id, iconShadow: iconShadow), const Gap(8), ], ), @@ -144,54 +136,93 @@ class _ChatRoomActionMenu extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final chatIdentity = ref.watch(chatroomIdentityProvider(id)); + return PopupMenuButton( icon: Icon(Icons.more_vert, shadows: [iconShadow]), itemBuilder: (context) => [ - PopupMenuItem( - onTap: () { - context.router.replace(EditChatRoute(id: id)); - }, - child: Row( - children: [ - Icon( - Icons.edit, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const Gap(12), - const Text('editChatRoom').tr(), - ], + if ((chatIdentity.value?.role ?? 0) >= 50) + PopupMenuItem( + onTap: () { + context.router.replace(EditChatRoute(id: id)); + }, + child: Row( + children: [ + Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const Gap(12), + const Text('editChatRoom').tr(), + ], + ), ), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.delete, color: Colors.red), - const Gap(12), - const Text( - 'deleteChatRoom', - style: TextStyle(color: Colors.red), - ).tr(), - ], - ), - onTap: () { - showConfirmAlert( - 'deleteChatRoomHint'.tr(), - 'deleteChatRoom'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.watch(apiClientProvider); - client.delete('/chat/$id'); - ref.invalidate(chatroomsJoinedProvider); - if (context.mounted) { - context.router.popUntil( - (route) => route is ChatRoomRoute, - ); + if ((chatIdentity.value?.role ?? 0) >= 100) + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const Gap(12), + const Text( + 'deleteChatRoom', + style: TextStyle(color: Colors.red), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'deleteChatRoomHint'.tr(), + 'deleteChatRoom'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/chat/$id'); + ref.invalidate(chatroomsJoinedProvider); + if (context.mounted) { + context.router.popUntil( + (route) => route is ChatRoomRoute, + ); + } } - } - }); - }, - ), + }); + }, + ) + else + PopupMenuItem( + child: Row( + children: [ + Icon( + Icons.exit_to_app, + color: Theme.of(context).colorScheme.error, + ), + const Gap(12), + Text( + 'leaveChatRoom', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'leaveChatRoomHint'.tr(), + 'leaveChatRoom'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/chat/$id/members/me'); + ref.invalidate(chatroomsJoinedProvider); + if (context.mounted) { + context.router.popUntil( + (route) => route is ChatRoomRoute, + ); + } + } + }); + }, + ), ], ); } diff --git a/lib/screens/realm/detail.dart b/lib/screens/realm/detail.dart index fca22f7..4b147f0 100644 --- a/lib/screens/realm/detail.dart +++ b/lib/screens/realm/detail.dart @@ -34,13 +34,6 @@ class RealmDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final realmState = ref.watch(realmProvider(slug)); - final realmIdentity = ref.watch(realmIdentityProvider(slug)); - - final isModerator = realmIdentity.when( - loading: () => false, - error: (error, _) => false, - data: (identity) => (identity?.role ?? 0) >= 50, - ); const iconShadow = Shadow( color: Colors.black54, @@ -88,8 +81,7 @@ class RealmDetailScreen extends HookConsumerWidget { ); }, ), - if (isModerator) - _RealmActionMenu(realmSlug: slug, iconShadow: iconShadow), + _RealmActionMenu(realmSlug: slug, iconShadow: iconShadow), const Gap(8), ], ), @@ -122,49 +114,135 @@ class _RealmActionMenu extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final realmIdentityAsync = ref.watch(realmIdentityProvider(realmSlug)); + final isModerator = realmIdentityAsync.when( + data: (identity) => (identity?.role ?? 0) >= 50, + loading: () => false, + error: (_, __) => false, + ); + return PopupMenuButton( icon: Icon(Icons.more_vert, shadows: [iconShadow]), itemBuilder: (context) => [ - PopupMenuItem( - onTap: () { - context.router.replace(EditRealmRoute(slug: realmSlug)); - }, - child: Row( - children: [ - Icon( - Icons.edit, - color: Theme.of(context).colorScheme.onSecondaryContainer, + if (isModerator) + PopupMenuItem( + onTap: () { + context.router.replace(EditRealmRoute(slug: realmSlug)); + }, + child: Row( + children: [ + Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const Gap(12), + const Text('editRealm').tr(), + ], + ), + ), + realmIdentityAsync.when( + data: + (identity) => + (identity?.role ?? 0) >= 100 + ? PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const Gap(12), + const Text( + 'deleteRealm', + style: TextStyle(color: Colors.red), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'deleteRealmHint'.tr(), + 'deleteRealm'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/realms/$realmSlug'); + ref.invalidate(realmsJoinedProvider); + if (context.mounted) + context.router.maybePop(true); + } + }); + }, + ) + : PopupMenuItem( + child: Row( + children: [ + Icon( + Icons.exit_to_app, + color: Theme.of(context).colorScheme.error, + ), + const Gap(12), + Text( + 'leaveRealm', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'leaveRealmHint'.tr(), + 'leaveRealm'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete( + '/realms/$realmSlug/members/me', + ); + ref.invalidate(realmsJoinedProvider); + if (context.mounted) { + context.router.maybePop(true); + } + } + }); + }, + ), + loading: + () => const PopupMenuItem( + enabled: false, + child: Center(child: CircularProgressIndicator()), + ), + error: + (_, __) => PopupMenuItem( + child: Row( + children: [ + Icon( + Icons.exit_to_app, + color: Theme.of(context).colorScheme.error, + ), + const Gap(12), + Text( + 'leaveRealm', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'leaveRealmHint'.tr(), + 'leaveRealm'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/realms/$realmSlug/members/me'); + ref.invalidate(realmsJoinedProvider); + if (context.mounted) { + context.router.maybePop(true); + } + } + }); + }, ), - const Gap(12), - const Text('editRealm').tr(), - ], - ), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.delete, color: Colors.red), - const Gap(12), - const Text( - 'deleteRealm', - style: TextStyle(color: Colors.red), - ).tr(), - ], - ), - onTap: () { - showConfirmAlert( - 'deleteRealmHint'.tr(), - 'deleteRealm'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.watch(apiClientProvider); - client.delete('/realms/$realmSlug'); - ref.invalidate(realmsJoinedProvider); - if (context.mounted) context.router.maybePop(true); - } - }); - }, ), ], ); diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index ebf7924..4fdc63a 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -379,7 +379,7 @@ class _RealmInviteSheet extends HookConsumerWidget { Future acceptInvite(SnRealmMember invite) async { try { final client = ref.read(apiClientProvider); - await client.post('/realms/invites/${invite.realm!.id}/accept'); + await client.post('/realms/invites/${invite.realm!.slug}/accept'); ref.invalidate(realmInvitesProvider); ref.invalidate(realmsJoinedProvider); } catch (err) { @@ -390,7 +390,7 @@ class _RealmInviteSheet extends HookConsumerWidget { Future declineInvite(SnRealmMember invite) async { try { final client = ref.read(apiClientProvider); - await client.post('/realms/invites/${invite.realm!.id}/decline'); + await client.post('/realms/invites/${invite.realm!.slug}/decline'); ref.invalidate(realmInvitesProvider); } catch (err) { showErrorAlert(err); @@ -452,7 +452,6 @@ class _RealmInviteSheet extends HookConsumerWidget { return ListTile( leading: ProfilePictureWidget( fileId: invite.realm!.pictureId, - radius: 24, fallbackIcon: Symbols.group, ), title: Text(invite.realm!.name), diff --git a/lib/services/responsive.dart b/lib/services/responsive.dart new file mode 100644 index 0000000..37db4a4 --- /dev/null +++ b/lib/services/responsive.dart @@ -0,0 +1 @@ +const kWideScreenWidth = 640; diff --git a/lib/widgets/chat/message_item.dart b/lib/widgets/chat/message_item.dart index 85ee280..9970e23 100644 --- a/lib/widgets/chat/message_item.dart +++ b/lib/widgets/chat/message_item.dart @@ -101,6 +101,7 @@ class MessageItem extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (showAvatar) ...[ + const Gap(8), Row( spacing: 8, mainAxisSize: MainAxisSize.min,