Implemented per stream subscription recovery with polling fallback
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user