loading state and error handling refactor

This commit is contained in:
2026-05-06 10:11:45 +02:00
parent 2c376afd91
commit 4b1d4379a0
48 changed files with 1377 additions and 354 deletions
+13 -13
View File
@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
@@ -12,6 +11,7 @@ import '../../../state/app/infrastructure/utilityWidgets/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/async_action_button.dart';
import '../../../widget/file_pick.dart';
import '../../../widget/placeholder_view.dart';
import 'widgets/file_element.dart';
@@ -175,12 +175,10 @@ class _FilesViewState extends State<_FilesView> {
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
reversed: currentSortDirection,
);
return LoaderOverlay(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length,
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
),
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length,
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
);
},
),
@@ -233,15 +231,17 @@ class _FilesViewState extends State<_FilesView> {
content: TextField(
controller: inputController,
decoration: const InputDecoration(labelText: 'Name'),
autofocus: true,
),
actions: [
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')),
TextButton(
onPressed: () {
bloc.createFolder(inputController.text);
Navigator.of(dialogCtx).pop();
AsyncDialogAction(
confirmLabel: 'Ordner erstellen',
onConfirm: () async {
if (inputController.text.trim().isEmpty) {
throw Exception('Bitte einen Namen eingeben.');
}
await bloc.createFolder(inputController.text.trim());
},
child: const Text('Ordner erstellen'),
),
],
),
@@ -254,7 +254,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
border: const UnderlineInputBorder(),
label: Text('Datei ${index+1}'),
errorText: currentFile.isConflicting ? 'existiert bereits' : null,
errorStyle: const TextStyle(color: Colors.red),
errorStyle: TextStyle(color: Theme.of(context).colorScheme.error),
),
onChanged: (input) {
currentFile.fileName = input;
@@ -159,11 +159,12 @@ class _FileElementState extends State<FileElement> {
showDialog(context: context, builder: (context) => ConfirmDialog(
title: 'Element löschen?',
content: 'Das Element wird unwiederruflich gelöscht.',
onConfirm: () {
WebdavApi.webdav
.then((value) => value.delete(PathUri.parse(widget.file.path)))
.then((value) => widget.refetch());
}
confirmButton: 'Löschen',
onConfirmAsync: () async {
final webdav = await WebdavApi.webdav;
await webdav.delete(PathUri.parse(widget.file.path));
widget.refetch();
},
));
},
),
@@ -16,7 +16,7 @@ class AboutSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final settings = context.watch<SettingsCubit>();
return Column(
children: [
ListTile(
@@ -22,11 +22,11 @@ class AccountSection extends StatelessWidget {
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
builder: (dialogContext) => ConfirmDialog(
title: 'Abmelden?',
content: 'Möchtest du dich wirklich abmelden?',
confirmButton: 'Abmelden',
onConfirm: () async {
onConfirmAsync: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
PaintingBinding.instance.imageCache.clear();
@@ -9,7 +9,7 @@ class AppearanceSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final settings = context.watch<SettingsCubit>();
return ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text('Farbgebung'),
@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../storage/settings.dart' as model;
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/cache_view.dart';
@@ -28,34 +29,43 @@ class _DevToolsSectionState extends State<DevToolsSection> {
title: const Text('Performance overlays'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.auto_graph_outlined),
title: const Text('Performance graph'),
trailing: Checkbox(
value: widget.settings.val().devToolsSettings.showPerformanceOverlay,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.showPerformanceOverlay = e!,
),
),
ListTile(
leading: const Icon(Icons.screen_search_desktop_outlined),
title: const Text('Indicate offscreen layers'),
trailing: Checkbox(
value: widget.settings.val().devToolsSettings.checkerboardOffscreenLayers,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardOffscreenLayers = e!,
),
),
ListTile(
leading: const Icon(Icons.imagesearch_roller_outlined),
title: const Text('Indicate raster cache images'),
trailing: Checkbox(
value: widget.settings.val().devToolsSettings.checkerboardRasterCacheImages,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!,
),
),
],
));
showDialog(
context: context,
builder: (dialogCtx) => BlocBuilder<SettingsCubit, model.Settings>(
bloc: widget.settings,
builder: (_, _) {
final dev = widget.settings.val().devToolsSettings;
return SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.auto_graph_outlined),
title: const Text('Performance graph'),
trailing: Checkbox(
value: dev.showPerformanceOverlay,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.showPerformanceOverlay = e!,
),
),
ListTile(
leading: const Icon(Icons.screen_search_desktop_outlined),
title: const Text('Indicate offscreen layers'),
trailing: Checkbox(
value: dev.checkerboardOffscreenLayers,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardOffscreenLayers = e!,
),
),
ListTile(
leading: const Icon(Icons.imagesearch_roller_outlined),
title: const Text('Indicate raster cache images'),
trailing: Checkbox(
value: dev.checkerboardRasterCacheImages,
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!,
),
),
],
);
},
),
);
},
),
ListTile(
@@ -8,7 +8,7 @@ class FilesSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final settings = context.watch<SettingsCubit>();
return Column(
children: [
ListTile(
@@ -10,7 +10,7 @@ class TalkSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final settings = context.watch<SettingsCubit>();
final talkSettings = settings.val().talkSettings;
final notificationSettings = settings.val().notificationSettings;
return Column(
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../view/pages/timetable/data/timetable_name_mode.dart';
class TimetableSection extends StatelessWidget {
@@ -10,8 +9,7 @@ class TimetableSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final timetableBloc = context.read<TimetableBloc>();
final settings = context.watch<SettingsCubit>();
final timetableSettings = settings.val().timetableSettings;
return Column(
children: [
@@ -34,10 +32,8 @@ class TimetableSection extends StatelessWidget {
),
))
.toList(),
onChanged: (value) {
settings.val(write: true).timetableSettings.timetableNameMode = value!;
timetableBloc.refresh();
},
onChanged: (value) =>
settings.val(write: true).timetableSettings.timetableNameMode = value!,
),
),
ListTile(
@@ -45,10 +41,8 @@ class TimetableSection extends StatelessWidget {
title: const Text('Doppelstunden zusammenhängend anzeigen'),
trailing: Checkbox(
value: timetableSettings.connectDoubleLessons,
onChanged: (e) {
settings.val(write: true).timetableSettings.connectDoubleLessons = e!;
timetableBloc.refresh();
},
onChanged: (e) =>
settings.val(write: true).timetableSettings.connectDoubleLessons = e!,
),
),
],
+16 -21
View File
@@ -1,8 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../storage/settings.dart' as model;
import 'sections/about_section.dart';
import 'sections/account_section.dart';
import 'sections/appearance_section.dart';
@@ -14,24 +11,22 @@ class Settings extends StatelessWidget {
const Settings({super.key});
@override
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(
builder: (context, _) => Scaffold(
appBar: AppBar(title: const Text('Einstellungen')),
body: ListView(
children: const [
AccountSection(),
Divider(),
AppearanceSection(),
Divider(),
TimetableSection(),
Divider(),
TalkSection(),
Divider(),
FilesSection(),
Divider(),
AboutSection(),
],
),
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Einstellungen')),
body: ListView(
children: const [
AccountSection(),
Divider(),
AppearanceSection(),
Divider(),
TimetableSection(),
Divider(),
TalkSection(),
Divider(),
FilesSection(),
Divider(),
AboutSection(),
],
),
);
}
+9 -3
View File
@@ -12,6 +12,7 @@ import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notify_updater.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirm_dialog.dart';
import '../../../widget/placeholder_view.dart';
import 'widgets/chat_tile.dart';
import 'widgets/split_view_placeholder.dart';
import 'join_chat.dart';
@@ -144,9 +145,7 @@ class _ChatListViewState extends State<_ChatListView> {
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
bloc.createDirectChat(username);
},
onConfirmAsync: () => bloc.createDirectChat(username),
).asDialog(context);
});
},
@@ -164,6 +163,13 @@ class _ChatListViewState extends State<_ChatListView> {
unreadToTop: talkSettings.sortUnreadToTop,
);
if (sorted.isEmpty) {
return const PlaceholderView(
icon: Icons.chat_bubble_outline,
text: 'Noch keine Chats — starte einen über das +-Symbol.',
);
}
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
+10 -13
View File
@@ -2,9 +2,11 @@
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import '../../../api/errors/error_mapper.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
import '../../../model/endpoint_data.dart';
import '../../../widget/app_progress_indicator.dart';
import '../../../widget/placeholder_view.dart';
class JoinChat extends SearchDelegate<String> {
@@ -16,17 +18,9 @@ class JoinChat extends SearchDelegate<String> {
future: future!.value,
builder: (context, snapshot) {
if(snapshot.connectionState != ConnectionState.done) {
return Container(
padding: const EdgeInsets.all(10),
child: const Center(
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
),
),
),
return const Padding(
padding: EdgeInsets.all(10),
child: Center(child: AppProgressIndicator.medium()),
);
}
return const SizedBox.shrink();
@@ -76,10 +70,13 @@ class JoinChat extends SearchDelegate<String> {
}
);
} else if(snapshot.hasError) {
return const PlaceholderView(icon: Icons.search_off, text: 'Ein fehler ist aufgetreten. Bist du mit dem Internet verbunden?');
return PlaceholderView(
icon: Icons.search_off,
text: errorToUserMessage(snapshot.error),
);
}
return const Center(child: CircularProgressIndicator());
return const Center(child: AppProgressIndicator.large());
},
);
}
+17 -16
View File
@@ -14,6 +14,7 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../extensions/text.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/loading_spinner.dart';
import '../../files/widgets/file_element.dart';
import '../data/chat_bubble_styles.dart';
@@ -306,22 +307,22 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
padding: EdgeInsets.zero,
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
onPressed: () {
if(hasSelfReacted) {
// Delete existing reaction
DeleteReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: DeleteReactMessageParams(e.key),
).run().then((value) => widget.refetch(renew: true));
} else {
// Add reaction
ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(e.key)
).run().then((value) => widget.refetch(renew: true));
}
runWithErrorDialog(context, () async {
if (hasSelfReacted) {
await DeleteReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: DeleteReactMessageParams(e.key),
).run();
} else {
await ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(e.key),
).run();
}
widget.refetch(renew: true);
});
},
),
);
@@ -11,6 +11,8 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/app_progress_indicator.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/debug/debug_tile.dart';
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
@@ -78,14 +80,12 @@ Future<void> showChatMessageOptionsDialog(
onTap: () => Navigator.of(dialogCtx).pop(),
),
if (canDelete)
ListTile(
AsyncListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Nachricht löschen'),
onTap: () async {
onPressed: () async {
await DeleteMessage(chatData.token, bubbleData.id).run();
if (!dialogCtx.mounted) return;
dialogCtx.read<ChatBloc>().refresh();
Navigator.of(dialogCtx).pop();
if (dialogCtx.mounted) dialogCtx.read<ChatBloc>().refresh();
},
),
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
@@ -94,7 +94,7 @@ Future<void> showChatMessageOptionsDialog(
);
}
class _ReactionsRow extends StatelessWidget {
class _ReactionsRow extends StatefulWidget {
final String chatToken;
final int messageId;
final void Function({bool renew}) onRefetch;
@@ -107,46 +107,83 @@ class _ReactionsRow extends StatelessWidget {
required this.dialogContext,
});
void _react(String emoji) {
Navigator.of(dialogContext).pop();
ReactMessage(
chatToken: chatToken,
messageId: messageId,
params: ReactMessageParams(emoji),
).run().then((_) => onRefetch(renew: true));
@override
State<_ReactionsRow> createState() => _ReactionsRowState();
}
class _ReactionsRowState extends State<_ReactionsRow> {
final AsyncActionController _controller = AsyncActionController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _react(String emoji) async {
final ok = await _controller.run(() async {
await ReactMessage(
chatToken: widget.chatToken,
messageId: widget.messageId,
params: ReactMessageParams(emoji),
).run();
});
if (!mounted) return;
if (ok) {
widget.onRefetch(renew: true);
if (widget.dialogContext.mounted) Navigator.of(widget.dialogContext).pop();
}
}
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
Widget build(BuildContext context) => AnimatedBuilder(
animation: _controller,
builder: (context, _) {
final busy = _controller.busy;
final err = _controller.error;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
Wrap(
alignment: WrapAlignment.center,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: busy ? null : () => _react(emoji),
child: Text(emoji),
),
),
onPressed: () => _react(emoji),
child: Text(emoji),
),
IconButton(
onPressed: busy ? null : () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: busy
? const AppProgressIndicator.small()
: const Icon(Icons.add_circle_outline_outlined),
),
],
),
IconButton(
onPressed: () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
if (err != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
err,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
),
),
icon: const Icon(Icons.add_circle_outline_outlined),
),
const Divider(),
],
),
const Divider(),
],
);
},
);
void _showEmojiPicker(BuildContext rowContext) {
+47 -31
View File
@@ -12,6 +12,7 @@ import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.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/file_pick.dart';
import '../../../../widget/focus_behaviour.dart';
import '../../files/files_upload_dialog.dart';
@@ -30,7 +31,8 @@ class ChatTextfield extends StatefulWidget {
class _ChatTextfieldState extends State<ChatTextfield> {
late SettingsCubit settings;
final TextEditingController _textBoxController = TextEditingController();
bool isLoading = false;
final AsyncActionController _sendController = AsyncActionController();
String? _sendError;
void share(String shareFolder, List<String> filePaths) {
for (final element in filePaths) {
@@ -92,6 +94,29 @@ class _ChatTextfieldState extends State<ChatTextfield> {
}
}
@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] ?? '';
@@ -135,6 +160,14 @@ class _ChatTextfieldState extends State<ChatTextfield> {
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: () {
@@ -200,36 +233,19 @@ class _ChatTextfieldState extends State<ChatTextfield> {
),
),
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),
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),
),
),
]),
],
+39 -50
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -11,6 +12,7 @@ import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dar
import '../../../../model/account_data.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/user_avatar.dart';
@@ -42,15 +44,14 @@ class _ChatTileState extends State<ChatTile> {
void _refreshList() => context.read<ChatListBloc>().refresh();
void setCurrentAsRead() {
SetReadMarker(
Future<void> _setCurrentAsRead() async {
await SetReadMarker(
widget.data.token,
true,
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
).run().then((_) {
if (!mounted) return;
_refreshList();
});
).run();
if (!mounted) return;
_refreshList();
}
@override
@@ -116,7 +117,7 @@ class _ChatTileState extends State<ChatTile> {
),
onTap: () {
if (selfUsername == null) return;
setCurrentAsRead();
unawaited(_setCurrentAsRead());
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
context.read<ChatBloc>().setToken(widget.data.token);
@@ -125,65 +126,53 @@ class _ChatTileState extends State<ChatTile> {
if (widget.disableContextActions) return;
showDialog(context: context, builder: (dialogCtx) => 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((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
child: ListTile(
if (widget.data.unreadMessages > 0)
AsyncListTile(
leading: const Icon(Icons.mark_chat_read_outlined),
title: const Text('Als gelesen markieren'),
onTap: () {
setCurrentAsRead();
Navigator.of(dialogCtx).pop();
onPressed: _setCurrentAsRead,
)
else
AsyncListTile(
leading: const Icon(Icons.mark_chat_unread_outlined),
title: const Text('Als ungelesen markieren'),
onPressed: () async {
await SetReadMarker(widget.data.token, false).run();
if (mounted) _refreshList();
},
),
),
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(
if (widget.data.isFavorite)
AsyncListTile(
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();
onPressed: () async {
await SetFavorite(widget.data.token, false).run();
if (mounted) _refreshList();
},
)
else
AsyncListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onPressed: () async {
await SetFavorite(widget.data.token, true).run();
if (mounted) _refreshList();
},
),
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Konversation verlassen'),
onTap: () {
Navigator.of(dialogCtx).pop();
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();
confirmButton: 'Verlassen',
onConfirmAsync: () async {
await LeaveRoom(widget.data.token).run();
if (mounted) _refreshList();
},
).asDialog(dialogCtx);
).asDialog(context);
},
),
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
@@ -15,9 +15,7 @@ Completer<void> showDeleteCustomEventDialog(BuildContext context, CustomTimetabl
content: 'Der ${event.rrule.isEmpty ? "Termin" : "Serientermin"} wird unwiederruflich gelöscht.',
confirmButton: 'Löschen',
onConfirm: () {
bloc.removeCustomEvent(event.id).then(completer.complete).onError((Object error, StackTrace stack) {
completer.completeError(error, stack);
});
bloc.removeCustomEvent(event.id).then(completer.complete).onError(completer.completeError);
},
).asDialog(context);
return completer;
+8 -3
View File
@@ -8,6 +8,7 @@ import '../../../state/app/infrastructure/loadableState/view/loadable_state_cons
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../storage/timetable_settings.dart';
import 'custom_events/custom_event_edit_dialog.dart';
import 'data/arbitrary_appointment.dart';
import 'data/lesson_period_schedule.dart';
@@ -30,6 +31,7 @@ class _TimetableState extends State<Timetable> {
List<Appointment>? _cachedAppointments;
int? _lastDataVersion;
TimetableSettings? _lastTimetableSettings;
DateTime _initialDisplayDate() => DateTime.now().add(const Duration(days: 2));
@@ -51,18 +53,21 @@ class _TimetableState extends State<Timetable> {
}
List<Appointment> _appointments(TimetableState state) {
if (_cachedAppointments != null && _lastDataVersion == state.dataVersion) {
final timetableSettings = context.watch<SettingsCubit>().val().timetableSettings;
if (_cachedAppointments != null &&
_lastDataVersion == state.dataVersion &&
identical(_lastTimetableSettings, timetableSettings)) {
return _cachedAppointments!;
}
_lastDataVersion = state.dataVersion;
_lastTimetableSettings = timetableSettings;
final settings = context.read<SettingsCubit>();
return _cachedAppointments = TimetableAppointmentFactory(
lessons: state.getAllKnownLessons().toList(),
customEvents: state.customEvents?.events ?? const [],
rooms: state.rooms!,
subjects: state.subjects!,
settings: settings.val().timetableSettings,
settings: timetableSettings,
now: DateTime.now(),
).build();
}