loading state and error handling refactor
This commit is contained in:
@@ -14,6 +14,7 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../extensions/text.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/loading_spinner.dart';
|
||||
import '../../files/widgets/file_element.dart';
|
||||
import '../data/chat_bubble_styles.dart';
|
||||
@@ -306,22 +307,22 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
|
||||
onPressed: () {
|
||||
if(hasSelfReacted) {
|
||||
// Delete existing reaction
|
||||
DeleteReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: DeleteReactMessageParams(e.key),
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
|
||||
} else {
|
||||
// Add reaction
|
||||
ReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: ReactMessageParams(e.key)
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
}
|
||||
runWithErrorDialog(context, () async {
|
||||
if (hasSelfReacted) {
|
||||
await DeleteReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: DeleteReactMessageParams(e.key),
|
||||
).run();
|
||||
} else {
|
||||
await ReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: ReactMessageParams(e.key),
|
||||
).run();
|
||||
}
|
||||
widget.refetch(renew: true);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
|
||||
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
|
||||
@@ -78,14 +80,12 @@ Future<void> showChatMessageOptionsDialog(
|
||||
onTap: () => Navigator.of(dialogCtx).pop(),
|
||||
),
|
||||
if (canDelete)
|
||||
ListTile(
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onTap: () async {
|
||||
onPressed: () async {
|
||||
await DeleteMessage(chatData.token, bubbleData.id).run();
|
||||
if (!dialogCtx.mounted) return;
|
||||
dialogCtx.read<ChatBloc>().refresh();
|
||||
Navigator.of(dialogCtx).pop();
|
||||
if (dialogCtx.mounted) dialogCtx.read<ChatBloc>().refresh();
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
|
||||
@@ -94,7 +94,7 @@ Future<void> showChatMessageOptionsDialog(
|
||||
);
|
||||
}
|
||||
|
||||
class _ReactionsRow extends StatelessWidget {
|
||||
class _ReactionsRow extends StatefulWidget {
|
||||
final String chatToken;
|
||||
final int messageId;
|
||||
final void Function({bool renew}) onRefetch;
|
||||
@@ -107,46 +107,83 @@ class _ReactionsRow extends StatelessWidget {
|
||||
required this.dialogContext,
|
||||
});
|
||||
|
||||
void _react(String emoji) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
ReactMessage(
|
||||
chatToken: chatToken,
|
||||
messageId: messageId,
|
||||
params: ReactMessageParams(emoji),
|
||||
).run().then((_) => onRefetch(renew: true));
|
||||
@override
|
||||
State<_ReactionsRow> createState() => _ReactionsRowState();
|
||||
}
|
||||
|
||||
class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
final AsyncActionController _controller = AsyncActionController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _react(String emoji) async {
|
||||
final ok = await _controller.run(() async {
|
||||
await ReactMessage(
|
||||
chatToken: widget.chatToken,
|
||||
messageId: widget.messageId,
|
||||
params: ReactMessageParams(emoji),
|
||||
).run();
|
||||
});
|
||||
if (!mounted) return;
|
||||
if (ok) {
|
||||
widget.onRefetch(renew: true);
|
||||
if (widget.dialogContext.mounted) Navigator.of(widget.dialogContext).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
Widget build(BuildContext context) => AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final busy = _controller.busy;
|
||||
final err = _controller.error;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
..._commonReactions.map(
|
||||
(emoji) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
..._commonReactions.map(
|
||||
(emoji) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
onPressed: busy ? null : () => _react(emoji),
|
||||
child: Text(emoji),
|
||||
),
|
||||
),
|
||||
onPressed: () => _react(emoji),
|
||||
child: Text(emoji),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: busy ? null : () => _showEmojiPicker(context),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
icon: busy
|
||||
? const AppProgressIndicator.small()
|
||||
: const Icon(Icons.add_circle_outline_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showEmojiPicker(context),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
if (err != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
err,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add_circle_outline_outlined),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _showEmojiPicker(BuildContext rowContext) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/file_pick.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import '../../files/files_upload_dialog.dart';
|
||||
@@ -30,7 +31,8 @@ class ChatTextfield extends StatefulWidget {
|
||||
class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
late SettingsCubit settings;
|
||||
final TextEditingController _textBoxController = TextEditingController();
|
||||
bool isLoading = false;
|
||||
final AsyncActionController _sendController = AsyncActionController();
|
||||
String? _sendError;
|
||||
|
||||
void share(String shareFolder, List<String> filePaths) {
|
||||
for (final element in filePaths) {
|
||||
@@ -92,6 +94,29 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sendController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _sendMessage(ChatBloc chatBloc) async {
|
||||
if (_textBoxController.text.isEmpty) return;
|
||||
final text = _textBoxController.text;
|
||||
final replyTo = chatBloc.state.data?.referenceMessageId?.toString();
|
||||
setState(() => _sendError = null);
|
||||
await SendMessage(
|
||||
widget.sendToToken,
|
||||
SendMessageParams(text, replyTo: replyTo),
|
||||
).run();
|
||||
if (!mounted) return;
|
||||
chatBloc.refresh();
|
||||
_textBoxController.text = '';
|
||||
_setDraft('');
|
||||
chatBloc.setReferenceMessageId(null);
|
||||
_setDraftReply(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
|
||||
@@ -135,6 +160,14 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
child: Column(
|
||||
children: [
|
||||
replyBanner,
|
||||
if (_sendError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
_sendError!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Row(children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
@@ -200,36 +233,19 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
FloatingActionButton(
|
||||
mini: true,
|
||||
onPressed: () {
|
||||
if (_textBoxController.text.isEmpty || isLoading) return;
|
||||
|
||||
setState(() => isLoading = true);
|
||||
SendMessage(
|
||||
widget.sendToToken,
|
||||
SendMessageParams(
|
||||
_textBoxController.text,
|
||||
replyTo: chatBloc.state.data?.referenceMessageId?.toString(),
|
||||
),
|
||||
).run().then((_) {
|
||||
if (!mounted) return;
|
||||
chatBloc.refresh();
|
||||
setState(() => isLoading = false);
|
||||
_textBoxController.text = '';
|
||||
_setDraft('');
|
||||
chatBloc.setReferenceMessageId(null);
|
||||
_setDraftReply(null);
|
||||
});
|
||||
},
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
elevation: 5,
|
||||
child: isLoading
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.send, color: Colors.white, size: 18),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textBoxController,
|
||||
builder: (context, value, _) => AsyncFab(
|
||||
mini: true,
|
||||
heroTag: 'chatSend_${widget.sendToToken}',
|
||||
icon: Icons.send,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
controller: _sendController,
|
||||
onPressed: value.text.trim().isEmpty ? null : () => _sendMessage(chatBloc),
|
||||
onError: (message) => setState(() => _sendError = message),
|
||||
onSuccess: () => setState(() => _sendError = null),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -11,6 +12,7 @@ import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dar
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
@@ -42,15 +44,14 @@ class _ChatTileState extends State<ChatTile> {
|
||||
|
||||
void _refreshList() => context.read<ChatListBloc>().refresh();
|
||||
|
||||
void setCurrentAsRead() {
|
||||
SetReadMarker(
|
||||
Future<void> _setCurrentAsRead() async {
|
||||
await SetReadMarker(
|
||||
widget.data.token,
|
||||
true,
|
||||
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
|
||||
).run().then((_) {
|
||||
if (!mounted) return;
|
||||
_refreshList();
|
||||
});
|
||||
).run();
|
||||
if (!mounted) return;
|
||||
_refreshList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -116,7 +117,7 @@ class _ChatTileState extends State<ChatTile> {
|
||||
),
|
||||
onTap: () {
|
||||
if (selfUsername == null) return;
|
||||
setCurrentAsRead();
|
||||
unawaited(_setCurrentAsRead());
|
||||
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
|
||||
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
|
||||
context.read<ChatBloc>().setToken(widget.data.token);
|
||||
@@ -125,65 +126,53 @@ class _ChatTileState extends State<ChatTile> {
|
||||
if (widget.disableContextActions) return;
|
||||
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: widget.data.unreadMessages > 0,
|
||||
replacement: ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: const Text('Als ungelesen markieren'),
|
||||
onTap: () {
|
||||
SetReadMarker(widget.data.token, false).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
child: ListTile(
|
||||
if (widget.data.unreadMessages > 0)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.mark_chat_read_outlined),
|
||||
title: const Text('Als gelesen markieren'),
|
||||
onTap: () {
|
||||
setCurrentAsRead();
|
||||
Navigator.of(dialogCtx).pop();
|
||||
onPressed: _setCurrentAsRead,
|
||||
)
|
||||
else
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: const Text('Als ungelesen markieren'),
|
||||
onPressed: () async {
|
||||
await SetReadMarker(widget.data.token, false).run();
|
||||
if (mounted) _refreshList();
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.data.isFavorite,
|
||||
replacement: ListTile(
|
||||
leading: const Icon(Icons.star_outline),
|
||||
title: const Text('Zu Favoriten hinzufügen'),
|
||||
onTap: () {
|
||||
SetFavorite(widget.data.token, true).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
child: ListTile(
|
||||
if (widget.data.isFavorite)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.stars_outlined),
|
||||
title: const Text('Von Favoriten entfernen'),
|
||||
onTap: () {
|
||||
SetFavorite(widget.data.token, false).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
onPressed: () async {
|
||||
await SetFavorite(widget.data.token, false).run();
|
||||
if (mounted) _refreshList();
|
||||
},
|
||||
)
|
||||
else
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.star_outline),
|
||||
title: const Text('Zu Favoriten hinzufügen'),
|
||||
onPressed: () async {
|
||||
await SetFavorite(widget.data.token, true).run();
|
||||
if (mounted) _refreshList();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Konversation verlassen'),
|
||||
onTap: () {
|
||||
Navigator.of(dialogCtx).pop();
|
||||
ConfirmDialog(
|
||||
title: 'Chat verlassen',
|
||||
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
confirmButton: 'Löschen',
|
||||
onConfirm: () {
|
||||
LeaveRoom(widget.data.token).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
confirmButton: 'Verlassen',
|
||||
onConfirmAsync: () async {
|
||||
await LeaveRoom(widget.data.token).run();
|
||||
if (mounted) _refreshList();
|
||||
},
|
||||
).asDialog(dialogCtx);
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
|
||||
|
||||
Reference in New Issue
Block a user