Better joining and leaving

This commit is contained in:
LittleSheep 2025-05-18 20:52:32 +08:00
parent 9c0221ab20
commit ad41de3674
8 changed files with 228 additions and 104 deletions

View File

@ -248,5 +248,9 @@
"openLinkConfirm": "Leaving the Solar Network", "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.", "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...", "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?"
} }

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@RoutePage() @RoutePage()
@ -10,6 +11,9 @@ class TabsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final useHorizontalLayout =
MediaQuery.of(context).size.width > kWideScreenWidth;
return AutoTabsRouter.pageView( return AutoTabsRouter.pageView(
routes: const [ routes: const [
ExploreRoute(), ExploreRoute(),
@ -17,6 +21,8 @@ class TabsScreen extends StatelessWidget {
RealmListRoute(), RealmListRoute(),
AccountRoute(), AccountRoute(),
], ],
scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, _) { builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context); final tabsRouter = AutoTabsRouter.of(context);
return Scaffold( return Scaffold(

View File

@ -759,9 +759,13 @@ class _ChatInput extends StatelessWidget {
controller: messageController, controller: messageController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
chatRoom.type == 1 (chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr( ? 'chatDirectMessageHint'.tr(
args: [chatRoom.members!.first.account.nick], args: [
chatRoom.members!
.map((e) => e.account.nick)
.join(', '),
],
) )
: 'chatMessageHint'.tr(args: [chatRoom.name!]), : 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none, border: InputBorder.none,

View File

@ -27,13 +27,6 @@ class ChatDetailScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final roomState = ref.watch(chatroomProvider(id)); 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( const iconShadow = Shadow(
color: Colors.black54, 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), const Gap(8),
], ],
), ),
@ -144,54 +136,93 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
return PopupMenuButton( return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]), icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder: itemBuilder:
(context) => [ (context) => [
PopupMenuItem( if ((chatIdentity.value?.role ?? 0) >= 50)
onTap: () { PopupMenuItem(
context.router.replace(EditChatRoute(id: id)); onTap: () {
}, context.router.replace(EditChatRoute(id: id));
child: Row( },
children: [ child: Row(
Icon( children: [
Icons.edit, Icon(
color: Theme.of(context).colorScheme.onSecondaryContainer, Icons.edit,
), color: Theme.of(context).colorScheme.onSecondaryContainer,
const Gap(12), ),
const Text('editChatRoom').tr(), const Gap(12),
], const Text('editChatRoom').tr(),
],
),
), ),
), if ((chatIdentity.value?.role ?? 0) >= 100)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.delete, color: Colors.red), const Icon(Icons.delete, color: Colors.red),
const Gap(12), const Gap(12),
const Text( const Text(
'deleteChatRoom', 'deleteChatRoom',
style: TextStyle(color: Colors.red), style: TextStyle(color: Colors.red),
).tr(), ).tr(),
], ],
), ),
onTap: () { onTap: () {
showConfirmAlert( showConfirmAlert(
'deleteChatRoomHint'.tr(), 'deleteChatRoomHint'.tr(),
'deleteChatRoom'.tr(), 'deleteChatRoom'.tr(),
).then((confirm) { ).then((confirm) {
if (confirm) { if (confirm) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
client.delete('/chat/$id'); client.delete('/chat/$id');
ref.invalidate(chatroomsJoinedProvider); ref.invalidate(chatroomsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.router.popUntil( context.router.popUntil(
(route) => route is ChatRoomRoute, (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,
);
}
}
});
},
),
], ],
); );
} }

View File

@ -34,13 +34,6 @@ class RealmDetailScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final realmState = ref.watch(realmProvider(slug)); 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( const iconShadow = Shadow(
color: Colors.black54, 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), const Gap(8),
], ],
), ),
@ -122,49 +114,135 @@ class _RealmActionMenu extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]), icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder: itemBuilder:
(context) => [ (context) => [
PopupMenuItem( if (isModerator)
onTap: () { PopupMenuItem(
context.router.replace(EditRealmRoute(slug: realmSlug)); onTap: () {
}, context.router.replace(EditRealmRoute(slug: realmSlug));
child: Row( },
children: [ child: Row(
Icon( children: [
Icons.edit, Icon(
color: Theme.of(context).colorScheme.onSecondaryContainer, 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);
}
});
},
), ),
], ],
); );

View File

@ -379,7 +379,7 @@ class _RealmInviteSheet extends HookConsumerWidget {
Future<void> acceptInvite(SnRealmMember invite) async { Future<void> acceptInvite(SnRealmMember invite) async {
try { try {
final client = ref.read(apiClientProvider); 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(realmInvitesProvider);
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
} catch (err) { } catch (err) {
@ -390,7 +390,7 @@ class _RealmInviteSheet extends HookConsumerWidget {
Future<void> declineInvite(SnRealmMember invite) async { Future<void> declineInvite(SnRealmMember invite) async {
try { try {
final client = ref.read(apiClientProvider); 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); ref.invalidate(realmInvitesProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@ -452,7 +452,6 @@ class _RealmInviteSheet extends HookConsumerWidget {
return ListTile( return ListTile(
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: invite.realm!.pictureId, fileId: invite.realm!.pictureId,
radius: 24,
fallbackIcon: Symbols.group, fallbackIcon: Symbols.group,
), ),
title: Text(invite.realm!.name), title: Text(invite.realm!.name),

View File

@ -0,0 +1 @@
const kWideScreenWidth = 640;

View File

@ -101,6 +101,7 @@ class MessageItem extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (showAvatar) ...[ if (showAvatar) ...[
const Gap(8),
Row( Row(
spacing: 8, spacing: 8,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,