claude refactor

This commit is contained in:
2026-05-04 13:54:39 +02:00
parent 9973f12733
commit 551c1bf1fa
125 changed files with 4484 additions and 2544 deletions
+78 -85
View File
@@ -1,79 +1,85 @@
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import '../../../api/marianumcloud/talk/createRoom/createRoom.dart';
import '../../../api/marianumcloud/talk/createRoom/createRoomParams.dart';
import '../../../model/chatList/chatListProps.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notifyUpdater.dart';
import '../../../storage/base/settingsProvider.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirmDialog.dart';
import '../../../widget/loadingSpinner.dart';
import 'components/chatTile.dart';
import 'components/splitViewPlaceholder.dart';
import 'joinChat.dart';
import 'searchChat.dart';
class ChatList extends StatefulWidget {
class ChatList extends StatelessWidget {
const ChatList({super.key});
@override
State<ChatList> createState() => _ChatListState();
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
}
class _ChatListState extends State<ChatList> {
late SettingsProvider settings;
class _ChatListView extends StatefulWidget {
const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override
void initState() {
super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false);
_settings = context.read<SettingsCubit>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_query();
if(!settings.val().notificationSettings.enabled && !settings.val().notificationSettings.askUsageDismissed) {
settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(
provisional: false
).then((value) {
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(context: context, builder: (context) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
));
break;
default:
break;
}
});
},
).asDialog(context);
}
});
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAskForNotificationPermission());
}
void _query({bool renew = false}) {
Provider.of<ChatListProps>(context, listen: false).run(renew: renew);
void _maybeAskForNotificationPermission() {
final notificationSettings = _settings.val().notificationSettings;
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
if (!mounted) return;
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(
context: context,
builder: (_) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
),
);
break;
default:
break;
}
});
},
).asDialog(context);
}
@override
Widget build(BuildContext context) {
ChatListProps? latestData;
final bloc = context.read<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
@@ -83,63 +89,50 @@ class _ChatListState extends State<ChatList> {
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
if(latestData == null) return;
showSearch(context: context, delegate: SearchChat(latestData!.getRoomsResponse.data.toList()));
onPressed: () {
final rooms = bloc.state.data?.rooms;
if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
},
)
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () async {
onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) {
if(username == null) return;
if (username == null || !context.mounted) return;
ConfirmDialog(
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
CreateRoom(CreateRoomParams(
roomType: 1,
invite: username,
)).run().then((value) {
_query(renew: true);
});
bloc.createDirectChat(username);
},
).asDialog(context);
});
},
child: const Icon(Icons.add_comment_outlined),
),
body: Consumer<ChatListProps>(
builder: (context, data, child) {
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
if(data.primaryLoading()) return const LoadingSpinner();
latestData = data;
var chats = <ChatTile>[];
for (var chatRoom in data.getRoomsResponse.sortBy(
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
final sorted = rooms.sortBy(
lastActivity: true,
favoritesToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortFavoritesToTop,
unreadToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortUnreadToTop,
)
) {
var hasDraft = settings.val().talkSettings.drafts.containsKey(chatRoom.token);
chats.add(ChatTile(data: chatRoom, query: _query, hasDraft: hasDraft));
}
favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: talkSettings.sortUnreadToTop,
);
return RefreshIndicator(
color: Theme.of(context).primaryColor,
onRefresh: () {
_query(renew: true);
return Future.delayed(const Duration(seconds: 3));
},
child: ListView(
padding: EdgeInsets.zero,
children: chats
),
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return ChatTile(data: room, hasDraft: hasDraft);
}).toList(),
);
},
),
+57 -61
View File
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import '../../../extensions/dateTime.dart';
import 'package:provider/provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../extensions/dateTime.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/appTheme.dart';
import '../../../model/chatList/chatProps.dart';
import '../../../widget/clickableAppBar.dart';
import '../../../widget/loadingSpinner.dart';
import '../../../widget/userAvatar.dart';
@@ -27,66 +27,63 @@ class ChatView extends StatefulWidget {
}
class _ChatViewState extends State<ChatView> {
final ScrollController _listController = ScrollController();
@override
void initState() {
super.initState();
}
void _query({bool renew = false}) {
Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.room.token);
void _refresh() {
context.read<ChatBloc>().setToken(widget.room.token);
}
@override
Widget build(BuildContext context) => Consumer<ChatProps>(
builder: (context, data, child) {
var messages = List<Widget>.empty(growable: true);
Widget build(BuildContext context) => BlocBuilder<ChatBloc, dynamic>(
builder: (context, _) {
final state = context.watch<ChatBloc>().state.data ?? const ChatState();
final response = state.chatResponse;
final isLoading = response == null;
if(!data.primaryLoading()) {
final messages = <Widget>[];
if (response != null) {
var lastDate = DateTime.now();
data.getChatResponse.sortByTimestamp().forEach((element) {
var elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
for (final element in response.sortByTimestamp()) {
final elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
if(element.systemMessage.contains('reaction')) return;
if(element.systemMessage.contains('poll_voted')) return;
var commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0');
if (element.systemMessage.contains('reaction')) continue;
if (element.systemMessage.contains('poll_voted')) continue;
final commonRead = int.parse(response.headers?['x-chat-last-common-read'] ?? '0');
if(!elementDate.isSameDay(lastDate)) {
if (!elementDate.isSameDay(lastDate)) {
lastDate = elementDate;
messages.add(ChatBubble(
context: context,
isSender: false,
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
chatData: widget.room,
refetch: _query,
refetch: ({bool renew = false}) => _refresh(),
));
}
messages.add(
ChatBubble(
context: context,
isSender: element.actorId == widget.selfId && element.messageType == GetRoomResponseObjectMessageType.comment,
bubbleData: element,
chatData: widget.room,
refetch: _query,
isRead: element.id <= commonRead,
selfId: widget.selfId,
)
);
});
if(data.getChatResponse.data.length >= 200) {
messages.add(ChatBubble(
context: context,
isSender: element.actorId == widget.selfId &&
element.messageType == GetRoomResponseObjectMessageType.comment,
bubbleData: element,
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
isRead: element.id <= commonRead,
selfId: widget.selfId,
));
}
if (response.data.length >= 200) {
messages.insert(0, ChatBubble(
context: context,
isSender: false,
bubbleData: GetChatResponseObject.getTextDummy(
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de'
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de',
),
chatData: widget.room,
refetch: _query,
refetch: ({bool renew = false}) => _refresh(),
));
}
}
@@ -94,9 +91,7 @@ class _ChatViewState extends State<ChatView> {
return Scaffold(
backgroundColor: const Color(0xffefeae2),
appBar: ClickableAppBar(
onTap: () {
TalkNavigator.pushSplitView(context, ChatInfo(widget.room));
},
onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)),
appBar: AppBar(
title: Row(
children: [
@@ -104,7 +99,7 @@ class _ChatViewState extends State<ChatView> {
const SizedBox(width: 10),
Expanded(
child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
)
),
],
),
),
@@ -117,26 +112,27 @@ class _ChatViewState extends State<ChatView> {
opacity: 1,
repeat: ImageRepeat.repeat,
invertColors: AppTheme.isDarkMode(context),
)
),
),
child: data.primaryLoading() ? const LoadingSpinner() : Column(
children: [
Expanded(
child: ListView(
reverse: true,
controller: _listController,
children: messages.reversed.toList(),
child: isLoading
? const LoadingSpinner()
: Column(
children: [
Expanded(
child: ListView(
reverse: true,
controller: _listController,
children: messages.reversed.toList(),
),
),
Container(
color: Theme.of(context).colorScheme.surface,
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token, selfId: widget.selfId)
: SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)),
),
],
),
),
Container(
color: Theme.of(context).colorScheme.surface,
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token, selfId: widget.selfId)
: SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)
),
)
],
),
),
);
},
@@ -16,8 +16,8 @@ class AnswerReference extends StatelessWidget {
return DecoratedBox(
decoration: BoxDecoration(
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200).withOpacity(0.2)
: style.getRemoteStyle(false).color!.withWhite(200).withOpacity(0.2),
? style.getSelfStyle(false).color!.withGreen(200).withValues(alpha: 0.2)
: style.getRemoteStyle(false).color!.withWhite(200).withValues(alpha: 0.2),
borderRadius: const BorderRadius.all(Radius.circular(5)),
border: Border(left: BorderSide(
color: referenceMessage.actorId == selfId
@@ -8,7 +8,7 @@ import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../extensions/text.dart';
import 'package:provider/provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
@@ -17,7 +17,7 @@ import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../model/chatList/chatProps.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../files/fileElement.dart';
@@ -189,9 +189,9 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
child: ListTile(
leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'),
onTap: () => {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token),
Navigator.of(context).pop(),
onTap: () {
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
Navigator.of(context).pop();
},
),
),
@@ -236,7 +236,8 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
title: const Text('Nachricht löschen'),
onTap: () {
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
Provider.of<ChatProps>(context, listen: false).run();
if (!context.mounted) return;
context.read<ChatBloc>().refresh();
Navigator.of(context).pop();
});
},
@@ -294,7 +295,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
_position = const Offset(0, 0);
});
if(widget.bubbleData.isReplyable && isAction) {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token);
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
}
},
onLongPress: showOptionsDialog,
@@ -341,6 +342,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
TextButton(onPressed: () {
downloadCore?.then((value) {
if(!value.isCancelled) value.cancel();
if (!context.mounted) return;
Navigator.of(context).pop();
});
setState(() {
@@ -5,14 +5,16 @@ import '../../../../theming/appTheme.dart';
extension ColorExtensions on Color {
Color invert() {
final r = 255 - red;
final g = 255 - green;
final b = 255 - blue;
return Color.fromARGB((opacity * 255).round(), r, g, b);
final invertedR = 1.0 - r;
final invertedG = 1.0 - g;
final invertedB = 1.0 - b;
return Color.from(alpha: a, red: invertedR, green: invertedG, blue: invertedB);
}
Color withWhite(int whiteValue) => Color.fromARGB(alpha, whiteValue, whiteValue, whiteValue);
Color withWhite(int whiteValue) {
final value = whiteValue / 255.0;
return Color.from(alpha: a, red: value, green: value, blue: value);
}
}
class ChatBubbleStyles {
+174 -166
View File
@@ -1,17 +1,17 @@
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 'package:provider/provider.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
import '../../../../api/marianumcloud/talk/sendMessage/sendMessage.dart';
import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../model/chatList/chatProps.dart';
import '../../../../storage/base/settingsProvider.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../files/filesUploadDialog.dart';
@@ -20,6 +20,7 @@ import 'answerReference.dart';
class ChatTextfield extends StatefulWidget {
final String sendToToken;
final String? selfId;
const ChatTextfield(this.sendToToken, {this.selfId, super.key});
@override
@@ -27,207 +28,214 @@ class ChatTextfield extends StatefulWidget {
}
class _ChatTextfieldState extends State<ChatTextfield> {
late SettingsProvider settings;
late SettingsCubit settings;
final TextEditingController _textBoxController = TextEditingController();
bool isLoading = false;
void _query() {
Provider.of<ChatProps>(context, listen: false).run();
}
void share(String shareFolder, List<String> filePaths) {
for (var element in filePaths) {
var fileName = element.split(Platform.pathSeparator).last;
for (final element in filePaths) {
final fileName = element.split(Platform.pathSeparator).last;
FileSharingApi().share(FileSharingApiParams(
shareType: 10,
shareWith: widget.sendToToken,
path: '$shareFolder/$fileName',
)).then((value) => _query());
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;
var shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) {
webdav.mkcol(PathUri.parse('/$shareFolder'));
});
const shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')));
if (!mounted) return;
pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
filePaths: paths,
remotePath: shareFolder,
onUploadFinished: (uploadedFilePaths) {
share(shareFolder, uploadedFilePaths);
},
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
uniqueNames: true,
),
);
}
void setDraft(String text) {
if(text.isNotEmpty) {
settings.val(write: true).talkSettings.drafts[widget.sendToToken] = text;
void _setDraft(String text) {
final talkSettings = settings.val(write: true).talkSettings;
if (text.isNotEmpty) {
talkSettings.drafts[widget.sendToToken] = text;
} else {
settings.val(write: true).talkSettings.drafts.removeWhere((key, value) => key == widget.sendToToken);
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 = Provider.of<SettingsProvider>(context, listen: false);
Provider.of<ChatProps>(context, listen: false).unsafeInternalSetReferenceMessageId =
settings.val().talkSettings.draftReplies[widget.sendToToken];
settings = context.read<SettingsCubit>();
final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken];
if (draftReply != null) {
context.read<ChatBloc>().setReferenceMessageId(draftReply);
}
}
@override
Widget build(BuildContext context) {
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
final chatBloc = context.watch<ChatBloc>();
final chatState = chatBloc.state.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: [
Consumer<ChatProps>(
builder: (context, data, child) {
if(data.getReferenceMessageId != null) {
var referenceMessage = data.getChatResponse.sortByTimestamp().where((element) => element.id == data.getReferenceMessageId).last;
return Row(
children: [
Expanded(
child: AnswerReference(
context: context,
referenceMessage: referenceMessage,
selfId: widget.selfId,
),
),
IconButton(
onPressed: () => data.setReferenceMessageId(null, context, widget.sendToToken),
icon: const Icon(Icons.close_outlined),
padding: const EdgeInsets.only(left: 0),
),
],
);
} else {
return const SizedBox.shrink();
}
},
),
Row(
children: <Widget>[
GestureDetector(
onTap: (){
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if(value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(context).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: (String text) {
if(text.trim().toLowerCase() == 'marbot marbot marbot') {
var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
setDraft(text);
},
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
FloatingActionButton(
mini: true,
onPressed: () {
if(_textBoxController.text.isEmpty) return;
if(isLoading) return;
setState(() {
isLoading = true;
});
SendMessage(widget.sendToToken, SendMessageParams(
_textBoxController.text,
replyTo: Provider.of<ChatProps>(context, listen: false).getReferenceMessageId.toString()
)).run().then((value) {
_query();
setState(() {
isLoading = false;
});
_textBoxController.text = '';
setDraft('');
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(null, context, widget.sendToToken);
});
},
backgroundColor: Theme.of(context).primaryColor,
elevation: 5,
child: isLoading
? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send, color: Colors.white, size: 18),
),
],
),
],
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,
Row(children: <Widget>[
GestureDetector(
onTap: () {
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(dialogCtx).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(dialogCtx).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),
FloatingActionButton(
mini: true,
onPressed: () {
if (_textBoxController.text.isEmpty || isLoading) return;
setState(() => isLoading = true);
SendMessage(
widget.sendToToken,
SendMessageParams(
_textBoxController.text,
replyTo: chatBloc.state.data?.referenceMessageId?.toString(),
),
).run().then((_) {
if (!mounted) return;
chatBloc.refresh();
setState(() => isLoading = false);
_textBoxController.text = '';
_setDraft('');
chatBloc.setReferenceMessageId(null);
_setDraftReply(null);
});
},
backgroundColor: Theme.of(context).primaryColor,
elevation: 5,
child: isLoading
? Container(
padding: const EdgeInsets.all(10),
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
)
: const Icon(Icons.send, color: Colors.white, size: 18),
),
]),
],
),
),
],
);
),
]);
}
}
+150 -139
View File
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../api/marianumcloud/talk/leaveRoom/leaveRoom.dart';
@@ -10,7 +9,9 @@ import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
import '../../../../model/chatList/chatProps.dart';
import '../../../../model/accountData.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../widget/confirmDialog.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/userAvatar.dart';
@@ -19,167 +20,177 @@ import '../talkNavigator.dart';
class ChatTile extends StatefulWidget {
final GetRoomResponseObject data;
final void Function({bool renew}) query;
final bool disableContextActions;
final bool hasDraft;
const ChatTile({super.key, required this.data, required this.query, this.disableContextActions = false, this.hasDraft = false});
const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false});
@override
State<ChatTile> createState() => _ChatTileState();
}
class _ChatTileState extends State<ChatTile> {
late String selfUsername;
String? selfUsername;
@override
void initState() {
super.initState();
SharedPreferences.getInstance().then((value) => {
selfUsername = value.getString('username')!
AccountData().waitForPopulation().then((_) {
if (!mounted) return;
setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null);
});
}
void _refreshList() => context.read<ChatListBloc>().refresh();
void setCurrentAsRead() {
SetReadMarker(
widget.data.token,
true,
setReadMarkerParams: SetReadMarkerParams(
lastReadMessage: widget.data.lastMessage.id
)
).run().then((value) => widget.query(renew: true));
widget.data.token,
true,
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
).run().then((_) {
if (!mounted) return;
_refreshList();
});
}
@override
Widget build(BuildContext context) => Consumer<ChatProps>(builder: (context, chatData, child) {
var isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
var circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
Widget build(BuildContext context) {
final chatBloc = context.watch<ChatBloc>();
final isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
return ListTile(
style: ListTileStyle.list,
tileColor: chatData.currentToken() == widget.data.token && TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100)
: null,
leading: Stack(
style: ListTileStyle.list,
tileColor: chatBloc.state.data?.currentToken == widget.data.token && TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100)
: null,
leading: Stack(
children: [
circleAvatar,
Visibility(
visible: widget.data.isFavorite,
child: Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
),
),
)
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis)),
if (widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(Icons.edit_outlined, size: 15),
],
],
),
subtitle: Text(
'${Jiffy.parseFromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).fromNow()}: '
'${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}',
overflow: TextOverflow.ellipsis,
),
trailing: widget.data.unreadMessages <= 0
? null
: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
child: Text(
'${widget.data.unreadMessages}',
style: const TextStyle(color: Colors.white, fontSize: 15),
textAlign: TextAlign.center,
),
),
onTap: () {
if (selfUsername == null) return;
setCurrentAsRead();
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
context.read<ChatBloc>().setToken(widget.data.token);
},
onLongPress: () {
if (widget.disableContextActions) return;
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
children: [
circleAvatar,
Visibility(
visible: widget.data.isFavorite,
child: Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
),
),
)
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis),
),
if(widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(Icons.edit_outlined, size: 15),
],
],
),
subtitle: Text("${Jiffy.parseFromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).fromNow()}: ${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}", overflow: TextOverflow.ellipsis),
trailing: widget.data.unreadMessages <= 0
? null
: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
'${widget.data.unreadMessages}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
onTap: () async {
setCurrentAsRead();
var view = ChatView(room: widget.data, selfId: selfUsername, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.data.token);
},
onLongPress: () {
if(widget.disableContextActions) return;
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
Visibility(
visible: widget.data.unreadMessages > 0,
replacement: ListTile(
leading: const Icon(Icons.mark_chat_unread_outlined),
title: const Text('Als ungelesen markieren'),
onTap: () {
SetReadMarker(widget.data.token, false).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
child: ListTile(
leading: const Icon(Icons.mark_chat_read_outlined),
title: const Text('Als gelesen markieren'),
onTap: () {
setCurrentAsRead();
Navigator.of(context).pop();
},
),
),
Visibility(
visible: widget.data.isFavorite,
replacement: ListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onTap: () {
SetFavorite(widget.data.token, true).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
child: ListTile(
leading: const Icon(Icons.stars_outlined),
title: const Text('Von Favoriten entfernen'),
onTap: () {
SetFavorite(widget.data.token, false).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Konversation verlassen'),
visible: widget.data.unreadMessages > 0,
replacement: ListTile(
leading: const Icon(Icons.mark_chat_unread_outlined),
title: const Text('Als ungelesen markieren'),
onTap: () {
ConfirmDialog(
title: 'Chat verlassen',
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
confirmButton: 'Löschen',
onConfirm: () {
LeaveRoom(widget.data.token).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
).asDialog(context);
SetReadMarker(widget.data.token, false).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
DebugTile(context).jsonData(widget.data.toJson()),
],
));
},
);
});
child: ListTile(
leading: const Icon(Icons.mark_chat_read_outlined),
title: const Text('Als gelesen markieren'),
onTap: () {
setCurrentAsRead();
Navigator.of(dialogCtx).pop();
},
),
),
Visibility(
visible: widget.data.isFavorite,
replacement: ListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onTap: () {
SetFavorite(widget.data.token, true).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
child: ListTile(
leading: const Icon(Icons.stars_outlined),
title: const Text('Von Favoriten entfernen'),
onTap: () {
SetFavorite(widget.data.token, false).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Konversation verlassen'),
onTap: () {
ConfirmDialog(
title: 'Chat verlassen',
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
confirmButton: 'Löschen',
onConfirm: () {
LeaveRoom(widget.data.token).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
).asDialog(dialogCtx);
},
),
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
],
));
},
);
}
}
@@ -26,7 +26,7 @@ class _PollOptionsListState extends State<PollOptionsList> {
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
: 0;
var numVoters = widget.pollData.numVoters ?? 0;
double portion = numVoters == 0 ? 0 : (votes / numVoters);
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
return ListTile(
// enabled: false,
+1 -1
View File
@@ -25,7 +25,7 @@ class SearchChat extends SearchDelegate {
itemCount: items.length,
itemBuilder: (context, index) {
var item = items.elementAt(index);
return ChatTile(data: item, disableContextActions: true, query: ({bool renew = true}) {});
return ChatTile(data: item, disableContextActions: true);
},
);
}