2024-11-16 17:16:54 +00:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
2024-11-23 16:22:08 +00:00
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:material_symbols_icons/symbols.dart';
|
2024-11-16 17:16:54 +00:00
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
|
|
import 'package:surface/controllers/chat_message_controller.dart';
|
|
|
|
import 'package:surface/providers/channel.dart';
|
|
|
|
import 'package:surface/types/chat.dart';
|
|
|
|
import 'package:surface/widgets/chat/chat_message.dart';
|
|
|
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
|
|
|
import 'package:surface/widgets/dialog.dart';
|
|
|
|
import 'package:surface/widgets/loading_indicator.dart';
|
|
|
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|
|
|
|
|
|
|
class ChatRoomScreen extends StatefulWidget {
|
|
|
|
final String scope;
|
|
|
|
final String alias;
|
|
|
|
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|
|
|
bool _isBusy = false;
|
|
|
|
|
|
|
|
SnChannel? _channel;
|
|
|
|
|
2024-11-18 14:33:03 +00:00
|
|
|
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
2024-11-16 17:16:54 +00:00
|
|
|
late final ChatMessageController _messageController;
|
|
|
|
|
|
|
|
Future<void> _fetchChannel() async {
|
|
|
|
setState(() => _isBusy = true);
|
|
|
|
|
|
|
|
try {
|
|
|
|
final chan = context.read<ChatChannelProvider>();
|
|
|
|
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
|
|
|
} catch (err) {
|
|
|
|
if (!mounted) return;
|
|
|
|
context.showErrorDialog(err);
|
|
|
|
} finally {
|
|
|
|
setState(() => _isBusy = false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
_messageController = ChatMessageController(context);
|
|
|
|
_fetchChannel().then((_) async {
|
|
|
|
await _messageController.initialize(_channel!);
|
|
|
|
await _messageController.checkUpdate();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
|
|
|
title: Text(_channel?.name ?? 'loading'.tr()),
|
2024-11-23 16:22:08 +00:00
|
|
|
actions: [
|
|
|
|
IconButton(
|
|
|
|
onPressed: () {
|
|
|
|
GoRouter.of(context).pushNamed('chatCallRoom', pathParameters: {
|
|
|
|
'scope': widget.scope,
|
|
|
|
'alias': widget.alias,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
icon: const Icon(Symbols.voice_chat),
|
|
|
|
),
|
|
|
|
IconButton(onPressed: () {}, icon: const Icon(Symbols.more_vert)),
|
|
|
|
],
|
2024-11-16 17:16:54 +00:00
|
|
|
),
|
|
|
|
body: ListenableBuilder(
|
|
|
|
listenable: _messageController,
|
|
|
|
builder: (context, _) {
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
LoadingIndicator(isActive: _isBusy),
|
|
|
|
if (_messageController.isPending)
|
|
|
|
Expanded(
|
|
|
|
child: const CircularProgressIndicator().center(),
|
|
|
|
),
|
|
|
|
if (!_messageController.isPending)
|
|
|
|
Expanded(
|
|
|
|
child: InfiniteList(
|
2024-11-17 13:30:02 +00:00
|
|
|
reverse: true,
|
2024-11-17 14:42:09 +00:00
|
|
|
padding: const EdgeInsets.only(
|
|
|
|
left: 12,
|
|
|
|
right: 12,
|
|
|
|
top: 12,
|
2024-11-17 13:30:02 +00:00
|
|
|
),
|
2024-11-16 17:16:54 +00:00
|
|
|
hasReachedMax: _messageController.isAllLoaded,
|
|
|
|
itemCount: _messageController.messages.length,
|
|
|
|
isLoading: _messageController.isLoading,
|
|
|
|
onFetchData: () {
|
|
|
|
_messageController.loadMessages();
|
|
|
|
},
|
|
|
|
itemBuilder: (context, idx) {
|
|
|
|
final message = _messageController.messages[idx];
|
2024-11-17 14:42:09 +00:00
|
|
|
final nextMessage =
|
|
|
|
idx < _messageController.messages.length - 1
|
|
|
|
? _messageController.messages[idx + 1]
|
|
|
|
: null;
|
|
|
|
final previousMessage =
|
|
|
|
idx > 0 ? _messageController.messages[idx - 1] : null;
|
|
|
|
|
|
|
|
final canMerge = nextMessage != null &&
|
2024-11-19 14:17:17 +00:00
|
|
|
nextMessage.updatedAt == nextMessage.createdAt &&
|
2024-11-17 14:42:09 +00:00
|
|
|
nextMessage.senderId == message.senderId &&
|
|
|
|
message.createdAt
|
|
|
|
.difference(nextMessage.createdAt)
|
|
|
|
.inMinutes
|
|
|
|
.abs() <=
|
|
|
|
3;
|
|
|
|
final canMergePrevious = previousMessage != null &&
|
2024-11-19 14:17:17 +00:00
|
|
|
message.updatedAt == message.createdAt &&
|
2024-11-17 14:42:09 +00:00
|
|
|
previousMessage.senderId == message.senderId &&
|
|
|
|
message.createdAt
|
|
|
|
.difference(previousMessage.createdAt)
|
|
|
|
.inMinutes
|
|
|
|
.abs() <=
|
|
|
|
3;
|
|
|
|
|
2024-11-17 13:30:02 +00:00
|
|
|
return ChatMessage(
|
|
|
|
data: message,
|
2024-11-17 14:42:09 +00:00
|
|
|
isMerged: canMerge,
|
|
|
|
hasMerged: canMergePrevious,
|
2024-11-17 13:30:02 +00:00
|
|
|
isPending: _messageController.unconfirmedMessages
|
|
|
|
.contains(message.uuid),
|
2024-11-18 14:52:22 +00:00
|
|
|
onReply: (value) {
|
|
|
|
_inputGlobalKey.currentState?.setReply(value);
|
2024-11-18 14:33:03 +00:00
|
|
|
},
|
2024-11-18 15:59:08 +00:00
|
|
|
onEdit: (value) {
|
|
|
|
_inputGlobalKey.currentState?.setEdit(value);
|
|
|
|
},
|
|
|
|
onDelete: (value) {
|
|
|
|
_inputGlobalKey.currentState?.deleteMessage(value);
|
|
|
|
},
|
2024-11-17 13:30:02 +00:00
|
|
|
);
|
2024-11-16 17:16:54 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
if (!_messageController.isPending)
|
|
|
|
Material(
|
|
|
|
elevation: 2,
|
2024-11-18 14:33:03 +00:00
|
|
|
child: ChatMessageInput(
|
|
|
|
key: _inputGlobalKey,
|
|
|
|
controller: _messageController,
|
|
|
|
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
2024-11-16 17:16:54 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|