added native features like homescreen-widgets and share intents #97

Merged
MineTec merged 5 commits from develop-native into develop 2026-05-09 19:35:32 +00:00
3 changed files with 150 additions and 5 deletions
Showing only changes of commit b422430994 - Show all commits
+12
View File
@@ -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;