refactored data providers with centralized cache resolution, unified UI using custom dialogs and bottom sheets, and enhanced network error handling for Dio and TLS errors
This commit is contained in:
@@ -40,9 +40,8 @@ class BubbleStyle {
|
||||
final double borderRadius;
|
||||
}
|
||||
|
||||
/// Lightweight chat bubble. Replaces the abandoned `bubble` package: renders a
|
||||
/// rounded container with optional shadow / border. The nip is conveyed by
|
||||
/// flattening one corner so the bubble visually anchors to the speaker side.
|
||||
/// The "nip" is faked by flattening one corner so the bubble anchors to
|
||||
/// the speaker side.
|
||||
class Bubble extends StatelessWidget {
|
||||
const Bubble({required this.child, required this.style, super.key});
|
||||
|
||||
|
||||
@@ -246,9 +246,6 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
}
|
||||
}
|
||||
|
||||
/// Stack inside the bubble: actor name (top-left, optional), message body
|
||||
/// (centre), timestamp + read marker (bottom-right, optional), and a
|
||||
/// download progress bar overlaid at the bottom while a job is running.
|
||||
class _BubbleContent extends StatelessWidget {
|
||||
final Text actorText;
|
||||
final Text timeText;
|
||||
|
||||
@@ -14,13 +14,14 @@ import '../../../../utils/clipboard_helper.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
|
||||
|
||||
/// Long-press / double-tap options dialog for a single chat message bubble.
|
||||
/// The hosting [ChatBubble] keeps responsibility for rendering the bubble;
|
||||
/// this file owns the modal interactions (react, reply, copy, delete, ...).
|
||||
Future<void> showChatMessageOptionsDialog(
|
||||
void showChatMessageOptionsDialog(
|
||||
BuildContext context, {
|
||||
required GetRoomResponseObject chatData,
|
||||
required GetChatResponseObject bubbleData,
|
||||
@@ -34,63 +35,61 @@ Future<void> showChatMessageOptionsDialog(
|
||||
.add(const Duration(hours: 6))
|
||||
.isAfter(DateTime.now());
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (dialogCtx) => SimpleDialog(
|
||||
children: [
|
||||
if (canReact)
|
||||
_ReactionsRow(
|
||||
chatToken: chatData.token,
|
||||
messageId: bubbleData.id,
|
||||
onRefetch: onRefetch,
|
||||
dialogContext: dialogCtx,
|
||||
),
|
||||
if (bubbleData.isReplyable)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply_outlined),
|
||||
title: const Text('Antworten'),
|
||||
onTap: () {
|
||||
dialogCtx.read<ChatBloc>().setReferenceMessageId(bubbleData.id);
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
if (canReact)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||
title: const Text('Reaktionen'),
|
||||
onTap: () {
|
||||
Navigator.of(dialogCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id);
|
||||
},
|
||||
),
|
||||
if (bubbleData.message != '{file}')
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: const Text('Nachricht kopieren'),
|
||||
onTap: () {
|
||||
copyToClipboard(parentContext, bubbleData.message);
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sms_outlined),
|
||||
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
|
||||
onTap: () => Navigator.of(dialogCtx).pop(),
|
||||
),
|
||||
if (canDelete)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onPressed: () async {
|
||||
await DeleteMessage(chatData.token, bubbleData.id).run();
|
||||
if (dialogCtx.mounted) dialogCtx.read<ChatBloc>().refresh();
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
|
||||
],
|
||||
),
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
if (canReact)
|
||||
_ReactionsRow(
|
||||
chatToken: chatData.token,
|
||||
messageId: bubbleData.id,
|
||||
onRefetch: onRefetch,
|
||||
sheetContext: sheetCtx,
|
||||
),
|
||||
if (bubbleData.isReplyable)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply_outlined),
|
||||
title: const Text('Antworten'),
|
||||
onTap: () {
|
||||
sheetCtx.read<ChatBloc>().setReferenceMessageId(bubbleData.id);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
if (canReact)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||
title: const Text('Reaktionen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id);
|
||||
},
|
||||
),
|
||||
if (bubbleData.message != '{file}')
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: const Text('Nachricht kopieren'),
|
||||
onTap: () {
|
||||
copyToClipboard(parentContext, bubbleData.message);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sms_outlined),
|
||||
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
|
||||
onTap: () => Navigator.of(sheetCtx).pop(),
|
||||
),
|
||||
if (canDelete)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onPressed: () async {
|
||||
await DeleteMessage(chatData.token, bubbleData.id).run();
|
||||
if (sheetCtx.mounted) sheetCtx.read<ChatBloc>().refresh();
|
||||
},
|
||||
),
|
||||
DebugTile(sheetCtx).jsonData(bubbleData.toJson()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,13 +97,13 @@ class _ReactionsRow extends StatefulWidget {
|
||||
final String chatToken;
|
||||
final int messageId;
|
||||
final void Function({bool renew}) onRefetch;
|
||||
final BuildContext dialogContext;
|
||||
final BuildContext sheetContext;
|
||||
|
||||
const _ReactionsRow({
|
||||
required this.chatToken,
|
||||
required this.messageId,
|
||||
required this.onRefetch,
|
||||
required this.dialogContext,
|
||||
required this.sheetContext,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -131,7 +130,7 @@ class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
if (!mounted) return;
|
||||
if (ok) {
|
||||
widget.onRefetch(renew: true);
|
||||
if (widget.dialogContext.mounted) Navigator.of(widget.dialogContext).pop();
|
||||
if (widget.sheetContext.mounted) Navigator.of(widget.sheetContext).pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
import '../chat_view.dart';
|
||||
import '../talk_navigator.dart';
|
||||
@@ -124,8 +125,9 @@ class _ChatTileState extends State<ChatTile> {
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.disableContextActions) return;
|
||||
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
|
||||
children: [
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
if (widget.data.unreadMessages > 0)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.mark_chat_read_outlined),
|
||||
@@ -163,7 +165,7 @@ class _ChatTileState extends State<ChatTile> {
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Konversation verlassen'),
|
||||
onTap: () {
|
||||
Navigator.of(dialogCtx).pop();
|
||||
Navigator.of(sheetCtx).pop();
|
||||
ConfirmDialog(
|
||||
title: 'Chat verlassen',
|
||||
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
@@ -175,9 +177,9 @@ class _ChatTileState extends State<ChatTile> {
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
|
||||
DebugTile(sheetCtx).jsonData(widget.data.toJson()),
|
||||
],
|
||||
));
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class _PollOptionsListState extends State<PollOptionsList> {
|
||||
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
|
||||
|
||||
return ListTile(
|
||||
// enabled: false,
|
||||
isThreeLine: portionsVisible,
|
||||
dense: true,
|
||||
title: Text(
|
||||
|
||||
Reference in New Issue
Block a user