diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 08e0c71..9670498 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -70,5 +70,10 @@ "name": "Name", "description": "Description", "slug": "Slug", - "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe." + "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.", + "createChatRoom": "Create a Room", + "editChatRoom": "Edit a Room", + "chat": "Chat", + "chatMessageHint": "Message in {}", + "chatDirectMessageHint": "Message to {}" } diff --git a/lib/route.dart b/lib/route.dart index 5b25f16..3058d4d 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -16,6 +16,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: ExploreRoute.page, path: 'explore'), AutoRoute(page: AccountRoute.page, path: 'account'), AutoRoute(page: RealmListRoute.page, path: 'realms'), + AutoRoute(page: ChatListRoute.page, path: 'chat'), ], ), AutoRoute(page: LoginRoute.page, path: '/auth/login'), @@ -33,5 +34,8 @@ class AppRouter extends RootStackRouter { AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), + AutoRoute(page: NewChatRoute.page, path: '/chat/new'), + AutoRoute(page: EditChatRoute.page, path: '/chat/:id/edit'), + AutoRoute(page: ChatRoomRoute.page, path: '/chat/:id'), ]; } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 12aa68b..87d57e4 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -9,30 +9,32 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i12; -import 'package:flutter/material.dart' as _i13; -import 'package:island/models/post.dart' as _i14; +import 'package:auto_route/auto_route.dart' as _i14; +import 'package:flutter/material.dart' as _i15; +import 'package:island/models/post.dart' as _i16; import 'package:island/screens/account.dart' as _i1; -import 'package:island/screens/account/me.dart' as _i7; -import 'package:island/screens/account/me/publishers.dart' as _i3; -import 'package:island/screens/account/me/update.dart' as _i11; -import 'package:island/screens/auth/create_account.dart' as _i2; -import 'package:island/screens/auth/login.dart' as _i6; -import 'package:island/screens/auth/tabs.dart' as _i10; -import 'package:island/screens/explore.dart' as _i5; -import 'package:island/screens/posts/compose.dart' as _i8; -import 'package:island/screens/posts/detail.dart' as _i9; -import 'package:island/screens/realm/realms.dart' as _i4; +import 'package:island/screens/account/me.dart' as _i9; +import 'package:island/screens/account/me/publishers.dart' as _i5; +import 'package:island/screens/account/me/update.dart' as _i13; +import 'package:island/screens/auth/create_account.dart' as _i4; +import 'package:island/screens/auth/login.dart' as _i8; +import 'package:island/screens/auth/tabs.dart' as _i12; +import 'package:island/screens/chat/chat.dart' as _i2; +import 'package:island/screens/chat/room.dart' as _i3; +import 'package:island/screens/explore.dart' as _i7; +import 'package:island/screens/posts/compose.dart' as _i10; +import 'package:island/screens/posts/detail.dart' as _i11; +import 'package:island/screens/realm/realms.dart' as _i6; /// generated route for /// [_i1.AccountScreen] -class AccountRoute extends _i12.PageRouteInfo { - const AccountRoute({List<_i12.PageRouteInfo>? children}) +class AccountRoute extends _i14.PageRouteInfo { + const AccountRoute({List<_i14.PageRouteInfo>? children}) : super(AccountRoute.name, initialChildren: children); static const String name = 'AccountRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { return const _i1.AccountScreen(); @@ -41,28 +43,123 @@ class AccountRoute extends _i12.PageRouteInfo { } /// generated route for -/// [_i2.CreateAccountScreen] -class CreateAccountRoute extends _i12.PageRouteInfo { - const CreateAccountRoute({List<_i12.PageRouteInfo>? children}) - : super(CreateAccountRoute.name, initialChildren: children); +/// [_i2.ChatListScreen] +class ChatListRoute extends _i14.PageRouteInfo { + const ChatListRoute({List<_i14.PageRouteInfo>? children}) + : super(ChatListRoute.name, initialChildren: children); - static const String name = 'CreateAccountRoute'; + static const String name = 'ChatListRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i2.CreateAccountScreen(); + return const _i2.ChatListScreen(); }, ); } /// generated route for -/// [_i3.EditPublisherScreen] -class EditPublisherRoute extends _i12.PageRouteInfo { +/// [_i3.ChatRoomScreen] +class ChatRoomRoute extends _i14.PageRouteInfo { + ChatRoomRoute({ + _i15.Key? key, + required int id, + List<_i14.PageRouteInfo>? children, + }) : super( + ChatRoomRoute.name, + args: ChatRoomRouteArgs(key: key, id: id), + rawPathParams: {'id': id}, + initialChildren: children, + ); + + static const String name = 'ChatRoomRoute'; + + static _i14.PageInfo page = _i14.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: () => ChatRoomRouteArgs(id: pathParams.getInt('id')), + ); + return _i3.ChatRoomScreen(key: args.key, id: args.id); + }, + ); +} + +class ChatRoomRouteArgs { + const ChatRoomRouteArgs({this.key, required this.id}); + + final _i15.Key? key; + + final int id; + + @override + String toString() { + return 'ChatRoomRouteArgs{key: $key, id: $id}'; + } +} + +/// generated route for +/// [_i4.CreateAccountScreen] +class CreateAccountRoute extends _i14.PageRouteInfo { + const CreateAccountRoute({List<_i14.PageRouteInfo>? children}) + : super(CreateAccountRoute.name, initialChildren: children); + + static const String name = 'CreateAccountRoute'; + + static _i14.PageInfo page = _i14.PageInfo( + name, + builder: (data) { + return const _i4.CreateAccountScreen(); + }, + ); +} + +/// generated route for +/// [_i2.EditChatScreen] +class EditChatRoute extends _i14.PageRouteInfo { + EditChatRoute({_i15.Key? key, int? id, List<_i14.PageRouteInfo>? children}) + : super( + EditChatRoute.name, + args: EditChatRouteArgs(key: key, id: id), + rawPathParams: {'id': id}, + initialChildren: children, + ); + + static const String name = 'EditChatRoute'; + + static _i14.PageInfo page = _i14.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: () => EditChatRouteArgs(id: pathParams.optInt('id')), + ); + return _i2.EditChatScreen(key: args.key, id: args.id); + }, + ); +} + +class EditChatRouteArgs { + const EditChatRouteArgs({this.key, this.id}); + + final _i15.Key? key; + + final int? id; + + @override + String toString() { + return 'EditChatRouteArgs{key: $key, id: $id}'; + } +} + +/// generated route for +/// [_i5.EditPublisherScreen] +class EditPublisherRoute extends _i14.PageRouteInfo { EditPublisherRoute({ - _i13.Key? key, + _i15.Key? key, String? name, - List<_i12.PageRouteInfo>? children, + List<_i14.PageRouteInfo>? children, }) : super( EditPublisherRoute.name, args: EditPublisherRouteArgs(key: key, name: name), @@ -72,14 +169,14 @@ class EditPublisherRoute extends _i12.PageRouteInfo { static const String name = 'EditPublisherRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditPublisherRouteArgs(name: pathParams.optString('id')), ); - return _i3.EditPublisherScreen(key: args.key, name: args.name); + return _i5.EditPublisherScreen(key: args.key, name: args.name); }, ); } @@ -87,7 +184,7 @@ class EditPublisherRoute extends _i12.PageRouteInfo { class EditPublisherRouteArgs { const EditPublisherRouteArgs({this.key, this.name}); - final _i13.Key? key; + final _i15.Key? key; final String? name; @@ -98,12 +195,12 @@ class EditPublisherRouteArgs { } /// generated route for -/// [_i4.EditRealmScreen] -class EditRealmRoute extends _i12.PageRouteInfo { +/// [_i6.EditRealmScreen] +class EditRealmRoute extends _i14.PageRouteInfo { EditRealmRoute({ - _i13.Key? key, + _i15.Key? key, String? slug, - List<_i12.PageRouteInfo>? children, + List<_i14.PageRouteInfo>? children, }) : super( EditRealmRoute.name, args: EditRealmRouteArgs(key: key, slug: slug), @@ -113,14 +210,14 @@ class EditRealmRoute extends _i12.PageRouteInfo { static const String name = 'EditRealmRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditRealmRouteArgs(slug: pathParams.optString('slug')), ); - return _i4.EditRealmScreen(key: args.key, slug: args.slug); + return _i6.EditRealmScreen(key: args.key, slug: args.slug); }, ); } @@ -128,7 +225,7 @@ class EditRealmRoute extends _i12.PageRouteInfo { class EditRealmRouteArgs { const EditRealmRouteArgs({this.key, this.slug}); - final _i13.Key? key; + final _i15.Key? key; final String? slug; @@ -139,108 +236,124 @@ class EditRealmRouteArgs { } /// generated route for -/// [_i5.ExploreScreen] -class ExploreRoute extends _i12.PageRouteInfo { - const ExploreRoute({List<_i12.PageRouteInfo>? children}) +/// [_i7.ExploreScreen] +class ExploreRoute extends _i14.PageRouteInfo { + const ExploreRoute({List<_i14.PageRouteInfo>? children}) : super(ExploreRoute.name, initialChildren: children); static const String name = 'ExploreRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i5.ExploreScreen(); + return const _i7.ExploreScreen(); }, ); } /// generated route for -/// [_i6.LoginScreen] -class LoginRoute extends _i12.PageRouteInfo { - const LoginRoute({List<_i12.PageRouteInfo>? children}) +/// [_i8.LoginScreen] +class LoginRoute extends _i14.PageRouteInfo { + const LoginRoute({List<_i14.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i6.LoginScreen(); + return const _i8.LoginScreen(); }, ); } /// generated route for -/// [_i3.ManagedPublisherScreen] -class ManagedPublisherRoute extends _i12.PageRouteInfo { - const ManagedPublisherRoute({List<_i12.PageRouteInfo>? children}) +/// [_i5.ManagedPublisherScreen] +class ManagedPublisherRoute extends _i14.PageRouteInfo { + const ManagedPublisherRoute({List<_i14.PageRouteInfo>? children}) : super(ManagedPublisherRoute.name, initialChildren: children); static const String name = 'ManagedPublisherRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i3.ManagedPublisherScreen(); + return const _i5.ManagedPublisherScreen(); }, ); } /// generated route for -/// [_i7.MyselfProfileScreen] -class MyselfProfileRoute extends _i12.PageRouteInfo { - const MyselfProfileRoute({List<_i12.PageRouteInfo>? children}) +/// [_i9.MyselfProfileScreen] +class MyselfProfileRoute extends _i14.PageRouteInfo { + const MyselfProfileRoute({List<_i14.PageRouteInfo>? children}) : super(MyselfProfileRoute.name, initialChildren: children); static const String name = 'MyselfProfileRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i7.MyselfProfileScreen(); + return const _i9.MyselfProfileScreen(); }, ); } /// generated route for -/// [_i3.NewPublisherScreen] -class NewPublisherRoute extends _i12.PageRouteInfo { - const NewPublisherRoute({List<_i12.PageRouteInfo>? children}) +/// [_i2.NewChatScreen] +class NewChatRoute extends _i14.PageRouteInfo { + const NewChatRoute({List<_i14.PageRouteInfo>? children}) + : super(NewChatRoute.name, initialChildren: children); + + static const String name = 'NewChatRoute'; + + static _i14.PageInfo page = _i14.PageInfo( + name, + builder: (data) { + return const _i2.NewChatScreen(); + }, + ); +} + +/// generated route for +/// [_i5.NewPublisherScreen] +class NewPublisherRoute extends _i14.PageRouteInfo { + const NewPublisherRoute({List<_i14.PageRouteInfo>? children}) : super(NewPublisherRoute.name, initialChildren: children); static const String name = 'NewPublisherRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i3.NewPublisherScreen(); + return const _i5.NewPublisherScreen(); }, ); } /// generated route for -/// [_i4.NewRealmScreen] -class NewRealmRoute extends _i12.PageRouteInfo { - const NewRealmRoute({List<_i12.PageRouteInfo>? children}) +/// [_i6.NewRealmScreen] +class NewRealmRoute extends _i14.PageRouteInfo { + const NewRealmRoute({List<_i14.PageRouteInfo>? children}) : super(NewRealmRoute.name, initialChildren: children); static const String name = 'NewRealmRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i4.NewRealmScreen(); + return const _i6.NewRealmScreen(); }, ); } /// generated route for -/// [_i8.PostComposeScreen] -class PostComposeRoute extends _i12.PageRouteInfo { +/// [_i10.PostComposeScreen] +class PostComposeRoute extends _i14.PageRouteInfo { PostComposeRoute({ - _i13.Key? key, - _i14.SnPost? originalPost, - List<_i12.PageRouteInfo>? children, + _i15.Key? key, + _i16.SnPost? originalPost, + List<_i14.PageRouteInfo>? children, }) : super( PostComposeRoute.name, args: PostComposeRouteArgs(key: key, originalPost: originalPost), @@ -249,13 +362,13 @@ class PostComposeRoute extends _i12.PageRouteInfo { static const String name = 'PostComposeRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const PostComposeRouteArgs(), ); - return _i8.PostComposeScreen( + return _i10.PostComposeScreen( key: args.key, originalPost: args.originalPost, ); @@ -266,9 +379,9 @@ class PostComposeRoute extends _i12.PageRouteInfo { class PostComposeRouteArgs { const PostComposeRouteArgs({this.key, this.originalPost}); - final _i13.Key? key; + final _i15.Key? key; - final _i14.SnPost? originalPost; + final _i16.SnPost? originalPost; @override String toString() { @@ -277,12 +390,12 @@ class PostComposeRouteArgs { } /// generated route for -/// [_i9.PostDetailScreen] -class PostDetailRoute extends _i12.PageRouteInfo { +/// [_i11.PostDetailScreen] +class PostDetailRoute extends _i14.PageRouteInfo { PostDetailRoute({ - _i13.Key? key, + _i15.Key? key, required int id, - List<_i12.PageRouteInfo>? children, + List<_i14.PageRouteInfo>? children, }) : super( PostDetailRoute.name, args: PostDetailRouteArgs(key: key, id: id), @@ -292,14 +405,14 @@ class PostDetailRoute extends _i12.PageRouteInfo { static const String name = 'PostDetailRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostDetailRouteArgs(id: pathParams.getInt('id')), ); - return _i9.PostDetailScreen(key: args.key, id: args.id); + return _i11.PostDetailScreen(key: args.key, id: args.id); }, ); } @@ -307,7 +420,7 @@ class PostDetailRoute extends _i12.PageRouteInfo { class PostDetailRouteArgs { const PostDetailRouteArgs({this.key, required this.id}); - final _i13.Key? key; + final _i15.Key? key; final int id; @@ -318,12 +431,12 @@ class PostDetailRouteArgs { } /// generated route for -/// [_i8.PostEditScreen] -class PostEditRoute extends _i12.PageRouteInfo { +/// [_i10.PostEditScreen] +class PostEditRoute extends _i14.PageRouteInfo { PostEditRoute({ - _i13.Key? key, + _i15.Key? key, required int id, - List<_i12.PageRouteInfo>? children, + List<_i14.PageRouteInfo>? children, }) : super( PostEditRoute.name, args: PostEditRouteArgs(key: key, id: id), @@ -333,14 +446,14 @@ class PostEditRoute extends _i12.PageRouteInfo { static const String name = 'PostEditRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostEditRouteArgs(id: pathParams.getInt('id')), ); - return _i8.PostEditScreen(key: args.key, id: args.id); + return _i10.PostEditScreen(key: args.key, id: args.id); }, ); } @@ -348,7 +461,7 @@ class PostEditRoute extends _i12.PageRouteInfo { class PostEditRouteArgs { const PostEditRouteArgs({this.key, required this.id}); - final _i13.Key? key; + final _i15.Key? key; final int id; @@ -359,49 +472,49 @@ class PostEditRouteArgs { } /// generated route for -/// [_i4.RealmListScreen] -class RealmListRoute extends _i12.PageRouteInfo { - const RealmListRoute({List<_i12.PageRouteInfo>? children}) +/// [_i6.RealmListScreen] +class RealmListRoute extends _i14.PageRouteInfo { + const RealmListRoute({List<_i14.PageRouteInfo>? children}) : super(RealmListRoute.name, initialChildren: children); static const String name = 'RealmListRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i4.RealmListScreen(); + return const _i6.RealmListScreen(); }, ); } /// generated route for -/// [_i10.TabsScreen] -class TabsRoute extends _i12.PageRouteInfo { - const TabsRoute({List<_i12.PageRouteInfo>? children}) +/// [_i12.TabsScreen] +class TabsRoute extends _i14.PageRouteInfo { + const TabsRoute({List<_i14.PageRouteInfo>? children}) : super(TabsRoute.name, initialChildren: children); static const String name = 'TabsRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i10.TabsScreen(); + return const _i12.TabsScreen(); }, ); } /// generated route for -/// [_i11.UpdateProfileScreen] -class UpdateProfileRoute extends _i12.PageRouteInfo { - const UpdateProfileRoute({List<_i12.PageRouteInfo>? children}) +/// [_i13.UpdateProfileScreen] +class UpdateProfileRoute extends _i14.PageRouteInfo { + const UpdateProfileRoute({List<_i14.PageRouteInfo>? children}) : super(UpdateProfileRoute.name, initialChildren: children); static const String name = 'UpdateProfileRoute'; - static _i12.PageInfo page = _i12.PageInfo( + static _i14.PageInfo page = _i14.PageInfo( name, builder: (data) { - return const _i11.UpdateProfileScreen(); + return const _i13.UpdateProfileScreen(); }, ); } diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index 7ec429b..0616295 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -11,7 +11,12 @@ class TabsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return AutoTabsRouter.pageView( - routes: const [ExploreRoute(), RealmListRoute(), AccountRoute()], + routes: const [ + ExploreRoute(), + ChatListRoute(), + RealmListRoute(), + AccountRoute(), + ], builder: (context, child, _) { final tabsRouter = AutoTabsRouter.of(context); return Scaffold( @@ -26,6 +31,10 @@ class TabsScreen extends StatelessWidget { label: 'explore'.tr(), icon: const Icon(Symbols.explore), ), + NavigationDestination( + label: 'chat'.tr(), + icon: const Icon(Symbols.chat), + ), NavigationDestination( label: 'realms'.tr(), icon: const Icon(Symbols.workspaces), diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart new file mode 100644 index 0000000..6a182b2 --- /dev/null +++ b/lib/screens/chat/chat.dart @@ -0,0 +1,276 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:island/models/chat.dart'; +import 'package:island/models/file.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/route.gr.dart'; +import 'package:island/services/file.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:styled_widget/styled_widget.dart'; + +part 'chat.g.dart'; + +@riverpod +Future> chatroomsJoined(Ref ref) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/chat'); + return resp.data.map((e) => SnChat.fromJson(e)).cast().toList(); +} + +@RoutePage() +class ChatListScreen extends HookConsumerWidget { + const ChatListScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final chats = ref.watch(chatroomsJoinedProvider); + + return AppScaffold( + appBar: AppBar(title: Text('chat').tr()), + floatingActionButton: FloatingActionButton( + onPressed: () { + context.pushRoute(NewChatRoute()); + }, + child: const Icon(Symbols.add), + ), + body: chats.when( + data: + (items) => RefreshIndicator( + onRefresh: + () => Future.sync(() { + ref.invalidate(chatroomsJoinedProvider); + }), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return ListTile( + leading: + item.picture == null + ? CircleAvatar( + child: Text(item.name[0].toUpperCase()), + ) + : ProfilePictureWidget(item: item.picture), + title: Text(item.name), + subtitle: Text(item.description), + onTap: () { + context.pushRoute(ChatRoomRoute(id: item.id)); + }, + ); + }, + ), + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ), + ); + } +} + +@riverpod +Future chatroom(Ref ref, int? identifier) async { + if (identifier == null) return null; + final client = ref.watch(apiClientProvider); + final resp = await client.get('/chat/$identifier'); + return SnChat.fromJson(resp.data); +} + +@RoutePage() +class NewChatScreen extends StatelessWidget { + const NewChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + return EditChatScreen(); + } +} + +@RoutePage() +class EditChatScreen extends HookConsumerWidget { + final int? id; + const EditChatScreen({super.key, @PathParam("id") this.id}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey(), []); + + final submitting = useState(false); + + final nameController = useTextEditingController(); + final descriptionController = useTextEditingController(); + final picture = useState(null); + final background = useState(null); + + final chat = ref.watch(chatroomProvider(id)); + + useEffect(() { + if (chat.value != null) { + nameController.text = chat.value!.name; + descriptionController.text = chat.value!.description; + picture.value = chat.value!.picture; + background.value = chat.value!.background; + } + return; + }, [chat]); + + void setPicture(String position) async { + final result = await ref + .read(imagePickerProvider) + .pickImage(source: ImageSource.gallery); + if (result == null) return; + + submitting.value = true; + try { + final baseUrl = ref.watch(serverUrlProvider); + final atk = await getFreshAtk( + ref.watch(tokenPairProvider), + baseUrl, + onRefreshed: (atk, rtk) { + setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); + ref.invalidate(tokenPairProvider); + }, + ); + if (atk == null) throw ArgumentError('Access token is null'); + final cloudFile = + await putMediaToCloud( + fileData: result, + atk: atk, + baseUrl: baseUrl, + filename: result.name, + mimetype: result.mimeType ?? 'image/jpeg', + ).future; + if (cloudFile == null) { + throw ArgumentError('Failed to upload the file...'); + } + switch (position) { + case 'picture': + picture.value = cloudFile; + case 'background': + background.value = cloudFile; + } + } catch (err) { + showErrorAlert(err); + } finally { + submitting.value = false; + } + } + + Future performAction() async { + if (!formKey.currentState!.validate()) return; + + submitting.value = true; + try { + final client = ref.watch(apiClientProvider); + final resp = await client.request( + id == null ? '/chat' : '/chat/$id', + data: { + 'name': nameController.text, + 'description': descriptionController.text, + 'background_id': background.value?.id, + 'picture_id': picture.value?.id, + }, + options: Options(method: id == null ? 'POST' : 'PATCH'), + ); + if (context.mounted) { + context.maybePop(SnChat.fromJson(resp.data)); + } + } catch (err) { + showErrorAlert(err); + } finally { + submitting.value = false; + } + } + + return AppScaffold( + appBar: AppBar( + title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(), + leading: const PageBackButton(), + ), + body: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + GestureDetector( + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: + background.value != null + ? CloudFileWidget( + item: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + onTap: () { + setPicture('background'); + }, + ), + Positioned( + left: 20, + bottom: -32, + child: GestureDetector( + child: ProfilePictureWidget( + item: picture.value, + radius: 40, + fallbackIcon: Symbols.group, + ), + onTap: () { + setPicture('picture'); + }, + ), + ), + ], + ), + ).padding(bottom: 32), + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: nameController, + decoration: const InputDecoration(labelText: 'Name'), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: descriptionController, + decoration: const InputDecoration(labelText: 'Description'), + minLines: 3, + maxLines: null, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: submitting.value ? null : performAction, + label: const Text('Save'), + icon: const Icon(Symbols.save), + ), + ), + ], + ).padding(all: 24), + ), + ], + ), + ); + } +} diff --git a/lib/screens/chat/chat.g.dart b/lib/screens/chat/chat.g.dart new file mode 100644 index 0000000..2e13029 --- /dev/null +++ b/lib/screens/chat/chat.g.dart @@ -0,0 +1,167 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatroomsJoinedHash() => r'3a2db4159663c54dfd7bc40519e2faa6df69b41f'; + +/// See also [chatroomsJoined]. +@ProviderFor(chatroomsJoined) +final chatroomsJoinedProvider = + AutoDisposeFutureProvider>.internal( + chatroomsJoined, + name: r'chatroomsJoinedProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatroomsJoinedHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef>; +String _$chatroomHash() => r'27bd4cb49326bb2f2eac7d7db9db7f610e21afb2'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [chatroom]. +@ProviderFor(chatroom) +const chatroomProvider = ChatroomFamily(); + +/// See also [chatroom]. +class ChatroomFamily extends Family> { + /// See also [chatroom]. + const ChatroomFamily(); + + /// See also [chatroom]. + ChatroomProvider call(int? identifier) { + return ChatroomProvider(identifier); + } + + @override + ChatroomProvider getProviderOverride(covariant ChatroomProvider provider) { + return call(provider.identifier); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatroomProvider'; +} + +/// See also [chatroom]. +class ChatroomProvider extends AutoDisposeFutureProvider { + /// See also [chatroom]. + ChatroomProvider(int? identifier) + : this._internal( + (ref) => chatroom(ref as ChatroomRef, identifier), + from: chatroomProvider, + name: r'chatroomProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatroomHash, + dependencies: ChatroomFamily._dependencies, + allTransitiveDependencies: ChatroomFamily._allTransitiveDependencies, + identifier: identifier, + ); + + ChatroomProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.identifier, + }) : super.internal(); + + final int? identifier; + + @override + Override overrideWith( + FutureOr Function(ChatroomRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ChatroomProvider._internal( + (ref) => create(ref as ChatroomRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + identifier: identifier, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _ChatroomProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatroomProvider && other.identifier == identifier; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, identifier.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatroomRef on AutoDisposeFutureProviderRef { + /// The parameter `identifier` of this provider. + int? get identifier; +} + +class _ChatroomProviderElement extends AutoDisposeFutureProviderElement + with ChatroomRef { + _ChatroomProviderElement(super.provider); + + @override + int? get identifier => (origin as ChatroomProvider).identifier; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/chat/new_chat.dart b/lib/screens/chat/new_chat.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart new file mode 100644 index 0000000..6968936 --- /dev/null +++ b/lib/screens/chat/room.dart @@ -0,0 +1,118 @@ +import 'package:auto_route/annotations.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'chat.dart'; + +@RoutePage() +class ChatRoomScreen extends HookConsumerWidget { + final int id; + const ChatRoomScreen({super.key, @PathParam("id") required this.id}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final chatRoom = ref.watch(chatroomProvider(id)); + + final messageController = useTextEditingController(); + + return Scaffold( + appBar: AppBar( + title: chatRoom.when( + data: + (room) => Row( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 26, + width: 26, + child: + room?.picture != null + ? ProfilePictureWidget( + item: room?.picture, + fallbackIcon: Symbols.chat, + ) + : CircleAvatar( + child: Text( + room?.name[0].toUpperCase() ?? '', + style: const TextStyle(fontSize: 12), + ), + ), + ), + Text(room?.name ?? 'unknown').fontSize(19).tr(), + ], + ), + loading: () => const Text('Loading...'), + error: (_, __) => const Text('Error'), + ), + actions: [ + IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), + const Gap(8), + ], + ), + body: Column( + children: [ + Expanded( + child: chatRoom.when( + data: (room) => SizedBox.expand(), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ), + ), + Material( + elevation: 2, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 3, + ), + ], + ), + child: Row( + children: [ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + Expanded( + child: TextField( + controller: messageController, + decoration: InputDecoration( + hintText: 'chatMessageHint'.tr( + args: [chatRoom.value?.name ?? 'unknown'.tr()], + ), + border: OutlineInputBorder(), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + maxLines: null, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + const Gap(8), + IconButton( + icon: const Icon(Icons.send), + color: Theme.of(context).colorScheme.primary, + onPressed: () {}, + ), + ], + ).padding(bottom: MediaQuery.of(context).padding.bottom), + ), + ), + ], + ), + ); + } +}