Files
Client/lib/view/pages/talk/widgets/chat_textfield.dart
T
2026-05-08 20:12:40 +02:00

318 lines
11 KiB
Dart

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/webdav/webdav_api.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/details_bottom_sheet.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;
final String? selfId;
const ChatTextfield(this.sendToToken, {this.selfId, super.key});
@override
State<ChatTextfield> createState() => _ChatTextfieldState();
}
class _ChatTextfieldState extends State<ChatTextfield> {
late SettingsCubit settings;
final TextEditingController _textBoxController = TextEditingController();
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();
});
}
}
Future<void> mediaUpload(List<String>? paths) async {
if (paths == null) return;
const shareFolder = 'MarianumMobile';
unawaited(
WebdavApi.webdav.then(
(webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')),
),
);
if (!mounted) return;
unawaited(
pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
filePaths: paths,
remotePath: shareFolder,
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
uniqueNames: true,
),
),
);
}
void _setDraft(String text) {
final talkSettings = settings.val(write: true).talkSettings;
if (text.isNotEmpty) {
talkSettings.drafts[widget.sendToToken] = text;
} else {
talkSettings.drafts.removeWhere((key, _) => key == widget.sendToToken);
}
}
void _setDraftReply(int? messageId) {
final talkSettings = settings.val(write: true).talkSettings;
if (messageId != null) {
talkSettings.draftReplies[widget.sendToToken] = messageId;
} else {
talkSettings.draftReplies.removeWhere(
(key, _) => key == widget.sendToToken,
);
}
}
@override
void initState() {
super.initState();
settings = context.read<SettingsCubit>();
final draftReply = settings
.val()
.talkSettings
.draftReplies[widget.sendToToken];
if (draftReply != null) {
context.read<ChatBloc>().setReferenceMessageId(draftReply);
}
}
@override
void dispose() {
_sendController.dispose();
super.dispose();
}
Future<void> _sendMessage(ChatBloc chatBloc) async {
if (_textBoxController.text.isEmpty) return;
final text = _textBoxController.text;
final replyTo = chatBloc.state.data?.referenceMessageId?.toString();
setState(() => _sendError = null);
await SendMessage(
widget.sendToToken,
SendMessageParams(text, replyTo: replyTo),
).run();
if (!mounted) return;
chatBloc.refresh();
_textBoxController.text = '';
_setDraft('');
chatBloc.setReferenceMessageId(null);
_setDraftReply(null);
}
@override
Widget build(BuildContext context) {
_textBoxController.text =
settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
final chatBloc = context.watch<ChatBloc>();
final chatState = chatBloc.state.data;
Widget replyBanner = const SizedBox.shrink();
if (chatState != null &&
chatState.referenceMessageId != null &&
chatState.chatResponse != null) {
try {
final referenceMessage = chatState.chatResponse!
.sortByTimestamp()
.firstWhere((e) => e.id == chatState.referenceMessageId);
replyBanner = Row(
children: [
Expanded(
child: AnswerReference(
context: context,
referenceMessage: referenceMessage,
selfId: widget.selfId,
),
),
IconButton(
onPressed: () {
chatBloc.setReferenceMessageId(null);
_setDraftReply(null);
},
icon: const Icon(Icons.close_outlined),
padding: const EdgeInsets.only(left: 0),
),
],
);
} catch (_) {
/* reference no longer in current chat data */
}
}
return Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(
left: 10,
bottom: 3,
top: 3,
right: 10,
),
width: double.infinity,
child: Column(
children: [
replyBanner,
if (_sendError != null)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
_sendError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
Row(
children: <Widget>[
GestureDetector(
onTap: () {
showDetailsBottomSheet(
context,
children: (sheetCtx) => [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Galerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) {
mediaUpload(
value.map((e) => e.path).toList(),
);
}
});
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Foto aufnehmen'),
onTap: () {
FilePick.cameraPick().then((image) {
if (image != null) mediaUpload([image.path]);
});
Navigator.of(sheetCtx).pop();
},
),
],
);
},
child: Material(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.attach_file_outlined,
color: Colors.white,
size: 20,
),
),
),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
autocorrect: true,
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (text) {
if (text.trim().toLowerCase() ==
'marbot marbot marbot') {
const newText =
'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
_setDraft(text);
},
onTapOutside: (_) =>
FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _textBoxController,
builder: (context, value, _) => AsyncFab(
mini: true,
heroTag: 'chatSend_${widget.sendToToken}',
icon: Icons.send,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
controller: _sendController,
onPressed: value.text.trim().isEmpty
? null
: () => _sendMessage(chatBloc),
onError: (message) =>
setState(() => _sendError = message),
onSuccess: () => setState(() => _sendError = null),
),
),
],
),
],
),
),
),
],
);
}
}