add syncing waiting / error / failed indicator

This commit is contained in:
Henry Hiles 2026-10-14 23:21:31 -04:00
commit 07700c1eff
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 186 additions and 75 deletions

View file

@ -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");
} }

View file

@ -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);

View file

@ -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)

View file

@ -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,12 +58,19 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
), ),
], ],
) )
: Center( : Column(
children: [
const SyncStatusBanner(),
Expanded(
child: Center(
child: Column( child: Column(
mainAxisSize: .min, mainAxisSize: .min,
children: [Loading(), Text("Syncing...")], children: [Loading(), Text("Syncing...")],
), ),
), ),
),
],
),
drawer: isDesktop || !initComplete ? null : Sidebar(), drawer: isDesktop || !initComplete ? null : Sidebar(),
), ),
); );

View file

@ -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,7 +205,11 @@ final class const RoomChat({
}, },
onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer, onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer,
), ),
body: Row( body: Column(
children: [
const SyncStatusBanner(),
Expanded(
child: Row(
children: [ children: [
Expanded( Expanded(
child: Stack( child: Stack(
@ -243,13 +253,17 @@ final class const RoomChat({
left: 0, left: 0,
right: 0, right: 0,
child: MeasureSize( child: MeasureSize(
onChange: (size) => composerSize.value = size.height, onChange: (size) =>
composerSize.value = size.height,
child: Composer( child: Composer(
roomId, roomId,
node: composerNode, node: composerNode,
onSend: onSend:
(text, {required shouldMention, required tags}) => (
notifier text, {
required shouldMention,
required tags,
}) => notifier
.send( .send(
text, text,
tags: tags, tags: tags,
@ -271,7 +285,11 @@ final class const RoomChat({
), ),
), ),
if (memberListOpened && showMembersByDefault) MemberList(roomId), if (memberListOpened && showMembersByDefault)
MemberList(roomId),
],
),
),
], ],
), ),
), ),

View 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),
],
),
),
),
),
);
}
}