folder restructuring

This commit is contained in:
2026-05-05 21:44:23 +02:00
parent db9c3386f1
commit 4f796dac2e
102 changed files with 1254 additions and 879 deletions
-142
View File
@@ -1,142 +0,0 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notifyUpdater.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirmDialog.dart';
import 'components/chatTile.dart';
import 'components/splitViewPlaceholder.dart';
import 'joinChat.dart';
import 'searchChat.dart';
class ChatList extends StatelessWidget {
const ChatList({super.key});
@override
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
}
class _ChatListView extends StatefulWidget {
const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override
void initState() {
super.initState();
_settings = context.read<SettingsCubit>();
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAskForNotificationPermission());
}
void _maybeAskForNotificationPermission() {
final notificationSettings = _settings.val().notificationSettings;
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
if (!mounted) return;
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(
context: context,
builder: (_) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
),
);
break;
default:
break;
}
});
},
).asDialog(context);
}
@override
Widget build(BuildContext context) {
final bloc = context.read<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
child: Scaffold(
appBar: AppBar(
title: const Text('Talk'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
final rooms = bloc.state.data?.rooms;
if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
},
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) {
if (username == null || !context.mounted) return;
ConfirmDialog(
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
bloc.createDirectChat(username);
},
).asDialog(context);
});
},
child: const Icon(Icons.add_comment_outlined),
),
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
final sorted = rooms.sortBy(
lastActivity: true,
favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: talkSettings.sortUnreadToTop,
);
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return ChatTile(data: room, hasDraft: hasDraft);
}).toList(),
);
},
),
),
);
}
}
+180
View File
@@ -0,0 +1,180 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notify_updater.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirm_dialog.dart';
import 'widgets/chat_tile.dart';
import 'widgets/split_view_placeholder.dart';
import 'join_chat.dart';
import 'search_chat.dart';
class ChatList extends StatelessWidget {
const ChatList({super.key});
@override
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
}
class _ChatListView extends StatefulWidget {
const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override
void initState() {
super.initState();
_settings = context.read<SettingsCubit>();
AppRoutes.pendingChatToken.addListener(_maybeOpenPendingChat);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_maybeAskForNotificationPermission();
_maybeOpenPendingChat();
});
}
@override
void dispose() {
AppRoutes.pendingChatToken.removeListener(_maybeOpenPendingChat);
super.dispose();
}
void _maybeOpenPendingChat() {
if (!mounted) return;
final resolved = AppRoutes.resolvePendingChat(context);
if (resolved == null) return;
AppRoutes.pendingChatToken.value = null;
// Replace any chat already pushed on top of the chat list so a freshly
// tapped notification doesn't stack indefinitely on previous chats.
final navigator = Navigator.of(context);
if (navigator.canPop()) {
navigator.popUntil((route) => route.isFirst);
}
AppRoutes.openChatView(
context,
room: resolved.room,
selfId: resolved.selfId,
avatar: resolved.avatar,
overrideToSingleSubScreen: true,
);
}
void _maybeAskForNotificationPermission() {
final notificationSettings = _settings.val().notificationSettings;
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
if (!mounted) return;
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(
context: context,
builder: (_) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
),
);
break;
default:
break;
}
});
},
).asDialog(context);
}
@override
Widget build(BuildContext context) {
final bloc = context.read<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
child: BlocListener<ChatListBloc, LoadableState<ChatListState>>(
listener: (_, _) => _maybeOpenPendingChat(),
child: Scaffold(
appBar: AppBar(
title: const Text('Talk'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
final rooms = bloc.state.data?.rooms;
if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
},
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) {
if (username == null || !context.mounted) return;
ConfirmDialog(
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
bloc.createDirectChat(username);
},
).asDialog(context);
});
},
child: const Icon(Icons.add_comment_outlined),
),
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
final sorted = rooms.sortBy(
lastActivity: true,
favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: talkSettings.sortUnreadToTop,
);
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return ChatTile(data: room, hasDraft: hasDraft);
}).toList(),
);
},
),
),
),
);
}
}
@@ -3,17 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../extensions/dateTime.dart';
import '../../../extensions/date_time.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/appTheme.dart';
import '../../../widget/clickableAppBar.dart';
import '../../../widget/userAvatar.dart';
import 'chatDetails/chatInfo.dart';
import 'components/chatBubble.dart';
import 'components/chatTextfield.dart';
import 'talkNavigator.dart';
import '../../../theming/app_theme.dart';
import '../../../widget/clickable_app_bar.dart';
import '../../../widget/user_avatar.dart';
import 'details/chat_info.dart';
import 'widgets/chat_bubble.dart';
import 'widgets/chat_textfield.dart';
import 'talk_navigator.dart';
class ChatView extends StatefulWidget {
final GetRoomResponseObject room;
@@ -1,7 +1,7 @@
import 'package:bubble/bubble.dart';
import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart';
import '../../../../theming/app_theme.dart';
extension ColorExtensions on Color {
Color invert() {
@@ -5,9 +5,9 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../model/accountData.dart';
import '../../../../model/endpointData.dart';
import '../../../../utils/UrlOpener.dart';
import '../../../../model/account_data.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../utils/url_opener.dart';
class ChatMessage {
String originalMessage;
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../widget/largeProfilePictureView.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../../../widget/userAvatar.dart';
import '../talkNavigator.dart';
import 'participants/participantsListView.dart';
import '../../../../widget/large_profile_picture_view.dart';
import '../../../../widget/loading_spinner.dart';
import '../../../../widget/user_avatar.dart';
import '../talk_navigator.dart';
import 'participants_list_view.dart';
class ChatInfo extends StatefulWidget {
final GetRoomResponseObject room;
@@ -1,14 +1,14 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../model/accountData.dart';
import '../../../widget/centeredLeading.dart';
import '../../../widget/loadingSpinner.dart';
import '../../../widget/placeholderView.dart';
import '../../../widget/unimplementedDialog.dart';
import '../../../widget/userAvatar.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../../model/account_data.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/loading_spinner.dart';
import '../../../../widget/placeholder_view.dart';
import '../../../../widget/unimplemented_dialog.dart';
import '../../../../widget/user_avatar.dart';
class MessageReactions extends StatefulWidget {
final String token;
@@ -1,8 +1,8 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../../widget/userAvatar.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../widget/user_avatar.dart';
class ParticipantsListView extends StatelessWidget {
final GetParticipantsResponse participantsResponse;
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
import '../../../model/endpointData.dart';
import '../../../widget/placeholderView.dart';
import '../../../model/endpoint_data.dart';
import '../../../widget/placeholder_view.dart';
class JoinChat extends SearchDelegate<String> {
CancelableOperation<AutocompleteResponse>? future;
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import 'components/chatTile.dart';
import 'widgets/chat_tile.dart';
class SearchChat extends SearchDelegate {
List<GetRoomResponseObject> chats;
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import 'chatBubbleStyles.dart';
import '../data/chat_bubble_styles.dart';
class AnswerReference extends StatelessWidget {
final BuildContext context;
@@ -1,31 +1,26 @@
import 'package:bubble/bubble.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flowder/flowder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../extensions/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
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/debug/debugTile.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../files/fileElement.dart';
import 'answerReference.dart';
import 'chatBubbleStyles.dart';
import 'chatMessage.dart';
import '../messageReactions.dart';
import 'pollOptionsList.dart';
import '../../../../widget/loading_spinner.dart';
import '../../files/widgets/file_element.dart';
import '../data/chat_bubble_styles.dart';
import '../data/chat_message.dart';
import 'answer_reference.dart';
import 'chat_message_options_dialog.dart';
import 'poll_options_list.dart';
class ChatBubble extends StatefulWidget {
final BuildContext context;
@@ -77,176 +72,13 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
}
void showOptionsDialog() {
showDialog(context: context, builder: (context) {
var commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
return SimpleDialog(
children: [
Visibility(
visible: canReact,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
...commonReactions.map((e) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40)
),
onPressed: () {
Navigator.of(context).pop();
ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(e),
).run().then((value) => widget.refetch(renew: true));
},
child: Text(e),
),
),
IconButton(
onPressed: () {
showDialog(context: context, builder: (context) => AlertDialog(
contentPadding: const EdgeInsets.all(15),
titlePadding: const EdgeInsets.only(left: 6, top: 15),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 10),
const Text('Reagieren'),
],
),
content: SizedBox(
width: 256,
height: 270,
child: Column(
children: [
emojis.EmojiPicker(
config: emojis.Config(
height: 256,
// swapCategoryAndBottomBar: true, // TODO this property is no longer supported, need to find an replacement
emojiViewConfig: emojis.EmojiViewConfig(
backgroundColor: Theme.of(context).canvasColor,
recentsLimit: 67,
emojiSizeMax: 25,
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
columns: 7,
),
bottomActionBarConfig: const emojis.BottomActionBarConfig(
enabled: false,
),
categoryViewConfig: emojis.CategoryViewConfig(
backgroundColor: Theme.of(context).hoverColor,
iconColorSelected: Theme.of(context).primaryColor,
indicatorColor: Theme.of(context).primaryColor,
),
searchViewConfig: emojis.SearchViewConfig(
backgroundColor: Theme.of(context).dividerColor,
// buttonColor: Theme.of(context).dividerColor, // TODO property no longer supported
hintText: 'Suchen',
buttonIconColor: Colors.white,
),
),
onEmojiSelected: (emojis.Category? category, emojis.Emoji emoji) {
Navigator.of(context).pop();
Navigator.of(context).pop();
ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(emoji.emoji),
).run().then((value) => widget.refetch(renew: true));
},
),
],
),
),
));
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: const Icon(Icons.add_circle_outline_outlined),
),
],
),
const Divider(),
],
),
),
Visibility(
visible: widget.bubbleData.isReplyable,
child: ListTile(
leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'),
onTap: () {
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
Navigator.of(context).pop();
},
),
),
Visibility(
visible: canReact,
child: ListTile(
leading: const Icon(Icons.emoji_emotions_outlined),
title: const Text('Reaktionen'),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
token: widget.chatData.token,
messageId: widget.bubbleData.id,
)));
},
),
),
Visibility(
visible: widget.bubbleData.message != '{file}',
child: ListTile(
leading: const Icon(Icons.copy),
title: const Text('Nachricht kopieren'),
onTap: () => {
Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
Navigator.of(context).pop(),
},
),
),
Visibility(
visible: !kReleaseMode && !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne,
child: ListTile(
leading: const Icon(Icons.sms_outlined),
title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"),
onTap: () => {
Navigator.of(context).pop()
},
),
),
Visibility(
visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
child: ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Nachricht löschen'),
onTap: () {
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
if (!context.mounted) return;
context.read<ChatBloc>().refresh();
Navigator.of(context).pop();
});
},
),
),
DebugTile(context).jsonData(widget.bubbleData.toJson()),
],
);
});
showChatMessageOptionsDialog(
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
}
@@ -0,0 +1,202 @@
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
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/debug/debug_tile.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(
BuildContext context, {
required GetRoomResponseObject chatData,
required GetChatResponseObject bubbleData,
required bool isSender,
required void Function({bool renew}) onRefetch,
}) {
final parentContext = context;
final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
final canDelete = isSender &&
DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000)
.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: () {
Clipboard.setData(ClipboardData(text: 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)
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Nachricht löschen'),
onTap: () async {
await DeleteMessage(chatData.token, bubbleData.id).run();
if (!dialogCtx.mounted) return;
dialogCtx.read<ChatBloc>().refresh();
Navigator.of(dialogCtx).pop();
},
),
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
],
),
);
}
class _ReactionsRow extends StatelessWidget {
final String chatToken;
final int messageId;
final void Function({bool renew}) onRefetch;
final BuildContext dialogContext;
const _ReactionsRow({
required this.chatToken,
required this.messageId,
required this.onRefetch,
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
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: () => _react(emoji),
child: Text(emoji),
),
),
IconButton(
onPressed: () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: const Icon(Icons.add_circle_outline_outlined),
),
],
),
const Divider(),
],
);
void _showEmojiPicker(BuildContext rowContext) {
showDialog(
context: rowContext,
builder: (pickerCtx) => AlertDialog(
contentPadding: const EdgeInsets.all(15),
titlePadding: const EdgeInsets.only(left: 6, top: 15),
title: Row(
children: [
IconButton(
onPressed: () => Navigator.of(pickerCtx).pop(),
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 10),
const Text('Reagieren'),
],
),
content: SizedBox(
width: 256,
height: 270,
child: emojis.EmojiPicker(
config: emojis.Config(
height: 256,
emojiViewConfig: emojis.EmojiViewConfig(
backgroundColor: Theme.of(pickerCtx).canvasColor,
recentsLimit: 67,
emojiSizeMax: 25,
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
columns: 7,
),
bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false),
categoryViewConfig: emojis.CategoryViewConfig(
backgroundColor: Theme.of(pickerCtx).hoverColor,
iconColorSelected: Theme.of(pickerCtx).primaryColor,
indicatorColor: Theme.of(pickerCtx).primaryColor,
),
searchViewConfig: emojis.SearchViewConfig(
backgroundColor: Theme.of(pickerCtx).dividerColor,
hintText: 'Suchen',
buttonIconColor: Colors.white,
),
),
onEmojiSelected: (_, emoji) {
Navigator.of(pickerCtx).pop();
_react(emoji.emoji);
},
),
),
),
);
}
}
@@ -12,10 +12,10 @@ 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/filePick.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../files/filesUploadDialog.dart';
import 'answerReference.dart';
import '../../../../widget/file_pick.dart';
import '../../../../widget/focus_behaviour.dart';
import '../../files/files_upload_dialog.dart';
import 'answer_reference.dart';
class ChatTextfield extends StatefulWidget {
final String sendToToken;
@@ -9,14 +9,14 @@ import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
import '../../../../model/accountData.dart';
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/confirmDialog.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/userAvatar.dart';
import '../chatView.dart';
import '../talkNavigator.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/user_avatar.dart';
import '../chat_view.dart';
import '../talk_navigator.dart';
class ChatTile extends StatefulWidget {
final GetRoomResponseObject data;
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
import '../../../../utils/UrlOpener.dart';
import '../../../../utils/url_opener.dart';
class PollOptionsList extends StatefulWidget {
final GetPollStateResponseObject pollData;
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart';
import '../../../../theming/app_theme.dart';
class SplitViewPlaceholder extends StatelessWidget {
const SplitViewPlaceholder({super.key});