Direct messages

This commit is contained in:
2025-05-04 16:05:18 +08:00
parent 45fe3d191d
commit 4b6a5c28de
12 changed files with 278 additions and 114 deletions

View File

@ -1,7 +1,10 @@
import 'dart:convert';
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_expandable_fab/flutter_expandable_fab.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -12,6 +15,7 @@ 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/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';
@ -23,10 +27,13 @@ import 'package:styled_widget/styled_widget.dart';
part 'chat.g.dart';
@riverpod
Future<List<SnChat>> chatroomsJoined(Ref ref) async {
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/chat');
return resp.data.map((e) => SnChat.fromJson(e)).cast<SnChat>().toList();
return resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
.toList();
}
@RoutePage()
@ -37,6 +44,23 @@ class ChatListScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final chats = ref.watch(chatroomsJoinedProvider);
final fabKey = useMemoized(() => GlobalKey<ExpandableFabState>(), []);
Future<void> createDirectMessage() async {
final result = await showCupertinoModalBottomSheet(
context: context,
builder: (context) => AccountPickerSheet(),
);
if (result == null) return;
final client = ref.read(apiClientProvider);
try {
await client.post('/chat/direct', data: {'related_user_id': result.id});
ref.refresh(chatroomsJoinedProvider.future);
} catch (err) {
showErrorAlert(err);
}
}
return AppScaffold(
appBar: AppBar(
title: Text('chat').tr(),
@ -53,12 +77,66 @@ class ChatListScreen extends HookConsumerWidget {
const Gap(8),
],
),
floatingActionButton: FloatingActionButton(
heroTag: Key("chat-page-fab"),
onPressed: () {
context.pushRoute(NewChatRoute());
},
child: const Icon(Symbols.add),
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
key: fabKey,
distance: 75,
type: ExpandableFabType.up,
childrenAnimation: ExpandableFabAnimation.none,
overlayStyle: ExpandableFabOverlayStyle(
color: Theme.of(
context,
).colorScheme.surface.withAlpha((255 * 0.5).round()),
),
openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Symbols.add, size: 28),
fabSize: ExpandableFabSize.regular,
foregroundColor:
Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
),
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
heroTag: Key("chat-page-fab"),
child: const Icon(Symbols.close, size: 28),
fabSize: ExpandableFabSize.regular,
foregroundColor:
Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
),
children: [
Row(
children: [
Text('createChatRoom').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'createChatRoom'.tr(),
onPressed: () {
context.pushRoute(NewChatRoute()).then((value) {
if (value != null) {
ref.refresh(chatroomsJoinedProvider.future);
}
});
},
child: const Icon(Symbols.chat_add_on),
),
],
),
Row(
children: [
Text('createDirectMessage').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'createDirectMessage'.tr(),
onPressed: createDirectMessage,
child: const Icon(Symbols.communication),
),
],
),
],
),
body: chats.when(
data:
@ -72,6 +150,18 @@ class ChatListScreen extends HookConsumerWidget {
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item.type == 1) {
return ListTile(
leading: ProfilePictureWidget(
fileId: item.members!.first.account.profile.pictureId,
),
title: Text(item.members!.first.account.nick),
subtitle: Text("An direct message"),
onTap: () {
context.pushRoute(ChatRoomRoute(id: item.id));
},
);
}
return ListTile(
leading:
item.pictureId == null
@ -89,18 +179,24 @@ class ChatListScreen extends HookConsumerWidget {
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
error:
(error, stack) => GestureDetector(
child: Center(child: Text('Error: $error')),
onTap: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
);
}
}
@riverpod
Future<SnChat?> chatroom(Ref ref, int? identifier) async {
Future<SnChatRoom?> 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);
return SnChatRoom.fromJson(resp.data);
}
@riverpod
@ -208,7 +304,7 @@ class EditChatScreen extends HookConsumerWidget {
options: Options(method: id == null ? 'POST' : 'PATCH'),
);
if (context.mounted) {
context.maybePop(SnChat.fromJson(resp.data));
context.maybePop(SnChatRoom.fromJson(resp.data));
}
} catch (err) {
showErrorAlert(err);

View File

@ -6,12 +6,12 @@ part of 'chat.dart';
// RiverpodGenerator
// **************************************************************************
String _$chatroomsJoinedHash() => r'3a2db4159663c54dfd7bc40519e2faa6df69b41f';
String _$chatroomsJoinedHash() => r'0c93fd3cb8fe5c87626836ced4f244bfa7598582';
/// See also [chatroomsJoined].
@ProviderFor(chatroomsJoined)
final chatroomsJoinedProvider =
AutoDisposeFutureProvider<List<SnChat>>.internal(
AutoDisposeFutureProvider<List<SnChatRoom>>.internal(
chatroomsJoined,
name: r'chatroomsJoinedProvider',
debugGetCreateSourceHash:
@ -24,8 +24,8 @@ final chatroomsJoinedProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChat>>;
String _$chatroomHash() => r'27bd4cb49326bb2f2eac7d7db9db7f610e21afb2';
typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>;
String _$chatroomHash() => r'3a945a61ea434f860fbeae9d40778fbfceddc5db';
/// Copied from Dart SDK
class _SystemHash {
@ -53,7 +53,7 @@ class _SystemHash {
const chatroomProvider = ChatroomFamily();
/// See also [chatroom].
class ChatroomFamily extends Family<AsyncValue<SnChat?>> {
class ChatroomFamily extends Family<AsyncValue<SnChatRoom?>> {
/// See also [chatroom].
const ChatroomFamily();
@ -83,7 +83,7 @@ class ChatroomFamily extends Family<AsyncValue<SnChat?>> {
}
/// See also [chatroom].
class ChatroomProvider extends AutoDisposeFutureProvider<SnChat?> {
class ChatroomProvider extends AutoDisposeFutureProvider<SnChatRoom?> {
/// See also [chatroom].
ChatroomProvider(int? identifier)
: this._internal(
@ -113,7 +113,7 @@ class ChatroomProvider extends AutoDisposeFutureProvider<SnChat?> {
@override
Override overrideWith(
FutureOr<SnChat?> Function(ChatroomRef provider) create,
FutureOr<SnChatRoom?> Function(ChatroomRef provider) create,
) {
return ProviderOverride(
origin: this,
@ -130,7 +130,7 @@ class ChatroomProvider extends AutoDisposeFutureProvider<SnChat?> {
}
@override
AutoDisposeFutureProviderElement<SnChat?> createElement() {
AutoDisposeFutureProviderElement<SnChatRoom?> createElement() {
return _ChatroomProviderElement(this);
}
@ -150,12 +150,13 @@ class ChatroomProvider extends AutoDisposeFutureProvider<SnChat?> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ChatroomRef on AutoDisposeFutureProviderRef<SnChat?> {
mixin ChatroomRef on AutoDisposeFutureProviderRef<SnChatRoom?> {
/// The parameter `identifier` of this provider.
int? get identifier;
}
class _ChatroomProviderElement extends AutoDisposeFutureProviderElement<SnChat?>
class _ChatroomProviderElement
extends AutoDisposeFutureProviderElement<SnChatRoom?>
with ChatroomRef {
_ChatroomProviderElement(super.provider);

View File

@ -443,19 +443,28 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26,
width: 26,
child:
room?.pictureId != null
room!.type == 1
? ProfilePictureWidget(
fileId: room?.pictureId,
fileId:
room.members!.first.account.profile.pictureId,
)
: room.pictureId != null
? ProfilePictureWidget(
fileId: room.pictureId,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room?.name[0].toUpperCase() ?? '',
room.name[0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(room?.name ?? 'unknown'.tr()).fontSize(19),
Text(
room!.type == 1
? room.members!.first.account.nick
: room.name,
).fontSize(19),
],
),
loading: () => const Text('Loading...'),
@ -613,7 +622,7 @@ class ChatRoomScreen extends HookConsumerWidget {
class _ChatInput extends StatelessWidget {
final TextEditingController messageController;
final SnChat chatRoom;
final SnChatRoom chatRoom;
final VoidCallback onSend;
final VoidCallback onClear;
final Function(bool isPhoto) onPickFile;
@ -744,7 +753,12 @@ class _ChatInput extends StatelessWidget {
child: TextField(
controller: messageController,
decoration: InputDecoration(
hintText: 'chatMessageHint'.tr(args: [chatRoom.name]),
hintText:
chatRoom.type == 1
? 'chatDirectMessageHint'.tr(
args: [chatRoom.members!.first.account.nick],
)
: 'chatMessageHint'.tr(args: [chatRoom.name]),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(

View File

@ -55,9 +55,26 @@ class ChatDetailScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
currentRoom?.backgroundId != null
currentRoom!.type == 1 &&
currentRoom
.members!
.first
.account
.profile
.backgroundId !=
null
? CloudImageWidget(
fileId: currentRoom!.backgroundId!,
fileId:
currentRoom
.members!
.first
.account
.profile
.backgroundId!,
)
: currentRoom.backgroundId != null
? CloudImageWidget(
fileId: currentRoom.backgroundId!,
fit: BoxFit.cover,
)
: Container(
@ -65,7 +82,9 @@ class ChatDetailScreen extends HookConsumerWidget {
Theme.of(context).appBarTheme.backgroundColor,
),
title: Text(
currentRoom?.name ?? 'unknown'.tr(),
currentRoom.type == 1
? currentRoom.members!.first.account.nick
: currentRoom.name,
).textColor(Theme.of(context).appBarTheme.foregroundColor),
),
actions: [