refactored direct chat logic into a shared utility, implemented direct message shortcuts in the participant list and message reactions, and added reaction visibility checks in the message options dialog
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
|
||||
/// Opens an existing 1:1 chat with [actorId] or, after confirmation, creates
|
||||
/// one. Shared between the group-message context menu and the reactions
|
||||
/// overview (per-participant chat shortcut).
|
||||
void openOrCreateDirectChat(
|
||||
BuildContext context, {
|
||||
required String actorId,
|
||||
required String actorDisplayName,
|
||||
}) {
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
|
||||
GetRoomResponseObject? findExisting() {
|
||||
final rooms = chatListBloc.state.data?.rooms;
|
||||
if (rooms == null) return null;
|
||||
for (final room in rooms.data) {
|
||||
if (room.type == GetRoomResponseObjectConversationType.oneToOne &&
|
||||
room.name == actorId) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void switchToChat(GetRoomResponseObject room) {
|
||||
// Pop the previous ChatView first — otherwise it stays in the
|
||||
// back-stack with a now-mismatched currentToken and renders empty
|
||||
// on back-swipe. Stop at popups so an open dialog stays alive.
|
||||
Navigator.of(
|
||||
context,
|
||||
).popUntil((route) => route.isFirst || route is PopupRoute);
|
||||
AppRoutes.openChatByToken(context, room.token);
|
||||
}
|
||||
|
||||
final existing = findExisting();
|
||||
if (existing != null) {
|
||||
switchToChat(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmDialog(
|
||||
title: 'Privatchat starten?',
|
||||
content:
|
||||
'Es existiert noch kein Privatchat mit $actorDisplayName. '
|
||||
'Soll einer erstellt werden?',
|
||||
confirmButton: 'Erstellen',
|
||||
onConfirmAsync: () async {
|
||||
await chatListBloc.createDirectChat(actorId);
|
||||
final created = findExisting();
|
||||
if (created == null) {
|
||||
throw Exception(
|
||||
'Privatchat konnte nach dem Erstellen nicht gefunden werden.',
|
||||
);
|
||||
}
|
||||
if (context.mounted) {
|
||||
switchToChat(created);
|
||||
}
|
||||
},
|
||||
).asDialog(context);
|
||||
}
|
||||
@@ -77,7 +77,10 @@ class _ChatInfoState extends State<ChatInfo> {
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => TalkNavigator.pushSplitView(
|
||||
context,
|
||||
ParticipantsListView(participants!),
|
||||
ParticipantsListView(
|
||||
participants!,
|
||||
showDirectMessageAction: isGroup,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/get_reactions/get_reactions.dart';
|
||||
@@ -7,8 +6,8 @@ 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';
|
||||
import '../data/open_direct_chat.dart';
|
||||
|
||||
class MessageReactions extends StatefulWidget {
|
||||
final String token;
|
||||
@@ -63,25 +62,28 @@ class _MessageReactionsState extends State<MessageReactions> {
|
||||
leading: CenteredLeading(Text(entry.key)),
|
||||
title: Text('${entry.value.length} mal reagiert'),
|
||||
children: entry.value.map((e) {
|
||||
var isSelf = AccountData().getUsername() == e.actorId;
|
||||
final isSelf = AccountData().getUsername() == e.actorId;
|
||||
final isGuest =
|
||||
e.actorType ==
|
||||
GetReactionsResponseObjectActorType.guests;
|
||||
return ListTile(
|
||||
leading: UserAvatar(id: e.actorId, isGroup: false),
|
||||
title: Text(e.actorDisplayName),
|
||||
subtitle: isSelf
|
||||
? const Text('Du')
|
||||
: e.actorType ==
|
||||
GetReactionsResponseObjectActorType.guests
|
||||
: isGuest
|
||||
? const Text('Gast')
|
||||
: null,
|
||||
trailing: isSelf
|
||||
trailing: isSelf || isGuest
|
||||
? null
|
||||
: Visibility(
|
||||
visible: kReleaseMode,
|
||||
child: IconButton(
|
||||
onPressed: () =>
|
||||
UnimplementedDialog.show(context),
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
: IconButton(
|
||||
tooltip: 'Private Nachricht',
|
||||
onPressed: () => openOrCreateDirectChat(
|
||||
context,
|
||||
actorId: e.actorId,
|
||||
actorDisplayName: e.actorDisplayName,
|
||||
),
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -2,11 +2,23 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
import '../data/open_direct_chat.dart';
|
||||
|
||||
class ParticipantsListView extends StatelessWidget {
|
||||
final GetParticipantsResponse participantsResponse;
|
||||
const ParticipantsListView(this.participantsResponse, {super.key});
|
||||
|
||||
/// Hide the per-participant direct-message shortcut: in a 1:1 chat the
|
||||
/// only other participant *is* the current chat — the shortcut would
|
||||
/// just navigate back to where the user already is.
|
||||
final bool showDirectMessageAction;
|
||||
|
||||
const ParticipantsListView(
|
||||
this.participantsResponse, {
|
||||
this.showDirectMessageAction = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -24,6 +36,7 @@ class ParticipantsListView extends StatelessWidget {
|
||||
(participant) => participant.participantType,
|
||||
);
|
||||
|
||||
final selfId = AccountData().getUsername();
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Mitglieder')),
|
||||
body: ListView(
|
||||
@@ -35,15 +48,30 @@ class ParticipantsListView extends StatelessWidget {
|
||||
title: Text(entry.key.prettyName),
|
||||
titleTextStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
...entry.value.map(
|
||||
(participant) => ListTile(
|
||||
...entry.value.map((participant) {
|
||||
final canDirectMessage =
|
||||
showDirectMessageAction &&
|
||||
participant.actorType == 'users' &&
|
||||
participant.actorId != selfId;
|
||||
return ListTile(
|
||||
leading: UserAvatar(id: participant.actorId),
|
||||
title: Text(participant.displayName),
|
||||
subtitle: participant.statusMessage != null
|
||||
? Text(participant.statusMessage!)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
trailing: canDirectMessage
|
||||
? IconButton(
|
||||
tooltip: 'Private Nachricht',
|
||||
onPressed: () => openOrCreateDirectChat(
|
||||
context,
|
||||
actorId: participant.actorId,
|
||||
actorDisplayName: participant.displayName,
|
||||
),
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
Divider(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,13 +10,13 @@ import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../share_intent/remote_file_ref.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../utils/clipboard_helper.dart';
|
||||
import '../../../../widget/app_progress_indicator.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 '../data/open_direct_chat.dart';
|
||||
|
||||
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
|
||||
|
||||
@@ -40,6 +40,7 @@ void showChatMessageOptionsDialog(
|
||||
final parentContext = context;
|
||||
final canReact =
|
||||
bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
|
||||
final hasReactions = bubbleData.reactions?.isNotEmpty ?? false;
|
||||
final canDelete =
|
||||
isSender &&
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
@@ -66,7 +67,7 @@ void showChatMessageOptionsDialog(
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
if (canReact)
|
||||
if (canReact && hasReactions)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||
title: const Text('Reaktionen'),
|
||||
@@ -129,7 +130,7 @@ void showChatMessageOptionsDialog(
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
_openOrCreateDirectChat(
|
||||
openOrCreateDirectChat(
|
||||
parentContext,
|
||||
actorId: bubbleData.actorId,
|
||||
actorDisplayName: bubbleData.actorDisplayName,
|
||||
@@ -160,62 +161,6 @@ void showChatMessageOptionsDialog(
|
||||
);
|
||||
}
|
||||
|
||||
void _openOrCreateDirectChat(
|
||||
BuildContext context, {
|
||||
required String actorId,
|
||||
required String actorDisplayName,
|
||||
}) {
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
|
||||
GetRoomResponseObject? findExisting() {
|
||||
final rooms = chatListBloc.state.data?.rooms;
|
||||
if (rooms == null) return null;
|
||||
for (final room in rooms.data) {
|
||||
if (room.type == GetRoomResponseObjectConversationType.oneToOne &&
|
||||
room.name == actorId) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void switchToChat(GetRoomResponseObject room) {
|
||||
// Pop the previous ChatView first — otherwise it stays in the
|
||||
// back-stack with a now-mismatched currentToken and renders empty
|
||||
// on back-swipe. Stop at popups so an open dialog stays alive.
|
||||
Navigator.of(
|
||||
context,
|
||||
).popUntil((route) => route.isFirst || route is PopupRoute);
|
||||
AppRoutes.openChatByToken(context, room.token);
|
||||
}
|
||||
|
||||
final existing = findExisting();
|
||||
if (existing != null) {
|
||||
switchToChat(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmDialog(
|
||||
title: 'Privatchat starten?',
|
||||
content:
|
||||
'Es existiert noch kein Privatchat mit $actorDisplayName. '
|
||||
'Soll einer erstellt werden?',
|
||||
confirmButton: 'Erstellen',
|
||||
onConfirmAsync: () async {
|
||||
await chatListBloc.createDirectChat(actorId);
|
||||
final created = findExisting();
|
||||
if (created == null) {
|
||||
throw Exception(
|
||||
'Privatchat konnte nach dem Erstellen nicht gefunden werden.',
|
||||
);
|
||||
}
|
||||
if (context.mounted) {
|
||||
switchToChat(created);
|
||||
}
|
||||
},
|
||||
).asDialog(context);
|
||||
}
|
||||
|
||||
class _ReactionsRow extends StatefulWidget {
|
||||
final String chatToken;
|
||||
final int messageId;
|
||||
|
||||
Reference in New Issue
Block a user