add syncing waiting / error / failed indicator
This commit is contained in:
parent
a5fef138dd
commit
07700c1eff
6 changed files with 186 additions and 75 deletions
|
|
@ -85,10 +85,6 @@ class GomuksListenerController extends AsyncNotifier<void> {
|
||||||
.read(SpaceEdgesController.provider.notifier)
|
.read(SpaceEdgesController.provider.notifier)
|
||||||
.set(spaceEdges);
|
.set(spaceEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ref
|
|
||||||
// .read(SyncStatusController.provider.notifier)
|
|
||||||
// .set(SyncStatus.fromJson(decodedMuksEvent));
|
|
||||||
default:
|
default:
|
||||||
debugPrint("Unhandled event: $muksEventType");
|
debugPrint("Unhandled event: $muksEventType");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/epoch_date_time_converter.dart";
|
||||||
|
|
||||||
part "sync_status.freezed.dart";
|
part "sync_status.freezed.dart";
|
||||||
part "sync_status.g.dart";
|
part "sync_status.g.dart";
|
||||||
|
|
@ -7,8 +8,9 @@ part "sync_status.g.dart";
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class const SyncStatus({
|
class const SyncStatus({
|
||||||
required final SyncStatusType type,
|
required final SyncStatusType type,
|
||||||
required final String? error,
|
|
||||||
required final int errorCount,
|
required final int errorCount,
|
||||||
|
final String? error,
|
||||||
|
@EpochDateTimeConverter() final DateTime? lastSync,
|
||||||
}) with _$SyncStatus {
|
}) with _$SyncStatus {
|
||||||
Map<String, Object?> toJson() => _$SyncStatusToJson(this);
|
Map<String, Object?> toJson() => _$SyncStatusToJson(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ final class const ErrorDialog(
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("An Error Occurred"),
|
title: Text("An Error Occurred"),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: SelectableText("$error\n\n$stackTrace"),
|
child: SelectableText(
|
||||||
|
"$error${stackTrace == null ? "" : "\n\n$stackTrace"}",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (provider != null)
|
if (provider != null)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import "package:nexus/controllers/key.dart";
|
||||||
import "package:nexus/controllers/notification_request.dart";
|
import "package:nexus/controllers/notification_request.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/sidebar.dart";
|
import "package:nexus/widgets/sidebar.dart";
|
||||||
|
import "package:nexus/widgets/sync_status_banner.dart";
|
||||||
import "package:nexus/widgets/room_chat/room_chat.dart";
|
import "package:nexus/widgets/room_chat/room_chat.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
||||||
|
|
@ -57,11 +58,18 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Center(
|
: Column(
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisSize: .min,
|
const SyncStatusBanner(),
|
||||||
children: [Loading(), Text("Syncing...")],
|
Expanded(
|
||||||
),
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [Loading(), Text("Syncing...")],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
drawer: isDesktop || !initComplete ? null : Sidebar(),
|
drawer: isDesktop || !initComplete ? null : Sidebar(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import "package:nexus/widgets/loading.dart";
|
||||||
import "package:nexus/widgets/pinned_events_drawer.dart";
|
import "package:nexus/widgets/pinned_events_drawer.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
|
import "package:nexus/widgets/sync_status_banner.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
||||||
import "package:nexus/helpers/extensions/build_event_options.dart";
|
import "package:nexus/helpers/extensions/build_event_options.dart";
|
||||||
|
|
@ -62,7 +63,12 @@ final class const RoomChat({
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
onOpenDrawer: Scaffold.of(context).openDrawer,
|
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
body: nothing,
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const SyncStatusBanner(),
|
||||||
|
Expanded(child: nothing),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,79 +205,91 @@ final class const RoomChat({
|
||||||
},
|
},
|
||||||
onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer,
|
onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer,
|
||||||
),
|
),
|
||||||
body: Row(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SyncStatusBanner(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Expanded(
|
||||||
child: Padding(
|
child: Stack(
|
||||||
padding: .symmetric(horizontal: 4),
|
children: [
|
||||||
child: switch (controllerData) {
|
Positioned.fill(
|
||||||
AsyncLoading(value: _?) ||
|
child: Padding(
|
||||||
AsyncData(value: _) => ChatTimeline(
|
padding: .symmetric(horizontal: 4),
|
||||||
scroll: scroll,
|
child: switch (controllerData) {
|
||||||
roomId: roomId,
|
AsyncLoading(value: _?) ||
|
||||||
jumpToEvent: jumpToEvent,
|
AsyncData(value: _) => ChatTimeline(
|
||||||
getEventOptions: getEventOptions,
|
scroll: scroll,
|
||||||
highlightedEvent: highlightedEvent.value,
|
roomId: roomId,
|
||||||
composerHeight: composerSize.value,
|
jumpToEvent: jumpToEvent,
|
||||||
|
getEventOptions: getEventOptions,
|
||||||
|
highlightedEvent: highlightedEvent.value,
|
||||||
|
composerHeight: composerSize.value,
|
||||||
|
),
|
||||||
|
AsyncLoading _ => Loading(),
|
||||||
|
AsyncError(:final error, :final stackTrace) =>
|
||||||
|
ErrorDialog(error, stackTrace),
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
AsyncLoading _ => Loading(),
|
Positioned(
|
||||||
AsyncError(:final error, :final stackTrace) =>
|
right: 16,
|
||||||
ErrorDialog(error, stackTrace),
|
bottom: composerSize.value,
|
||||||
},
|
child: IgnorePointer(
|
||||||
),
|
ignoring: scroll.atBottom,
|
||||||
),
|
child: AnimatedOpacity(
|
||||||
Positioned(
|
opacity: scroll.atBottom ? 0 : 1,
|
||||||
right: 16,
|
duration: const Duration(milliseconds: 200),
|
||||||
bottom: composerSize.value,
|
child: FloatingActionButton.small(
|
||||||
child: IgnorePointer(
|
onPressed: scroll.jumpToBottom,
|
||||||
ignoring: scroll.atBottom,
|
child: const Icon(Icons.keyboard_arrow_down),
|
||||||
child: AnimatedOpacity(
|
),
|
||||||
opacity: scroll.atBottom ? 0 : 1,
|
),
|
||||||
duration: const Duration(milliseconds: 200),
|
),
|
||||||
child: FloatingActionButton.small(
|
|
||||||
onPressed: scroll.jumpToBottom,
|
|
||||||
child: const Icon(Icons.keyboard_arrow_down),
|
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
),
|
bottom: 0,
|
||||||
),
|
left: 0,
|
||||||
Positioned(
|
right: 0,
|
||||||
bottom: 0,
|
child: MeasureSize(
|
||||||
left: 0,
|
onChange: (size) =>
|
||||||
right: 0,
|
composerSize.value = size.height,
|
||||||
child: MeasureSize(
|
child: Composer(
|
||||||
onChange: (size) => composerSize.value = size.height,
|
roomId,
|
||||||
child: Composer(
|
node: composerNode,
|
||||||
roomId,
|
onSend:
|
||||||
node: composerNode,
|
(
|
||||||
onSend:
|
text, {
|
||||||
(text, {required shouldMention, required tags}) =>
|
required shouldMention,
|
||||||
notifier
|
required tags,
|
||||||
.send(
|
}) => notifier
|
||||||
text,
|
.send(
|
||||||
tags: tags,
|
text,
|
||||||
relationType: relationType.value,
|
tags: tags,
|
||||||
shouldMention: shouldMention,
|
relationType: relationType.value,
|
||||||
relatedEventRowId: relatedEvent.value,
|
shouldMention: shouldMention,
|
||||||
)
|
relatedEventRowId: relatedEvent.value,
|
||||||
.onError(showError),
|
)
|
||||||
relationType: relationType.value,
|
.onError(showError),
|
||||||
relatedEvent: relatedEvent.value,
|
relationType: relationType.value,
|
||||||
onDismiss: () {
|
relatedEvent: relatedEvent.value,
|
||||||
relatedEvent.value = null;
|
onDismiss: () {
|
||||||
composerNode.requestFocus();
|
relatedEvent.value = null;
|
||||||
},
|
composerNode.requestFocus();
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (memberListOpened && showMembersByDefault)
|
||||||
|
MemberList(roomId),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (memberListOpened && showMembersByDefault) MemberList(roomId),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
85
lib/widgets/sync_status_banner.dart
Normal file
85
lib/widgets/sync_status_banner.dart
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/sync_status.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:timeago/timeago.dart";
|
||||||
|
|
||||||
|
class const SyncStatusBanner({super.key}) extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final status = ref.watch(SyncStatusController.provider);
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
final banner = switch (status?.type) {
|
||||||
|
null || .ok => null,
|
||||||
|
.waiting => (
|
||||||
|
title: "Waiting for sync...",
|
||||||
|
icon: Icons.sync,
|
||||||
|
background: colorScheme.primary,
|
||||||
|
foreground: colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
.erroring => (
|
||||||
|
title: "Sync is failing, retrying...",
|
||||||
|
icon: Icons.sync_problem,
|
||||||
|
background: colorScheme.error,
|
||||||
|
foreground: colorScheme.onError,
|
||||||
|
),
|
||||||
|
.permanentlyFailed => (
|
||||||
|
title: "Sync has permanently failed",
|
||||||
|
icon: Icons.cloud_off,
|
||||||
|
background: colorScheme.error,
|
||||||
|
foreground: colorScheme.onError,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
final details = status == null
|
||||||
|
? null
|
||||||
|
: [
|
||||||
|
?status.error,
|
||||||
|
if (status.errorCount > 0) "Failed attempts: ${status.errorCount}.",
|
||||||
|
if (status.lastSync case final lastSync?)
|
||||||
|
"Last synced ${format(lastSync)}.",
|
||||||
|
];
|
||||||
|
|
||||||
|
return AnimatedSize(
|
||||||
|
duration: const .new(milliseconds: 200),
|
||||||
|
alignment: .topCenter,
|
||||||
|
child: banner == null
|
||||||
|
? const SizedBox(width: double.infinity)
|
||||||
|
: Material(
|
||||||
|
color: banner.background,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: details == null
|
||||||
|
? null
|
||||||
|
: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ErrorDialog(details.join("\n\n"), null),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const .symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
Icon(banner.icon, size: 20, color: banner.foreground),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
banner.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: banner.foreground,
|
||||||
|
fontWeight: .bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (details != null)
|
||||||
|
Icon(Icons.info_outline, color: banner.foreground),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue