Implemented per stream subscription recovery with polling fallback

This commit is contained in:
2026-03-01 17:24:04 +08:00
parent e91e7b43d2
commit c9479f01f0
19 changed files with 894 additions and 494 deletions
+7 -38
View File
@@ -13,6 +13,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/reconnect_overlay.dart';
import '../../providers/realtime_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
import '../../theme/app_surfaces.dart';
@@ -353,7 +354,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
return ResponsiveBody(
child: Skeletonizer(
enabled: realtime.isConnecting,
enabled: realtime.isAnyStreamRecovering,
child: LayoutBuilder(
builder: (context, constraints) {
final sections = <Widget>[
@@ -449,43 +450,11 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
),
),
if (realtime.isConnecting)
Positioned.fill(
child: AbsorbPointer(
absorbing: true,
child: Container(
color: Theme.of(
context,
).colorScheme.surface.withAlpha((0.35 * 255).round()),
alignment: Alignment.topCenter,
padding: const EdgeInsets.only(top: 36),
child: SizedBox(
width: 280,
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 12),
Expanded(
child: Text('Reconnecting realtime…'),
),
],
),
),
),
),
),
),
if (realtime.isAnyStreamRecovering)
const Positioned(
bottom: 16,
right: 16,
child: ReconnectIndicator(),
),
],
);
+32 -23
View File
@@ -239,7 +239,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final realtime = ref.watch(realtimeControllerProvider);
final isRetrieving =
realtime.isConnecting ||
realtime.isAnyStreamRecovering ||
tasksAsync.isLoading ||
ticketsAsync.isLoading ||
officesAsync.isLoading ||
@@ -2684,37 +2684,46 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final typingController = _maybeTypingController(typingChannelId);
typingController?.stopTyping();
final message = await ref
.read(ticketsControllerProvider)
.sendTaskMessage(
taskId: task.id,
ticketId: task.ticketId,
content: content,
);
// Capture mentioned user ids and clear the composer immediately so the
// UI does not block while the network call completes. Perform the send
// and mention notification creation in a background Future.
final mentionUserIds = _extractMentionedUserIds(
content,
profiles,
currentUserId,
);
if (mentionUserIds.isNotEmpty && currentUserId != null) {
await ref
.read(notificationsControllerProvider)
.createMentionNotifications(
userIds: mentionUserIds,
actorId: currentUserId,
ticketId: task.ticketId,
taskId: task.id,
messageId: message.id,
);
}
ref.invalidate(taskMessagesProvider(task.id));
if (task.ticketId != null) {
ref.invalidate(ticketMessagesProvider(task.ticketId!));
}
if (mounted) {
_messageController.clear();
_clearMentions();
}
Future(() async {
try {
final message = await ref
.read(ticketsControllerProvider)
.sendTaskMessage(
taskId: task.id,
ticketId: task.ticketId,
content: content,
);
if (mentionUserIds.isNotEmpty && currentUserId != null) {
try {
await ref
.read(notificationsControllerProvider)
.createMentionNotifications(
userIds: mentionUserIds,
actorId: currentUserId,
ticketId: task.ticketId,
taskId: task.id,
messageId: message.id,
);
} catch (_) {}
}
} catch (e, st) {
debugPrint('sendTaskMessage error: $e\n$st');
}
});
}
void _handleComposerChanged(
+2 -2
View File
@@ -104,7 +104,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
final realtime = ref.watch(realtimeControllerProvider);
final showSkeleton =
realtime.isConnecting ||
realtime.isAnyStreamRecovering ||
tasksAsync.maybeWhen(loading: () => true, orElse: () => false) ||
ticketsAsync.maybeWhen(loading: () => true, orElse: () => false) ||
officesAsync.maybeWhen(loading: () => true, orElse: () => false) ||
@@ -534,7 +534,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
),
),
),
const ReconnectOverlay(),
const ReconnectIndicator(),
],
);
}
+26 -14
View File
@@ -528,29 +528,41 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
_maybeTypingController(widget.ticketId)?.stopTyping();
final message = await ref
.read(ticketsControllerProvider)
.sendTicketMessage(ticketId: widget.ticketId, content: content);
// Capture mentions and clear the composer immediately so the UI
// remains snappy. Perform the network send and notification creation
// in a fire-and-forget background Future.
final mentionUserIds = _extractMentionedUserIds(
content,
profiles,
currentUserId,
);
if (mentionUserIds.isNotEmpty && currentUserId != null) {
await ref
.read(notificationsControllerProvider)
.createMentionNotifications(
userIds: mentionUserIds,
actorId: currentUserId,
ticketId: widget.ticketId,
messageId: message.id,
);
}
ref.invalidate(ticketMessagesProvider(widget.ticketId));
if (mounted) {
_messageController.clear();
_clearMentions();
}
Future(() async {
try {
final message = await ref
.read(ticketsControllerProvider)
.sendTicketMessage(ticketId: widget.ticketId, content: content);
if (mentionUserIds.isNotEmpty && currentUserId != null) {
try {
await ref
.read(notificationsControllerProvider)
.createMentionNotifications(
userIds: mentionUserIds,
actorId: currentUserId,
ticketId: widget.ticketId,
messageId: message.id,
);
} catch (_) {}
}
} catch (e, st) {
debugPrint('sendTicketMessage error: $e\n$st');
}
});
}
List<String> _extractMentionedUserIds(
+2 -2
View File
@@ -64,7 +64,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
final profilesAsync = ref.watch(profilesProvider);
final showSkeleton =
realtime.isConnecting ||
realtime.isAnyStreamRecovering ||
ticketsAsync.maybeWhen(loading: () => true, orElse: () => false) ||
officesAsync.maybeWhen(loading: () => true, orElse: () => false) ||
profilesAsync.maybeWhen(loading: () => true, orElse: () => false) ||
@@ -347,7 +347,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
),
),
),
const ReconnectOverlay(),
const ReconnectIndicator(),
],
);
}