implemented native share intent support for android and ios with chat and folder pickers
This commit is contained in:
@@ -21,7 +21,7 @@ void showAddFileSheet(
|
||||
title: const Text('Ordner erstellen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_showCreateFolderDialog(context, bloc);
|
||||
showCreateFolderDialog(context, bloc);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -56,7 +56,7 @@ void showAddFileSheet(
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateFolderDialog(BuildContext context, FilesBloc bloc) {
|
||||
void showCreateFolderDialog(BuildContext context, FilesBloc bloc) {
|
||||
final inputController = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../api/marianumcloud/talk/share_files_to_chat.dart';
|
||||
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../share_intent/pending_share.dart';
|
||||
import '../../../share_intent/share_intent_listener.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/info_dialog.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import '../files/files_upload_dialog.dart';
|
||||
import '../talk/search_chat.dart';
|
||||
import '../talk/widgets/chat_tile.dart';
|
||||
|
||||
class ShareChatPicker extends StatelessWidget {
|
||||
final PendingShare share;
|
||||
|
||||
const ShareChatPicker({super.key, required this.share});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Chat auswählen'),
|
||||
actions: [
|
||||
Builder(
|
||||
builder: (ctx) => IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
final rooms = ctx.read<ChatListBloc>().state.data?.rooms;
|
||||
if (rooms == null) return;
|
||||
showSearch(
|
||||
context: ctx,
|
||||
delegate: SearchChat(
|
||||
rooms.data.where((r) => r.readOnly == 0).toList(),
|
||||
onTapOverride: (room) {
|
||||
Navigator.of(ctx).pop();
|
||||
_onChatPicked(ctx, room);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
|
||||
child: (state, _) {
|
||||
final rooms = state.rooms;
|
||||
if (rooms == null) return const SizedBox.shrink();
|
||||
final sorted = rooms
|
||||
.sortBy(
|
||||
lastActivity: true,
|
||||
favoritesToTop: talkSettings.sortFavoritesToTop,
|
||||
unreadToTop: talkSettings.sortUnreadToTop,
|
||||
)
|
||||
// Hide chats the user can't write to (announcement channels,
|
||||
// archived rooms, …) — uploading there would only fail at the
|
||||
// share-API call with 403.
|
||||
.where((r) => r.readOnly == 0)
|
||||
.toList();
|
||||
if (sorted.isEmpty) {
|
||||
return const PlaceholderView(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
text: 'Keine schreibbaren Chats verfügbar',
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, i) => ChatTile(
|
||||
data: sorted[i],
|
||||
disableContextActions: true,
|
||||
onTapOverride: (room) => _onChatPicked(context, room),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onChatPicked(
|
||||
BuildContext context,
|
||||
GetRoomResponseObject room,
|
||||
) async {
|
||||
if (share.hasFiles) {
|
||||
try {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.mkcol(PathUri.parse('/$talkShareFolder'));
|
||||
} catch (_) {
|
||||
// mkcol throws when the folder already exists; ignore.
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: share.filePaths,
|
||||
remotePath: talkShareFolder,
|
||||
uniqueNames: true,
|
||||
onUploadFinished: (uploaded) =>
|
||||
_afterFilesUploaded(context, room, uploaded),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (share.hasText) {
|
||||
_setDraftAndOpenChat(context, room);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _afterFilesUploaded(
|
||||
BuildContext context,
|
||||
GetRoomResponseObject room,
|
||||
List<String> uploadedRemotePaths,
|
||||
) async {
|
||||
// Block the picker UI while the share-API roundtrips run, otherwise the
|
||||
// user can re-tap a chat and double-share the same files.
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const PopScope(
|
||||
canPop: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await shareFilesToChat(
|
||||
token: room.token,
|
||||
remoteFilePaths: uploadedRemotePaths,
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
if (context.mounted) {
|
||||
InfoDialog.show(
|
||||
context,
|
||||
'Datei konnte nicht im Chat geteilt werden: $e',
|
||||
title: 'Fehler',
|
||||
copyable: true,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
// The blocking dialog is popped together with the picker by
|
||||
// _setDraftAndOpenChat's popUntil(isFirst) below.
|
||||
_setDraftAndOpenChat(context, room);
|
||||
}
|
||||
|
||||
void _setDraftAndOpenChat(BuildContext context, GetRoomResponseObject room) {
|
||||
if (share.hasText) {
|
||||
final settings = context.read<SettingsCubit>();
|
||||
settings.val(write: true).talkSettings.drafts[room.token] = share.text!;
|
||||
}
|
||||
ShareIntentListener.instance.clear();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
AppRoutes.openChatByToken(context, room.token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../share_intent/pending_share.dart';
|
||||
import '../../../share_intent/share_intent_listener.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_bloc.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import '../files/data/sort_options.dart';
|
||||
import '../files/files_upload_dialog.dart';
|
||||
import '../files/widgets/add_file_menu.dart';
|
||||
import '../files/widgets/files_sort_actions.dart';
|
||||
|
||||
class ShareFolderPicker extends StatelessWidget {
|
||||
final PendingShare share;
|
||||
|
||||
const ShareFolderPicker({super.key, required this.share});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocModule<FilesBloc, LoadableState<FilesState>>(
|
||||
create: (_) => FilesBloc(),
|
||||
child: (context, _, _) => _ShareFolderPickerView(share: share),
|
||||
);
|
||||
}
|
||||
|
||||
class _ShareFolderPickerView extends StatefulWidget {
|
||||
final PendingShare share;
|
||||
const _ShareFolderPickerView({required this.share});
|
||||
|
||||
@override
|
||||
State<_ShareFolderPickerView> createState() => _ShareFolderPickerViewState();
|
||||
}
|
||||
|
||||
class _ShareFolderPickerViewState extends State<_ShareFolderPickerView> {
|
||||
late final SettingsCubit _settings;
|
||||
late SortOption _currentSort;
|
||||
late bool _ascending;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settings = context.read<SettingsCubit>();
|
||||
_currentSort = _settings.val().fileSettings.sortBy;
|
||||
_ascending = _settings.val().fileSettings.ascending;
|
||||
}
|
||||
|
||||
void _enter(FilesBloc bloc, List<String> currentPath, String folderName) {
|
||||
bloc.setPath([...currentPath, folderName]);
|
||||
}
|
||||
|
||||
void _goUp(FilesBloc bloc, List<String> currentPath) {
|
||||
if (currentPath.isEmpty) return;
|
||||
bloc.setPath(currentPath.sublist(0, currentPath.length - 1));
|
||||
}
|
||||
|
||||
Future<void> _uploadHere(List<String> currentPath) async {
|
||||
await pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: widget.share.filePaths,
|
||||
remotePath: currentPath.join('/'),
|
||||
onUploadFinished: (_) => _afterUploaded(currentPath),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _afterUploaded(List<String> targetPath) {
|
||||
ShareIntentListener.instance.clear();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
AppRoutes.openFolder(context, targetPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<FilesBloc>();
|
||||
return BlocBuilder<FilesBloc, LoadableState<FilesState>>(
|
||||
buildWhen: (a, b) => a.data?.currentPath != b.data?.currentPath,
|
||||
builder: (_, outerState) {
|
||||
final currentPath = outerState.data?.currentPath ?? const [];
|
||||
return PopScope(
|
||||
// Back navigates one level up while inside a sub-folder; only the
|
||||
// root level actually closes the picker. Matches the standard
|
||||
// files-app pattern and keeps the AppBar back-arrow consistent
|
||||
// with the chat picker.
|
||||
canPop: currentPath.isEmpty,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
if (currentPath.isNotEmpty) _goUp(bloc, currentPath);
|
||||
},
|
||||
child: _buildScaffold(context, bloc, currentPath),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScaffold(
|
||||
BuildContext context,
|
||||
FilesBloc bloc,
|
||||
List<String> currentPath,
|
||||
) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
currentPath.isEmpty ? 'Ordner wählen' : '/${currentPath.join('/')}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.create_new_folder_outlined),
|
||||
tooltip: 'Ordner erstellen',
|
||||
onPressed: () => showCreateFolderDialog(context, bloc),
|
||||
),
|
||||
FilesSortActions(
|
||||
currentSort: _currentSort,
|
||||
ascending: _ascending,
|
||||
onDirectionChanged: (e) {
|
||||
setState(() {
|
||||
_ascending = e;
|
||||
_settings.val(write: true).fileSettings.ascending = e;
|
||||
});
|
||||
},
|
||||
onSortChanged: (e) {
|
||||
setState(() {
|
||||
_currentSort = e;
|
||||
_settings.val(write: true).fileSettings.sortBy = e;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
heroTag: 'shareUploadHere',
|
||||
onPressed: () => _uploadHere(currentPath),
|
||||
icon: const Icon(Icons.upload),
|
||||
label: const Text('Hier hochladen'),
|
||||
),
|
||||
body: LoadableStateConsumer<FilesBloc, FilesState>(
|
||||
isReady: (state) => state.listing != null,
|
||||
child: (state, _) {
|
||||
final listing = state.listing!;
|
||||
final entries = listing.sortBy(
|
||||
sortOption: _currentSort,
|
||||
foldersToTop: _settings.val().fileSettings.sortFoldersToTop,
|
||||
reversed: _ascending,
|
||||
);
|
||||
|
||||
if (entries.isEmpty) {
|
||||
return PlaceholderView(
|
||||
icon: Icons.folder_off_rounded,
|
||||
text: state.currentPath.isEmpty
|
||||
? 'Leer. Du kannst hier direkt hochladen.'
|
||||
: 'Ordner ist leer. Du kannst hier hochladen.',
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: entries.length,
|
||||
itemBuilder: (context, i) {
|
||||
final entry = entries[i];
|
||||
if (entry.isDirectory) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(entry.name),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _enter(bloc, state.currentPath, entry.name),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: Text(entry.name),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../share_intent/pending_share.dart';
|
||||
import '../../../share_intent/share_intent_listener.dart';
|
||||
|
||||
class ShareTargetPage extends StatelessWidget {
|
||||
final PendingShare share;
|
||||
|
||||
const ShareTargetPage({super.key, required this.share});
|
||||
|
||||
static const _imageExtensions = {
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.heic',
|
||||
'.heif',
|
||||
'.bmp',
|
||||
};
|
||||
|
||||
bool _isImagePath(String path) {
|
||||
final lower = path.toLowerCase();
|
||||
return _imageExtensions.any(lower.endsWith);
|
||||
}
|
||||
|
||||
String _appBarTitle() {
|
||||
if (share.hasFiles && share.hasText) return 'Inhalte teilen';
|
||||
if (share.hasFiles) {
|
||||
return share.filePaths.length == 1
|
||||
? '1 Datei teilen'
|
||||
: '${share.filePaths.length} Dateien teilen';
|
||||
}
|
||||
return 'Inhalt teilen';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) ShareIntentListener.instance.clear();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text(_appBarTitle())),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (share.hasFiles) _buildFilePreview(context),
|
||||
if (share.hasFiles && share.hasText)
|
||||
const SizedBox(height: 12),
|
||||
if (share.hasText) _buildTextPreview(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.ios_share,
|
||||
size: 44,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Wo möchtest du teilen?',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.chat_bubble_outline),
|
||||
title: const Text('An Chat senden'),
|
||||
subtitle: const Text(
|
||||
'Datei oder Text in einem Talk-Chat teilen',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () =>
|
||||
AppRoutes.openShareChatPicker(context, share),
|
||||
),
|
||||
ListTile(
|
||||
enabled: share.hasFiles,
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: const Text('In Dateien speichern'),
|
||||
subtitle: Text(
|
||||
share.hasFiles
|
||||
? 'In einen Nextcloud-Ordner hochladen'
|
||||
: 'Nur für Dateien verfügbar',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: share.hasFiles
|
||||
? () => AppRoutes.openShareFolderPicker(context, share)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildFilePreview(BuildContext context) {
|
||||
if (share.filePaths.length == 1) {
|
||||
final path = share.filePaths.first;
|
||||
final name = path.split(Platform.pathSeparator).last;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 320),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _isImagePath(path)
|
||||
? Image.file(
|
||||
File(path),
|
||||
fit: BoxFit.contain,
|
||||
// Decode at most ~1080px so 50-MP gallery photos don't
|
||||
// balloon the decode buffer just to render at <320px high.
|
||||
cacheWidth: 1080,
|
||||
errorBuilder: (_, _, _) => _fileFallbackLarge(name),
|
||||
)
|
||||
: _fileFallbackLarge(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: share.filePaths.length,
|
||||
itemBuilder: (context, i) {
|
||||
final path = share.filePaths[i];
|
||||
final name = path.split(Platform.pathSeparator).last;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _isImagePath(path)
|
||||
? Image.file(
|
||||
File(path),
|
||||
fit: BoxFit.cover,
|
||||
// Grid tiles are ~half-screen wide; 480px decode is
|
||||
// sharp on 3x displays without blowing up memory when
|
||||
// many files are shared at once.
|
||||
cacheWidth: 480,
|
||||
errorBuilder: (_, _, _) => _fileFallbackLarge(name),
|
||||
)
|
||||
: _fileFallbackLarge(name),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextPreview(BuildContext context) => Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
share.text!,
|
||||
maxLines: 6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _fileFallbackLarge(String name) => Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.insert_drive_file_outlined, size: 64),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
name,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import 'widgets/chat_tile.dart';
|
||||
|
||||
class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
|
||||
List<GetRoomResponseObject> chats;
|
||||
final void Function(GetRoomResponseObject room)? onTapOverride;
|
||||
|
||||
SearchChat(this.chats);
|
||||
SearchChat(this.chats, {this.onTapOverride});
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) => [
|
||||
@@ -34,7 +35,11 @@ class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var item = items.elementAt(index);
|
||||
return ChatTile(data: item, disableContextActions: true);
|
||||
return ChatTile(
|
||||
data: item,
|
||||
disableContextActions: true,
|
||||
onTapOverride: onTapOverride,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/files_sharing/file_sharing_api.dart';
|
||||
import '../../../../api/marianumcloud/files_sharing/file_sharing_api_params.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/webdav/webdav_api.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
@@ -36,30 +34,21 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
final AsyncActionController _sendController = AsyncActionController();
|
||||
String? _sendError;
|
||||
|
||||
void share(String shareFolder, List<String> filePaths) {
|
||||
for (final element in filePaths) {
|
||||
final fileName = element.split(Platform.pathSeparator).last;
|
||||
FileSharingApi()
|
||||
.share(
|
||||
FileSharingApiParams(
|
||||
shareType: 10,
|
||||
shareWith: widget.sendToToken,
|
||||
path: '$shareFolder/$fileName',
|
||||
),
|
||||
)
|
||||
.then((_) {
|
||||
if (mounted) context.read<ChatBloc>().refresh();
|
||||
});
|
||||
}
|
||||
void share(List<String> uploadedRemotePaths) {
|
||||
shareFilesToChat(
|
||||
token: widget.sendToToken,
|
||||
remoteFilePaths: uploadedRemotePaths,
|
||||
).then((_) {
|
||||
if (mounted) context.read<ChatBloc>().refresh();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> mediaUpload(List<String>? paths) async {
|
||||
if (paths == null) return;
|
||||
|
||||
const shareFolder = 'MarianumMobile';
|
||||
unawaited(
|
||||
WebdavApi.webdav.then(
|
||||
(webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')),
|
||||
(webdav) => webdav.mkcol(PathUri.parse('/$talkShareFolder')),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -70,8 +59,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: paths,
|
||||
remotePath: shareFolder,
|
||||
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
|
||||
remotePath: talkShareFolder,
|
||||
onUploadFinished: share,
|
||||
uniqueNames: true,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -25,11 +25,17 @@ class ChatTile extends StatefulWidget {
|
||||
final bool disableContextActions;
|
||||
final bool hasDraft;
|
||||
|
||||
/// When set, replaces the default tap-into-chat behaviour. Used by the
|
||||
/// share-intent picker to surface the room selection without opening the
|
||||
/// chat view itself.
|
||||
final void Function(GetRoomResponseObject room)? onTapOverride;
|
||||
|
||||
const ChatTile({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.disableContextActions = false,
|
||||
this.hasDraft = false,
|
||||
this.onTapOverride,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -143,6 +149,10 @@ class _ChatTileState extends State<ChatTile> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.onTapOverride != null) {
|
||||
widget.onTapOverride!(widget.data);
|
||||
return;
|
||||
}
|
||||
if (selfUsername == null) return;
|
||||
unawaited(_setCurrentAsRead());
|
||||
final view = ChatView(
|
||||
|
||||
Reference in New Issue
Block a user