added native features like homescreen-widgets and share intents #97
@@ -135,6 +135,18 @@ class AppRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void openForwardMessageToChat(
|
||||||
|
BuildContext context, {
|
||||||
|
String? text,
|
||||||
|
RemoteFileRef? file,
|
||||||
|
}) {
|
||||||
|
pushScreen(
|
||||||
|
context,
|
||||||
|
withNavBar: false,
|
||||||
|
screen: ShareChatPicker.forMessageForward(text: text, file: file),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static void openInternalSaveToFolder(
|
static void openInternalSaveToFolder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
RemoteFileRef file,
|
RemoteFileRef file,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
|||||||
|
|
||||||
import '../../../api/errors/error_mapper.dart';
|
import '../../../api/errors/error_mapper.dart';
|
||||||
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
|
import '../../../api/marianumcloud/talk/send_message/send_message.dart';
|
||||||
|
import '../../../api/marianumcloud/talk/send_message/send_message_params.dart';
|
||||||
import '../../../api/marianumcloud/talk/share_files_to_chat.dart';
|
import '../../../api/marianumcloud/talk/share_files_to_chat.dart';
|
||||||
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||||
import '../../../routing/app_routes.dart';
|
import '../../../routing/app_routes.dart';
|
||||||
@@ -47,6 +49,23 @@ class ShareChatPicker extends StatelessWidget {
|
|||||||
onPicked: (ctx, room) => _internalShareFlow(ctx, room, file),
|
onPicked: (ctx, room) => _internalShareFlow(ctx, room, file),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Forward an existing Talk message (text and/or already-uploaded file
|
||||||
|
/// attachment) into another chat. The attachment is re-shared via the same
|
||||||
|
/// FileSharingApi path used for [forInternalShare]; plain text is posted
|
||||||
|
/// with [SendMessage].
|
||||||
|
factory ShareChatPicker.forMessageForward({
|
||||||
|
String? text,
|
||||||
|
RemoteFileRef? file,
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
text != null || file != null,
|
||||||
|
'forMessageForward requires either text or file',
|
||||||
|
);
|
||||||
|
return ShareChatPicker._(
|
||||||
|
onPicked: (ctx, room) => _forwardMessageFlow(ctx, room, text, file),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
|
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
|
||||||
@@ -217,6 +236,39 @@ Future<void> _internalShareFlow(
|
|||||||
_finishWithChat(context, room);
|
_finishWithChat(context, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _forwardMessageFlow(
|
||||||
|
BuildContext context,
|
||||||
|
GetRoomResponseObject room,
|
||||||
|
String? text,
|
||||||
|
RemoteFileRef? file,
|
||||||
|
) async {
|
||||||
|
unawaited(_showBlockingSpinner(context));
|
||||||
|
try {
|
||||||
|
if (file != null) {
|
||||||
|
await shareFilesToChat(
|
||||||
|
token: room.token,
|
||||||
|
remoteFilePaths: [file.path],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
await SendMessage(room.token, SendMessageParams(text)).run();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
if (context.mounted) {
|
||||||
|
InfoDialog.show(
|
||||||
|
context,
|
||||||
|
errorToUserMessage(e),
|
||||||
|
title: 'Fehler',
|
||||||
|
copyable: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
_finishWithChat(context, room);
|
||||||
|
}
|
||||||
|
|
||||||
/// Modal progress overlay shown during share-API roundtrips. The dialog is
|
/// Modal progress overlay shown during share-API roundtrips. The dialog is
|
||||||
/// popped together with the picker by the subsequent popUntil(isFirst).
|
/// popped together with the picker by the subsequent popUntil(isFirst).
|
||||||
Future<void> _showBlockingSpinner(BuildContext context) => showDialog<void>(
|
Future<void> _showBlockingSpinner(BuildContext context) => showDialog<void>(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@@ -11,9 +10,11 @@ import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
|||||||
import '../../../../routing/app_routes.dart';
|
import '../../../../routing/app_routes.dart';
|
||||||
import '../../../../share_intent/remote_file_ref.dart';
|
import '../../../../share_intent/remote_file_ref.dart';
|
||||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.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 '../../../../utils/clipboard_helper.dart';
|
||||||
import '../../../../widget/app_progress_indicator.dart';
|
import '../../../../widget/app_progress_indicator.dart';
|
||||||
import '../../../../widget/async_action_button.dart';
|
import '../../../../widget/async_action_button.dart';
|
||||||
|
import '../../../../widget/confirm_dialog.dart';
|
||||||
import '../../../../widget/debug/debug_tile.dart';
|
import '../../../../widget/debug/debug_tile.dart';
|
||||||
import '../../../../widget/details_bottom_sheet.dart';
|
import '../../../../widget/details_bottom_sheet.dart';
|
||||||
|
|
||||||
@@ -104,13 +105,39 @@ void showChatMessageOptionsDialog(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!kReleaseMode &&
|
if (canReact && (bubbleData.message != '{file}' || attachedFile != null))
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.forward_outlined),
|
||||||
|
title: const Text('Weiterleiten'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(sheetCtx).pop();
|
||||||
|
if (!parentContext.mounted) return;
|
||||||
|
AppRoutes.openForwardMessageToChat(
|
||||||
|
parentContext,
|
||||||
|
text: bubbleData.message == '{file}' ? null : bubbleData.message,
|
||||||
|
file: attachedFile != null
|
||||||
|
? RemoteFileRef.fromTalk(attachedFile)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (canReact &&
|
||||||
!isSender &&
|
!isSender &&
|
||||||
chatData.type != GetRoomResponseObjectConversationType.oneToOne)
|
chatData.type != GetRoomResponseObjectConversationType.oneToOne &&
|
||||||
|
bubbleData.actorType ==
|
||||||
|
GetRoomResponseObjectMessageActorType.user)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.sms_outlined),
|
leading: const Icon(Icons.sms_outlined),
|
||||||
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
|
title: Text('Private Nachricht an ${bubbleData.actorDisplayName}'),
|
||||||
onTap: () => Navigator.of(sheetCtx).pop(),
|
onTap: () {
|
||||||
|
Navigator.of(sheetCtx).pop();
|
||||||
|
if (!parentContext.mounted) return;
|
||||||
|
_openOrCreateDirectChat(
|
||||||
|
parentContext,
|
||||||
|
actorId: bubbleData.actorId,
|
||||||
|
actorDisplayName: bubbleData.actorDisplayName,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (canDelete)
|
if (canDelete)
|
||||||
AsyncListTile(
|
AsyncListTile(
|
||||||
@@ -126,6 +153,60 @@ 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 current ChatView before swapping the global ChatBloc token —
|
||||||
|
// otherwise the previous group chat stays mounted in the back-stack and
|
||||||
|
// would render empty after a back-swipe (currentToken no longer matches).
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
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 {
|
class _ReactionsRow extends StatefulWidget {
|
||||||
final String chatToken;
|
final String chatToken;
|
||||||
final int messageId;
|
final int messageId;
|
||||||
|
|||||||
Reference in New Issue
Block a user