Skip to content

Commit f53d20f

Browse files
committed
fix(sync): ignore stale cloud-audio fetches after delete
1 parent 22925b4 commit f53d20f

2 files changed

Lines changed: 79 additions & 9 deletions

File tree

app/lib/pages/conversations/private_cloud_sync_page.dart

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ import 'package:omi/utils/l10n_extensions.dart';
1515
import 'package:omi/utils/logger.dart';
1616

1717
class PrivateCloudSyncPage extends StatefulWidget {
18-
const PrivateCloudSyncPage({super.key});
18+
const PrivateCloudSyncPage({
19+
super.key,
20+
this.loadCloudAudioConversations,
21+
this.deleteAllCloudAudio,
22+
this.confirmDeleteOverride,
23+
});
24+
25+
final Future<List<CloudAudioConversation>> Function()? loadCloudAudioConversations;
26+
final Future<bool> Function()? deleteAllCloudAudio;
27+
final Future<bool?> Function(BuildContext context)? confirmDeleteOverride;
1928

2029
@override
2130
State<PrivateCloudSyncPage> createState() => _PrivateCloudSyncPageState();
@@ -30,6 +39,7 @@ class _PrivateCloudSyncPageState extends State<PrivateCloudSyncPage> {
3039
List<CloudAudioConversation> _conversations = [];
3140
bool? _lastPrivateCloudSyncEnabled;
3241
int _playbackGeneration = 0;
42+
int _cloudAudioRequestGeneration = 0;
3343

3444
final AudioPlayer _audioPlayer = AudioPlayer();
3545
StreamSubscription<PlayerState>? _playerStateSubscription;
@@ -77,10 +87,18 @@ class _PrivateCloudSyncPageState extends State<PrivateCloudSyncPage> {
7787
_currentPlayingConversationId = null;
7888
_isAudioLoading = false;
7989
if (clearConversations) {
90+
_invalidateCloudAudioRequests(clearLoading: true);
8091
_conversations = [];
8192
}
8293
}
8394

95+
void _invalidateCloudAudioRequests({bool clearLoading = false}) {
96+
_cloudAudioRequestGeneration++;
97+
if (clearLoading) {
98+
_isLoadingAudio = false;
99+
}
100+
}
101+
84102
bool _shouldAbortPlaybackStart(String conversationId, int playbackGeneration) {
85103
return !mounted ||
86104
_isDeleting ||
@@ -98,17 +116,18 @@ class _PrivateCloudSyncPageState extends State<PrivateCloudSyncPage> {
98116

99117
Future<void> _loadCloudAudioConversations() async {
100118
if (!mounted) return;
119+
final requestGeneration = ++_cloudAudioRequestGeneration;
101120
setState(() => _isLoadingAudio = true);
102121
try {
103-
final conversations = await getCloudAudioConversations();
104-
if (!mounted) return;
122+
final conversations = await (widget.loadCloudAudioConversations ?? getCloudAudioConversations)();
123+
if (!mounted || requestGeneration != _cloudAudioRequestGeneration) return;
105124
setState(() {
106125
_conversations = conversations;
107126
_isLoadingAudio = false;
108127
});
109128
} catch (e) {
110129
Logger.debug('Error loading cloud audio conversations: $e');
111-
if (!mounted) return;
130+
if (!mounted || requestGeneration != _cloudAudioRequestGeneration) return;
112131
setState(() => _isLoadingAudio = false);
113132
}
114133
}
@@ -286,14 +305,17 @@ class _PrivateCloudSyncPageState extends State<PrivateCloudSyncPage> {
286305
}
287306

288307
Future<void> _deleteAllAudio() async {
289-
final confirmed = await _showDeleteAllDialog();
308+
final confirmed = await (widget.confirmDeleteOverride?.call(context) ?? _showDeleteAllDialog());
290309
if (confirmed != true) return;
291310

292-
setState(() => _isDeleting = true);
311+
setState(() {
312+
_isDeleting = true;
313+
_invalidateCloudAudioRequests(clearLoading: true);
314+
});
293315
_cancelPendingPlayback();
294316

295317
try {
296-
final success = await deleteAllCloudAudio();
318+
final success = await (widget.deleteAllCloudAudio ?? deleteAllCloudAudio)();
297319
if (!mounted) return;
298320
setState(() => _isDeleting = false);
299321
if (success) {

app/test/widgets/private_cloud_sync_page_test.dart

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_localizations/flutter_localizations.dart';
35
import 'package:flutter_test/flutter_test.dart';
46
import 'package:provider/provider.dart';
57
import 'package:shared_preferences/shared_preferences.dart';
68

79
import 'package:omi/backend/preferences.dart';
10+
import 'package:omi/backend/http/api/audio.dart';
811
import 'package:omi/l10n/app_localizations.dart';
912
import 'package:omi/pages/conversations/private_cloud_sync_page.dart';
1013
import 'package:omi/providers/user_provider.dart';
@@ -29,7 +32,13 @@ void main() {
2932
await SharedPreferencesUtil.init();
3033
});
3134

32-
Future<void> _pumpPage(WidgetTester tester, UserProvider userProvider) async {
35+
Future<void> _pumpPage(
36+
WidgetTester tester,
37+
UserProvider userProvider, {
38+
Future<List<CloudAudioConversation>> Function()? loadCloudAudioConversations,
39+
Future<bool> Function()? deleteAllCloudAudioOverride,
40+
Future<bool?> Function(BuildContext context)? confirmDeleteOverride,
41+
}) async {
3342
await tester.pumpWidget(
3443
ChangeNotifierProvider<UserProvider>.value(
3544
value: userProvider,
@@ -41,7 +50,11 @@ void main() {
4150
GlobalCupertinoLocalizations.delegate,
4251
],
4352
supportedLocales: AppLocalizations.supportedLocales,
44-
home: const PrivateCloudSyncPage(),
53+
home: PrivateCloudSyncPage(
54+
loadCloudAudioConversations: loadCloudAudioConversations ?? getCloudAudioConversations,
55+
deleteAllCloudAudio: deleteAllCloudAudioOverride ?? deleteAllCloudAudio,
56+
confirmDeleteOverride: confirmDeleteOverride,
57+
),
4558
),
4659
),
4760
);
@@ -60,4 +73,39 @@ void main() {
6073
expect(find.text(l10n.deleteAllAudio), findsOneWidget);
6174
expect(find.text(l10n.noCloudAudioFiles), findsOneWidget);
6275
});
76+
77+
testWidgets('ignores stale cloud-audio fetches after delete all succeeds', (tester) async {
78+
final userProvider = _StubUserProvider(enabled: true);
79+
final loadCompleter = Completer<List<CloudAudioConversation>>();
80+
addTearDown(userProvider.dispose);
81+
82+
await _pumpPage(
83+
tester,
84+
userProvider,
85+
loadCloudAudioConversations: () => loadCompleter.future,
86+
deleteAllCloudAudioOverride: () async => true,
87+
confirmDeleteOverride: (_) async => true,
88+
);
89+
90+
final context = tester.element(find.byType(PrivateCloudSyncPage));
91+
final l10n = AppLocalizations.of(context);
92+
93+
await tester.tap(find.text(l10n.deleteAllAudio));
94+
await tester.pump();
95+
await tester.pump();
96+
97+
loadCompleter.complete([
98+
CloudAudioConversation(
99+
id: 'conv-1',
100+
title: 'Phantom conversation',
101+
audioFileCount: 1,
102+
totalDuration: 42,
103+
),
104+
]);
105+
await tester.pump();
106+
await tester.pump();
107+
108+
expect(find.text('Phantom conversation'), findsNothing);
109+
expect(find.text(l10n.noCloudAudioFiles), findsOneWidget);
110+
});
63111
}

0 commit comments

Comments
 (0)