Solian/lib/screens/chat/chat.dart

270 lines
8.1 KiB
Dart
Raw Normal View History

2024-04-17 15:00:53 +00:00
import 'dart:convert';
import 'package:flutter/material.dart';
2024-04-26 17:36:54 +00:00
import 'package:flutter_animate/flutter_animate.dart';
2024-04-17 15:00:53 +00:00
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:solian/models/message.dart';
import 'package:solian/providers/auth.dart';
2024-05-01 16:49:38 +00:00
import 'package:solian/providers/chat.dart';
2024-04-26 17:36:54 +00:00
import 'package:solian/router.dart';
2024-04-17 15:00:53 +00:00
import 'package:solian/utils/service_url.dart';
import 'package:solian/utils/theme.dart';
2024-05-01 17:38:45 +00:00
import 'package:solian/widgets/chat/channel_action.dart';
2024-04-17 15:00:53 +00:00
import 'package:solian/widgets/chat/message.dart';
import 'package:solian/widgets/chat/message_action.dart';
2024-04-17 15:24:09 +00:00
import 'package:solian/widgets/chat/message_editor.dart';
2024-05-06 15:30:30 +00:00
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/scaffold.dart';
2024-04-26 17:36:54 +00:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2024-04-17 15:00:53 +00:00
2024-05-01 16:49:38 +00:00
class ChatScreen extends StatelessWidget {
2024-04-17 15:00:53 +00:00
final String alias;
2024-05-05 15:01:08 +00:00
final String realm;
2024-04-17 15:00:53 +00:00
2024-05-05 15:01:08 +00:00
const ChatScreen({super.key, required this.alias, this.realm = 'global'});
2024-04-17 15:00:53 +00:00
@override
2024-05-01 16:49:38 +00:00
Widget build(BuildContext context) {
2024-05-06 15:30:30 +00:00
final auth = context.read<AuthProvider>();
2024-05-01 17:38:45 +00:00
final chat = context.watch<ChatProvider>();
return IndentScaffold(
2024-05-01 17:38:45 +00:00
title: chat.focusChannel?.name ?? 'Loading...',
2024-05-01 16:49:38 +00:00
hideDrawer: true,
2024-05-08 14:01:06 +00:00
showSafeArea: true,
fixedAppBarColor: SolianTheme.isLargeScreen(context),
2024-05-01 17:38:45 +00:00
appBarActions: chat.focusChannel != null
? [
ChannelCallAction(
call: chat.ongoingCall,
channel: chat.focusChannel!,
2024-05-05 15:01:08 +00:00
realm: realm,
onUpdate: () => chat.fetchChannel(context, auth, chat.focusChannel!.alias, realm),
2024-05-01 17:38:45 +00:00
),
ChannelManageAction(
channel: chat.focusChannel!,
2024-05-05 15:01:08 +00:00
realm: realm,
onUpdate: () => chat.fetchChannel(context, auth, chat.focusChannel!.alias, realm),
2024-05-01 17:38:45 +00:00
),
]
: [],
2024-05-08 14:01:06 +00:00
body: ChatWidget(
2024-05-01 16:49:38 +00:00
alias: alias,
2024-05-05 15:01:08 +00:00
realm: realm,
2024-05-01 16:49:38 +00:00
),
);
}
2024-04-17 15:00:53 +00:00
}
2024-05-05 15:01:08 +00:00
class ChatWidget extends StatefulWidget {
2024-05-01 16:49:38 +00:00
final String alias;
2024-05-05 15:01:08 +00:00
final String realm;
2024-04-17 15:00:53 +00:00
2024-05-05 15:01:08 +00:00
const ChatWidget({super.key, required this.alias, required this.realm});
2024-04-17 15:00:53 +00:00
2024-05-01 16:49:38 +00:00
@override
2024-05-05 15:01:08 +00:00
State<ChatWidget> createState() => _ChatWidgetState();
2024-05-01 16:49:38 +00:00
}
2024-04-17 15:00:53 +00:00
2024-05-05 15:01:08 +00:00
class _ChatWidgetState extends State<ChatWidget> {
2024-05-01 16:49:38 +00:00
bool _isReady = false;
2024-04-17 15:00:53 +00:00
2024-05-01 16:49:38 +00:00
late final ChatProvider _chat;
2024-04-26 17:36:54 +00:00
2024-05-06 15:30:30 +00:00
Future<void> joinChannel() async {
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return;
var uri = getRequestUri(
'messaging',
'/api/channels/${widget.realm}/${widget.alias}/members/me',
);
var res = await auth.client!.post(uri);
if (res.statusCode == 200) {
setState(() {});
_chat.historyPagingController?.refresh();
2024-05-06 15:30:30 +00:00
} else {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message).then((_) {
SolianRouter.router.pop();
});
}
}
2024-04-17 15:00:53 +00:00
bool getMessageMergeable(Message? a, Message? b) {
2024-05-01 09:37:34 +00:00
if (a?.replyTo != null) return false;
2024-04-17 15:00:53 +00:00
if (a == null || b == null) return false;
if (a.senderId != b.senderId) return false;
2024-05-08 14:01:06 +00:00
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
2024-04-17 15:00:53 +00:00
}
Message? _editingItem;
Message? _replyingItem;
void viewActions(Message item) {
showModalBottomSheet(
context: context,
builder: (context) => ChatMessageAction(
channel: widget.alias,
2024-05-08 14:01:06 +00:00
realm: widget.realm,
item: item,
onEdit: () => setState(() {
_editingItem = item;
}),
onReply: () => setState(() {
_replyingItem = item;
}),
),
);
}
2024-05-06 15:30:30 +00:00
void showUnavailableDialog() {
final content = widget.realm == 'global'
? AppLocalizations.of(context)!.chatChannelUnavailableCaption
: AppLocalizations.of(context)!.chatChannelUnavailableCaptionWithRealm;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.chatChannelUnavailable),
content: Text(content),
actions: <Widget>[
TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop();
SolianRouter.router.pop();
},
),
...(widget.realm != 'global'
? [
TextButton(
child: Text(AppLocalizations.of(context)!.join),
onPressed: () {
Navigator.of(context).pop();
joinChannel();
},
),
]
: [])
],
),
);
}
2024-04-17 15:00:53 +00:00
@override
void initState() {
super.initState();
2024-05-02 13:19:37 +00:00
2024-05-06 15:30:30 +00:00
Future.delayed(Duration.zero, () async {
final auth = context.read<AuthProvider>();
if (!_chat.isOpened) await _chat.connect(auth);
2024-05-05 15:01:08 +00:00
_chat.fetchOngoingCall(widget.alias, widget.realm);
_chat.fetchChannel(context, auth, widget.alias, widget.realm).then((result) {
2024-05-06 15:30:30 +00:00
if (result.isAvailable == false) {
showUnavailableDialog();
}
});
2024-05-02 13:19:37 +00:00
});
2024-04-17 15:00:53 +00:00
}
@override
Widget build(BuildContext context) {
2024-04-26 17:36:54 +00:00
Widget chatHistoryBuilder(context, item, index) {
bool isMerged = false, hasMerged = false;
if (index > 0) {
hasMerged = getMessageMergeable(_chat.historyPagingController?.itemList?[index - 1], item);
2024-04-26 17:36:54 +00:00
}
if (index + 1 < (_chat.historyPagingController?.itemList?.length ?? 0)) {
isMerged = getMessageMergeable(item, _chat.historyPagingController?.itemList?[index + 1]);
2024-04-26 17:36:54 +00:00
}
return InkWell(
child: Container(
padding: EdgeInsets.only(
top: !isMerged ? 8 : 0,
bottom: !hasMerged ? 8 : 0,
left: 12,
right: 12,
),
child: ChatMessage(
item: item,
underMerged: isMerged,
),
),
onLongPress: () => viewActions(item),
2024-05-02 17:55:12 +00:00
).animate(key: Key('m${item.id}'), autoPlay: true).slideY(
curve: Curves.fastEaseInToSlowEaseOut,
duration: 350.ms,
begin: 0.25,
end: 0,
);
2024-04-26 17:36:54 +00:00
}
2024-05-01 16:49:38 +00:00
if (!_isReady) {
_isReady = true;
_chat = context.watch<ChatProvider>();
}
2024-04-26 17:36:54 +00:00
final callBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.call_received),
2024-05-01 11:39:48 +00:00
backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
2024-04-26 17:36:54 +00:00
dividerColor: const Color.fromARGB(1, 0, 0, 0),
content: Text(AppLocalizations.of(context)!.chatCallOngoing),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.chatCallJoin),
onPressed: () {
SolianRouter.router.pushNamed(
2024-04-26 17:36:54 +00:00
'chat.channel.call',
2024-05-01 16:49:38 +00:00
extra: _chat.ongoingCall,
2024-04-26 17:36:54 +00:00
pathParameters: {'channel': widget.alias},
);
},
),
],
);
if (_chat.focusChannel == null || _chat.historyPagingController == null) {
2024-05-02 13:19:37 +00:00
return const Center(child: CircularProgressIndicator());
}
2024-05-01 16:49:38 +00:00
return Stack(
children: [
Column(
children: [
Expanded(
child: PagedListView<int, Message>(
reverse: true,
pagingController: _chat.historyPagingController!,
builderDelegate: PagedChildBuilderDelegate<Message>(
animateTransitions: true,
transitionDuration: 350.ms,
itemBuilder: chatHistoryBuilder,
noItemsFoundIndicatorBuilder: (_) => Container(),
2024-05-02 13:19:37 +00:00
),
),
),
ChatMessageEditor(
realm: widget.realm,
channel: widget.alias,
editing: _editingItem,
replying: _replyingItem,
onReset: () => setState(() {
_editingItem = null;
_replyingItem = null;
}),
),
],
),
_chat.ongoingCall != null ? callBanner.animate().slideY() : Container(),
],
2024-04-17 15:00:53 +00:00
);
}
}