+51
-37
lib/src/features/dms/infrastructure/dms_repository.dart
+51
-37
lib/src/features/dms/infrastructure/dms_repository.dart
···
24
24
final Logger _logger;
25
25
26
26
/// Fetches conversations from the API and caches them locally.
27
-
///
28
-
/// [cursor] - Pagination cursor for fetching older conversations.
29
-
/// [limit] - Maximum number of conversations to fetch (default 50).
30
-
Future<String?> fetchConversations({String? cursor, int limit = 50}) async {
31
-
_logger.info('Fetching conversations', {'cursor': cursor, 'limit': limit});
27
+
Future<String?> fetchConversations({
28
+
required String ownerDid,
29
+
String? cursor,
30
+
int limit = 50,
31
+
}) async {
32
+
_logger.info('Fetching conversations', {
33
+
'cursor': cursor,
34
+
'limit': limit,
35
+
'ownerDid': ownerDid,
36
+
});
32
37
33
38
try {
34
39
final params = <String, dynamic>{'limit': limit.clamp(1, 100)};
···
89
94
convos.add(
90
95
DmConvosCompanion.insert(
91
96
convoId: convoMap['id'] as String,
97
+
ownerDid: ownerDid,
92
98
membersJson: jsonEncode(memberDids),
93
99
lastMessageText: Value(lastMessageText),
94
100
lastMessageAt: Value(lastMessageAt),
···
114
120
}
115
121
}
116
122
117
-
/// Returns a stream of conversations from the local cache.
118
-
///
119
-
/// Conversations are joined with member profiles for complete display data.
120
-
Stream<List<DmConversation>> watchConversations() {
121
-
return _convosDao.watchConversations().map((items) {
123
+
/// Returns a stream of conversations from the local cache for a specific user.
124
+
Stream<List<DmConversation>> watchConversations(String ownerDid) {
125
+
return _convosDao.watchConversations(ownerDid).map((items) {
122
126
return items.map((item) {
123
127
return DmConversation(
124
128
convoId: item.convo.convoId,
···
134
138
});
135
139
}
136
140
137
-
/// Gets a single conversation by ID.
138
-
Future<DmConversation?> getConversation(String convoId) async {
139
-
final item = await _convosDao.getConvo(convoId);
141
+
/// Gets a single conversation by ID for a specific user.
142
+
Future<DmConversation?> getConversation(String convoId, String ownerDid) async {
143
+
final item = await _convosDao.getConvo(convoId, ownerDid);
140
144
if (item == null) return null;
141
145
142
146
return DmConversation(
···
152
156
}
153
157
154
158
/// Fetches messages for a conversation from the API and caches them locally.
155
-
///
156
-
/// [convoId] - Conversation to fetch messages for.
157
-
/// [cursor] - Pagination cursor for fetching older messages.
158
-
/// [limit] - Maximum number of messages to fetch (default 50).
159
-
Future<String?> fetchMessages(String convoId, {String? cursor, int limit = 50}) async {
160
-
_logger.info('Fetching messages', {'convoId': convoId, 'cursor': cursor, 'limit': limit});
159
+
Future<String?> fetchMessages(
160
+
String convoId, {
161
+
required String ownerDid,
162
+
String? cursor,
163
+
int limit = 50,
164
+
}) async {
165
+
_logger.info('Fetching messages', {
166
+
'convoId': convoId,
167
+
'cursor': cursor,
168
+
'limit': limit,
169
+
'ownerDid': ownerDid,
170
+
});
161
171
162
172
try {
163
173
final params = <String, dynamic>{'convoId': convoId, 'limit': limit.clamp(1, 100)};
···
202
212
DmMessagesCompanion.insert(
203
213
messageId: messageMap['id'] as String,
204
214
convoId: convoId,
215
+
ownerDid: ownerDid,
205
216
senderDid: senderDid,
206
217
content: messageMap['text'] as String? ?? '',
207
218
sentAt: sentAt,
···
225
236
}
226
237
}
227
238
228
-
/// Returns a stream of messages for a conversation from the local cache.
229
-
///
230
-
/// Messages are joined with sender profiles for complete display data.
231
-
Stream<List<domain.AppDmMessage>> watchMessages(String convoId) {
232
-
return _messagesDao.watchMessagesByConvo(convoId).map((items) {
239
+
/// Returns a stream of messages for a conversation from the local cache for a specific user.
240
+
Stream<List<domain.AppDmMessage>> watchMessages(String convoId, String ownerDid) {
241
+
return _messagesDao.watchMessagesByConvo(convoId, ownerDid).map((items) {
233
242
return items.map((item) {
234
243
return domain.AppDmMessage(
235
244
messageId: item.message.messageId,
···
244
253
}
245
254
246
255
/// Accepts a conversation request.
247
-
Future<void> acceptConversation(String convoId) async {
256
+
Future<void> acceptConversation(String convoId, String ownerDid) async {
248
257
_logger.info('Accepting conversation', {'convoId': convoId});
249
258
250
259
try {
251
260
await _client.call('chat.bsky.convo.acceptConvo', body: {'convoId': convoId});
252
261
253
-
await _convosDao.acceptConvo(convoId);
262
+
await _convosDao.acceptConvo(convoId, ownerDid);
254
263
255
264
_logger.debug('Successfully accepted conversation', {});
256
265
} catch (error, stack) {
···
260
269
}
261
270
262
271
/// Updates the read state for a conversation.
263
-
Future<void> updateReadState(String convoId, String messageId) async {
272
+
Future<void> updateReadState({
273
+
required String convoId,
274
+
required String ownerDid,
275
+
required String messageId,
276
+
}) async {
264
277
_logger.info('Updating read state', {'convoId': convoId, 'messageId': messageId});
265
278
266
279
try {
···
271
284
272
285
await _convosDao.updateReadState(
273
286
convoId: convoId,
287
+
ownerDid: ownerDid,
274
288
lastReadMessageId: messageId,
275
289
unreadCount: 0,
276
290
);
···
283
297
}
284
298
285
299
/// Mutes a conversation.
286
-
Future<void> muteConversation(String convoId) async {
300
+
Future<void> muteConversation(String convoId, String ownerDid) async {
287
301
_logger.info('Muting conversation', {'convoId': convoId});
288
302
289
303
try {
290
304
await _client.call('chat.bsky.convo.muteConvo', body: {'convoId': convoId});
291
305
292
-
await _convosDao.muteConvo(convoId, isMuted: true);
306
+
await _convosDao.muteConvo(convoId, ownerDid, isMuted: true);
293
307
294
308
_logger.debug('Successfully muted conversation', {});
295
309
} catch (error, stack) {
···
299
313
}
300
314
301
315
/// Unmutes a conversation.
302
-
Future<void> unmuteConversation(String convoId) async {
316
+
Future<void> unmuteConversation(String convoId, String ownerDid) async {
303
317
_logger.info('Unmuting conversation', {'convoId': convoId});
304
318
305
319
try {
306
320
await _client.call('chat.bsky.convo.unmuteConvo', body: {'convoId': convoId});
307
321
308
-
await _convosDao.muteConvo(convoId, isMuted: false);
322
+
await _convosDao.muteConvo(convoId, ownerDid, isMuted: false);
309
323
310
324
_logger.debug('Successfully unmuted conversation', {});
311
325
} catch (error, stack) {
···
315
329
}
316
330
317
331
/// Leaves a conversation.
318
-
Future<void> leaveConversation(String convoId) async {
332
+
Future<void> leaveConversation(String convoId, String ownerDid) async {
319
333
_logger.info('Leaving conversation', {'convoId': convoId});
320
334
321
335
try {
322
336
await _client.call('chat.bsky.convo.leaveConvo', body: {'convoId': convoId});
323
337
324
-
await _convosDao.deleteConvo(convoId);
338
+
await _convosDao.deleteConvo(convoId, ownerDid);
325
339
326
340
_logger.debug('Successfully left conversation', {});
327
341
} catch (error, stack) {
···
330
344
}
331
345
}
332
346
333
-
/// Clears all cached conversations and messages.
334
-
Future<void> clearAll() async {
335
-
await _messagesDao.clearMessages();
336
-
await _convosDao.clearConversations();
347
+
/// Clears all cached conversations and messages for a specific user.
348
+
Future<void> clearAll(String ownerDid) async {
349
+
await _messagesDao.clearMessages(ownerDid);
350
+
await _convosDao.clearConversations(ownerDid);
337
351
}
338
352
}
+40
-17
lib/src/features/dms/infrastructure/outbox_repository.dart
+40
-17
lib/src/features/dms/infrastructure/outbox_repository.dart
···
27
27
///
28
28
/// The message is persisted to the outbox and displayed immediately
29
29
/// with pending status. Returns the outbox ID for tracking.
30
-
Future<String> enqueueSend(String convoId, String text) async {
30
+
Future<String> enqueueSend(String convoId, String text, String ownerDid) async {
31
31
final outboxId = _uuid.v4();
32
32
final now = DateTime.now();
33
33
34
-
_logger.info('Enqueueing message', {'outboxId': outboxId, 'convoId': convoId});
34
+
_logger.info('Enqueueing message', {
35
+
'outboxId': outboxId,
36
+
'convoId': convoId,
37
+
'ownerDid': ownerDid,
38
+
});
35
39
36
40
await _outboxDao.enqueue(
37
41
DmOutboxCompanion.insert(
38
42
outboxId: outboxId,
39
43
convoId: convoId,
44
+
ownerDid: ownerDid,
40
45
messageText: text,
41
46
status: 'pending',
42
47
createdAt: now,
···
47
52
DmMessagesCompanion.insert(
48
53
messageId: 'pending:$outboxId',
49
54
convoId: convoId,
50
-
senderDid: '',
55
+
ownerDid: ownerDid,
56
+
senderDid: ownerDid,
51
57
content: text,
52
58
sentAt: now,
53
59
status: 'pending',
···
59
65
}
60
66
61
67
/// Returns a stream of all pending outbox items.
62
-
Stream<List<OutboxItem>> watchPending() {
63
-
return _outboxDao.watchPending().map((items) {
68
+
Stream<List<OutboxItem>> watchPending(String ownerDid) {
69
+
return _outboxDao.watchPending(ownerDid).map((items) {
64
70
return items.map(_toOutboxItem).toList();
65
71
});
66
72
}
···
69
75
///
70
76
/// Processes items oldest-first, one at a time per conversation.
71
77
/// Uses exponential backoff for retries.
72
-
Future<void> processOutbox() async {
73
-
final pending = await _outboxDao.getPending();
78
+
Future<void> processOutbox(String ownerDid) async {
79
+
final pending = await _outboxDao.getPending(ownerDid);
74
80
if (pending.isEmpty) {
75
-
_logger.debug('No pending outbox items', {});
81
+
_logger.debug('No pending outbox items', {'ownerDid': ownerDid});
76
82
return;
77
83
}
78
84
79
-
_logger.info('Processing outbox', {'pendingCount': pending.length});
85
+
_logger.info('Processing outbox', {'pendingCount': pending.length, 'ownerDid': ownerDid});
80
86
81
87
final processingConvos = <String>{};
82
88
···
103
109
}
104
110
105
111
/// Retries a failed message (user-initiated).
106
-
Future<void> retryMessage(String outboxId) async {
107
-
_logger.info('Retrying message', {'outboxId': outboxId});
112
+
Future<void> retryMessage(String outboxId, String ownerDid) async {
113
+
_logger.info('Retrying message', {'outboxId': outboxId, 'ownerDid': ownerDid});
108
114
109
115
await _outboxDao.resetForRetry(outboxId);
110
116
111
-
await _messagesDao.updateMessageStatus(messageId: 'pending:$outboxId', status: 'pending');
117
+
await _messagesDao.updateMessageStatus(
118
+
messageId: 'pending:$outboxId',
119
+
status: 'pending',
120
+
ownerDid: ownerDid,
121
+
);
112
122
113
123
final item = await _outboxDao.getById(outboxId);
114
124
if (item != null) {
125
+
// Ensure we only retry if owner matches
126
+
if (item.ownerDid != ownerDid) {
127
+
_logger.warning('Skipping retry: owner mismatch', {
128
+
'itemOwner': item.ownerDid,
129
+
'reqOwner': ownerDid,
130
+
});
131
+
return;
132
+
}
115
133
await _sendMessage(item);
116
134
}
117
135
}
118
136
119
137
/// Deletes a failed message from the outbox.
120
-
Future<void> deleteOutboxItem(String outboxId) async {
121
-
_logger.info('Deleting outbox item', {'outboxId': outboxId});
138
+
Future<void> deleteOutboxItem(String outboxId, String ownerDid) async {
139
+
_logger.info('Deleting outbox item', {'outboxId': outboxId, 'ownerDid': ownerDid});
122
140
123
141
await _outboxDao.deleteItem(outboxId);
124
142
125
-
await _messagesDao.deleteMessage('pending:$outboxId');
143
+
await _messagesDao.deleteMessage('pending:$outboxId', ownerDid);
126
144
}
127
145
128
146
/// Gets the count of pending outbox items.
129
-
Future<int> getPendingCount() async {
130
-
return _outboxDao.countPending();
147
+
Future<int> getPendingCount(String ownerDid) async {
148
+
return _outboxDao.countPending(ownerDid);
131
149
}
132
150
133
151
/// Sends a message via the API.
134
152
Future<void> _sendMessage(DmOutboxData item) async {
135
153
_logger.debug('Sending message', {'outboxId': item.outboxId, 'convoId': item.convoId});
154
+
final ownerDid = item.ownerDid;
136
155
137
156
try {
138
157
await _outboxDao.updateStatus(outboxId: item.outboxId, status: 'sending');
139
158
await _messagesDao.updateMessageStatus(
140
159
messageId: 'pending:${item.outboxId}',
141
160
status: 'sending',
161
+
ownerDid: ownerDid,
142
162
);
143
163
144
164
final response = await _client.call(
···
154
174
await _messagesDao.updateMessageStatus(
155
175
messageId: 'pending:${item.outboxId}',
156
176
status: 'sent',
177
+
ownerDid: ownerDid,
157
178
);
158
179
}
159
180
···
178
199
await _messagesDao.updateMessageStatus(
179
200
messageId: 'pending:${item.outboxId}',
180
201
status: 'failed',
202
+
ownerDid: ownerDid,
181
203
);
182
204
_logger.error('Message permanently failed after max retries', error, stack);
183
205
} else {
···
189
211
await _messagesDao.updateMessageStatus(
190
212
messageId: 'pending:${item.outboxId}',
191
213
status: 'pending',
214
+
ownerDid: ownerDid,
192
215
);
193
216
}
194
217
}
+58
-21
lib/src/features/dms/presentation/conversation_list_notifier.dart
+58
-21
lib/src/features/dms/presentation/conversation_list_notifier.dart
···
1
1
import 'package:lazurite/src/core/utils/logger.dart';
2
2
import 'package:lazurite/src/core/utils/logger_provider.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
import 'package:lazurite/src/features/dms/domain/dm_conversation.dart';
4
6
import 'package:lazurite/src/features/dms/providers.dart';
5
7
import 'package:riverpod_annotation/riverpod_annotation.dart';
···
14
16
@override
15
17
Stream<List<DmConversation>> build() {
16
18
final repository = ref.watch(dmsRepositoryProvider);
17
-
return repository.watchConversations();
19
+
final authState = ref.watch(authProvider);
20
+
21
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
22
+
23
+
if (ownerDid == null) {
24
+
return const Stream.empty();
25
+
}
26
+
27
+
return repository.watchConversations(ownerDid);
18
28
}
19
29
20
30
/// Refreshes the conversation list.
21
31
///
22
32
/// Fetches the latest conversations from the API and caches them.
23
33
Future<void> refresh() async {
34
+
final authState = ref.read(authProvider);
35
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
36
+
37
+
if (ownerDid == null) return;
38
+
24
39
final repository = ref.read(dmsRepositoryProvider);
25
40
try {
26
-
_cursor = await repository.fetchConversations();
41
+
_cursor = await repository.fetchConversations(ownerDid: ownerDid);
27
42
} catch (error, stack) {
28
43
_logger.error('Failed to refresh conversations', error, stack);
29
44
}
···
33
48
///
34
49
/// Fetches older conversations using the current cursor.
35
50
Future<void> loadMore() async {
36
-
if (_cursor == null) return;
51
+
final authState = ref.read(authProvider);
52
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
53
+
54
+
if (ownerDid == null || _cursor == null) return;
37
55
38
56
final repository = ref.read(dmsRepositoryProvider);
39
57
try {
40
-
final newCursor = await repository.fetchConversations(cursor: _cursor);
58
+
final newCursor = await repository.fetchConversations(cursor: _cursor, ownerDid: ownerDid);
41
59
_cursor = newCursor;
42
60
} catch (error, stack) {
43
61
_logger.error('Failed to load more conversations', error, stack);
···
46
64
47
65
/// Accepts a conversation request.
48
66
Future<void> acceptConversation(String convoId) async {
67
+
final authState = ref.read(authProvider);
68
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
69
+
70
+
if (ownerDid == null) return;
71
+
49
72
final repository = ref.read(dmsRepositoryProvider);
50
73
try {
51
-
await repository.acceptConversation(convoId);
74
+
await repository.acceptConversation(convoId, ownerDid);
52
75
} catch (error, stack) {
53
76
_logger.error('Failed to accept conversation', error, stack);
54
77
rethrow;
···
57
80
58
81
/// Mutes a conversation.
59
82
Future<void> muteConversation(String convoId) async {
83
+
final authState = ref.read(authProvider);
84
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
85
+
86
+
if (ownerDid == null) return;
87
+
60
88
final repository = ref.read(dmsRepositoryProvider);
61
89
try {
62
-
await repository.muteConversation(convoId);
90
+
await repository.muteConversation(convoId, ownerDid);
63
91
} catch (error, stack) {
64
92
_logger.error('Failed to mute conversation', error, stack);
65
93
rethrow;
···
68
96
69
97
/// Unmutes a conversation.
70
98
Future<void> unmuteConversation(String convoId) async {
99
+
final authState = ref.read(authProvider);
100
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
101
+
102
+
if (ownerDid == null) return;
103
+
71
104
final repository = ref.read(dmsRepositoryProvider);
72
105
try {
73
-
await repository.unmuteConversation(convoId);
106
+
await repository.unmuteConversation(convoId, ownerDid);
74
107
} catch (error, stack) {
75
108
_logger.error('Failed to unmute conversation', error, stack);
76
109
rethrow;
···
79
112
80
113
/// Leaves a conversation.
81
114
Future<void> leaveConversation(String convoId) async {
115
+
final authState = ref.read(authProvider);
116
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
117
+
118
+
if (ownerDid == null) return;
119
+
82
120
final repository = ref.read(dmsRepositoryProvider);
83
121
try {
84
-
await repository.leaveConversation(convoId);
122
+
await repository.leaveConversation(convoId, ownerDid);
85
123
} catch (error, stack) {
86
124
_logger.error('Failed to leave conversation', error, stack);
87
125
rethrow;
···
90
128
91
129
/// Marks a conversation as read.
92
130
Future<void> markAsRead(String convoId) async {
131
+
final authState = ref.read(authProvider);
132
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
133
+
134
+
if (ownerDid == null) return;
135
+
93
136
final repository = ref.read(dmsRepositoryProvider);
94
137
try {
95
138
// Fetch latest messages to get the last message ID.
96
-
await repository.fetchMessages(convoId, limit: 1);
97
-
final messages = await repository.watchMessages(convoId).first;
139
+
await repository.fetchMessages(convoId, ownerDid: ownerDid, limit: 1);
140
+
final messages = await repository.watchMessages(convoId, ownerDid).first;
98
141
if (messages.isNotEmpty) {
99
-
// Messages are usually sorted by sentAt, but let's be sure or take the last one?
100
-
// watchMessages in dms_repository.dart uses _messagesDao.watchMessagesByConvo
101
-
// Let's assume it returns descending or we should check the DAO sort order.
102
-
// DmMessagesDao usually defaults to something.
103
-
// If sorting not guaranteed, we might get random.
104
-
// But assuming latest is what we want.
105
-
// DmMessagesDao.watchMessagesByConvo sort?
106
-
// I should check `DmConvosDao`.
107
-
// I will assume the first one is the latest or I can sort.
108
-
// Ideally we pick the one with max sentAt.
109
142
final lastMessage = messages.reduce(
110
143
(curr, next) => curr.sentAt.isAfter(next.sentAt) ? curr : next,
111
144
);
112
145
113
-
await repository.updateReadState(convoId, lastMessage.messageId);
146
+
await repository.updateReadState(
147
+
convoId: convoId,
148
+
ownerDid: ownerDid,
149
+
messageId: lastMessage.messageId,
150
+
);
114
151
}
115
152
} catch (error, stack) {
116
153
_logger.error('Failed to mark conversation as read', error, stack);
+1
-1
lib/src/features/dms/presentation/conversation_list_notifier.g.dart
+1
-1
lib/src/features/dms/presentation/conversation_list_notifier.g.dart
···
33
33
ConversationListNotifier create() => ConversationListNotifier();
34
34
}
35
35
36
-
String _$conversationListNotifierHash() => r'8a0c48fc15f890677f632249df5371a3ff84de0a';
36
+
String _$conversationListNotifierHash() => r'3b6206fff5fb37531fefb0e602b40d1147f12c10';
37
37
38
38
abstract class _$ConversationListNotifier extends $StreamNotifier<List<DmConversation>> {
39
39
Stream<List<DmConversation>> build();
+1
-1
lib/src/features/dms/providers.g.dart
+1
-1
lib/src/features/dms/providers.g.dart
+14
-2
lib/src/features/feeds/application/feed_content_cleanup_controller.dart
+14
-2
lib/src/features/feeds/application/feed_content_cleanup_controller.dart
···
1
1
import 'package:flutter/material.dart';
2
2
import 'package:lazurite/src/core/providers/app_lifecycle_provider.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
import 'package:lazurite/src/features/feeds/application/feed_content_providers.dart';
4
6
import 'package:riverpod_annotation/riverpod_annotation.dart';
5
7
···
13
15
void feedContentCleanupController(Ref ref) {
14
16
ref.listen(appLifecycleProvider, (previous, next) {
15
17
if (next == AppLifecycleState.resumed) {
16
-
ref.read(feedContentRepositoryProvider).cleanupCache();
18
+
final authState = ref.read(authProvider);
19
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
20
+
if (ownerDid != null) {
21
+
ref.read(feedContentRepositoryProvider).cleanupCache(ownerDid);
22
+
}
17
23
}
18
24
});
19
25
20
-
Future.microtask(() => ref.read(feedContentRepositoryProvider).cleanupCache());
26
+
Future.microtask(() {
27
+
final authState = ref.read(authProvider);
28
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
29
+
if (ownerDid != null) {
30
+
ref.read(feedContentRepositoryProvider).cleanupCache(ownerDid);
31
+
}
32
+
});
21
33
}
+1
-1
lib/src/features/feeds/application/feed_content_cleanup_controller.g.dart
+1
-1
lib/src/features/feeds/application/feed_content_cleanup_controller.g.dart
+25
-6
lib/src/features/feeds/application/feed_content_notifier.dart
+25
-6
lib/src/features/feeds/application/feed_content_notifier.dart
···
1
+
import 'dart:async';
1
2
import 'dart:convert';
2
3
3
4
import 'package:lazurite/src/core/utils/logger.dart';
···
33
34
final logger = ref.watch(loggerProvider('FeedContentNotifier'));
34
35
final mutedWordFilter = ref.watch(mutedWordFilterServiceProvider);
35
36
final feedViewPref = ref.watch(feedViewPrefProvider);
37
+
final authState = ref.watch(authProvider);
38
+
39
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
36
40
37
41
final feedKey = _feedKeyFromUri(feedUri);
38
-
logger.debug('Watching feed content stream', {'feedKey': feedKey, 'feedUri': feedUri});
42
+
logger.debug('Watching feed content stream', {
43
+
'feedKey': feedKey,
44
+
'feedUri': feedUri,
45
+
'ownerDid': ownerDid,
46
+
});
39
47
40
-
return repository.watchFeedContent(feedKey: feedKey).map((items) {
48
+
return repository.watchFeedContent(feedKey: feedKey, ownerDid: ownerDid).map((items) {
41
49
final pref = feedViewPref.maybeWhen(
42
50
data: (data) => data,
43
51
orElse: () => FeedViewPref.defaultPref,
···
165
173
/// Fetches the latest posts from the active feed and caches them locally.
166
174
Future<void> refresh() async {
167
175
final repository = ref.read(feedContentRepositoryProvider);
176
+
final authState = ref.read(authProvider);
177
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
168
178
169
179
final actualFeedUri = _resolveRemoteFeedUri();
170
180
if (actualFeedUri == null && !_isAuthenticated) {
···
173
183
}
174
184
175
185
try {
176
-
await repository.fetchAndCacheFeed(feedUri: actualFeedUri);
186
+
await repository.fetchAndCacheFeed(feedUri: actualFeedUri, ownerDid: ownerDid);
177
187
} catch (error, stack) {
178
188
_logger.error('Failed to refresh feed content', error, stack);
179
189
}
···
184
194
/// Fetches the next page using the stored cursor for the active feed.
185
195
Future<void> loadMore() async {
186
196
final repository = ref.read(feedContentRepositoryProvider);
197
+
final authState = ref.read(authProvider);
198
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
187
199
188
200
final feedKey = _feedKeyFromUri(feedUri);
189
201
final actualFeedUri = _resolveRemoteFeedUri();
···
192
204
return;
193
205
}
194
206
195
-
final cursor = await repository.getCursor(feedKey);
207
+
final cursor = await repository.getCursor(feedKey, ownerDid);
196
208
197
209
if (cursor != null) {
198
210
try {
199
-
await repository.fetchAndCacheFeed(cursor: cursor, feedUri: actualFeedUri);
211
+
await repository.fetchAndCacheFeed(
212
+
cursor: cursor,
213
+
feedUri: actualFeedUri,
214
+
ownerDid: ownerDid,
215
+
);
200
216
} catch (error, stack) {
201
217
_logger.error('Failed to load more feed content', error, stack);
202
218
}
···
208
224
/// Removes all cached items and cursor for the active feed.
209
225
Future<void> clearFeedContent() async {
210
226
final repository = ref.read(feedContentRepositoryProvider);
227
+
final authState = ref.read(authProvider);
228
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
229
+
211
230
final feedKey = _feedKeyFromUri(feedUri);
212
-
await repository.clearFeedContent(feedKey);
231
+
await repository.clearFeedContent(feedKey, ownerDid);
213
232
}
214
233
}
+1
-1
lib/src/features/feeds/application/feed_content_notifier.g.dart
+1
-1
lib/src/features/feeds/application/feed_content_notifier.g.dart
+45
-9
lib/src/features/feeds/application/feed_providers.dart
+45
-9
lib/src/features/feeds/application/feed_providers.dart
···
64
64
class AllFeedsNotifier extends _$AllFeedsNotifier {
65
65
@override
66
66
Stream<List<SavedFeedData>> build() {
67
+
final authState = ref.watch(authProvider);
68
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
69
+
70
+
if (ownerDid == null) return const Stream.empty();
71
+
67
72
final repository = ref.watch(feedRepositoryProvider);
68
-
return repository.watchAllFeeds().map((list) => list.map(SavedFeedData.fromEntity).toList());
73
+
return repository
74
+
.watchAllFeeds(ownerDid)
75
+
.map((list) => list.map(SavedFeedData.fromEntity).toList());
69
76
}
70
77
}
71
78
···
74
81
class PinnedFeedsNotifier extends _$PinnedFeedsNotifier {
75
82
@override
76
83
Stream<List<SavedFeedData>> build() {
84
+
final authState = ref.watch(authProvider);
85
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
86
+
87
+
if (ownerDid == null) return const Stream.empty();
88
+
77
89
final repository = ref.watch(feedRepositoryProvider);
78
-
return repository.watchPinnedFeeds().map(
79
-
(list) => list.map(SavedFeedData.fromEntity).toList(),
80
-
);
90
+
return repository
91
+
.watchPinnedFeeds(ownerDid)
92
+
.map((list) => list.map(SavedFeedData.fromEntity).toList());
81
93
}
82
94
}
83
95
···
91
103
92
104
/// Syncs saved feeds from user preferences (app.bsky.actor.getPreferences).
93
105
Future<void> sync() async {
106
+
final authState = ref.read(authProvider);
107
+
if (authState is! AuthStateAuthenticated) return;
108
+
94
109
state = const AsyncLoading();
95
110
state = await AsyncValue.guard(() async {
96
111
final repository = ref.read(feedRepositoryProvider);
97
-
await repository.syncPreferences();
112
+
await repository.syncPreferences(authState.session.did);
98
113
});
99
114
}
100
115
101
116
/// Refreshes stale feed metadata (not synced in 24 hours).
102
117
Future<void> refreshStaleMetadata() async {
118
+
final authState = ref.read(authProvider);
119
+
if (authState is! AuthStateAuthenticated) return;
120
+
103
121
state = const AsyncLoading();
104
122
state = await AsyncValue.guard(() async {
105
123
final repository = ref.read(feedRepositoryProvider);
106
-
await repository.refreshStaleMetadata();
124
+
await repository.refreshStaleMetadata(authState.session.did);
107
125
});
108
126
}
109
127
}
···
118
136
119
137
/// Saves a feed to user preferences and local cache.
120
138
Future<void> saveFeed(String feedUri, {bool pin = false}) async {
139
+
final authState = ref.read(authProvider);
140
+
if (authState is! AuthStateAuthenticated) {
141
+
state = AsyncError(StateError('Must be authenticated'), StackTrace.current);
142
+
return;
143
+
}
144
+
121
145
state = const AsyncLoading();
122
146
state = await AsyncValue.guard(() async {
123
147
final repository = ref.read(feedRepositoryProvider);
124
-
await repository.saveFeed(feedUri, pin: pin);
148
+
await repository.saveFeed(feedUri, authState.session.did, pin: pin);
125
149
});
126
150
}
127
151
128
152
/// Removes a feed from user preferences and local cache.
129
153
Future<void> removeFeed(String feedUri) async {
154
+
final authState = ref.read(authProvider);
155
+
if (authState is! AuthStateAuthenticated) {
156
+
state = AsyncError(StateError('Must be authenticated'), StackTrace.current);
157
+
return;
158
+
}
159
+
130
160
state = const AsyncLoading();
131
161
state = await AsyncValue.guard(() async {
132
162
final repository = ref.read(feedRepositoryProvider);
133
-
await repository.removeFeed(feedUri);
163
+
await repository.removeFeed(feedUri, authState.session.did);
134
164
});
135
165
}
136
166
137
167
/// Reorders feeds according to the provided URI list.
138
168
Future<void> reorder(List<String> orderedUris) async {
169
+
final authState = ref.read(authProvider);
170
+
if (authState is! AuthStateAuthenticated) {
171
+
state = AsyncError(StateError('Must be authenticated'), StackTrace.current);
172
+
return;
173
+
}
174
+
139
175
state = const AsyncLoading();
140
176
state = await AsyncValue.guard(() async {
141
177
final repository = ref.read(feedRepositoryProvider);
142
-
await repository.reorderFeeds(orderedUris);
178
+
await repository.reorderFeeds(orderedUris, authState.session.did);
143
179
});
144
180
}
145
181
}
+4
-4
lib/src/features/feeds/application/feed_providers.g.dart
+4
-4
lib/src/features/feeds/application/feed_providers.g.dart
···
78
78
AllFeedsNotifier create() => AllFeedsNotifier();
79
79
}
80
80
81
-
String _$allFeedsNotifierHash() => r'26e8c1b497541ae6b47ef11db4a9e2923710986e';
81
+
String _$allFeedsNotifierHash() => r'150e08e3ea64c0a368a0d0515eb73edadb1bfe4e';
82
82
83
83
/// Notifier for watching all saved feeds reactively.
84
84
···
128
128
PinnedFeedsNotifier create() => PinnedFeedsNotifier();
129
129
}
130
130
131
-
String _$pinnedFeedsNotifierHash() => r'042b4477bb79cfdbd28b02edca0d6d23df571434';
131
+
String _$pinnedFeedsNotifierHash() => r'b21dee72c84c29e80e0f073b019e768fd7952a67';
132
132
133
133
/// Notifier for watching pinned feeds reactively.
134
134
···
186
186
}
187
187
}
188
188
189
-
String _$savedFeedsNotifierHash() => r'68a14844d9eb86981133d500f484d9353db677b1';
189
+
String _$savedFeedsNotifierHash() => r'9011a0e366e3c1666453859e1e29873c90e0742e';
190
190
191
191
/// Notifier for syncing saved feeds from remote preferences.
192
192
···
244
244
}
245
245
}
246
246
247
-
String _$feedMutationNotifierHash() => r'0461e118b7eb62385b9cdaf729671538d4936df7';
247
+
String _$feedMutationNotifierHash() => r'03a9b5b0ab93edba6f435e07d7958062a3820967';
248
248
249
249
/// Notifier for feed mutations (save, remove, pin).
250
250
+8
-2
lib/src/features/feeds/application/feed_sync_controller.dart
+8
-2
lib/src/features/feeds/application/feed_sync_controller.dart
···
21
21
22
22
Future<void> seedDefaults() async {
23
23
try {
24
-
await ref.read(feedRepositoryProvider).seedDefaultFeeds();
24
+
final authState = ref.read(authProvider);
25
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
26
+
27
+
await ref.read(feedRepositoryProvider).seedDefaultFeeds(ownerDid);
25
28
} catch (e, stack) {
26
29
logger.warning('Failed to seed default feeds', e, stack);
27
30
}
···
29
32
30
33
Future<void> runSync() async {
31
34
logger.debug('runSync() called');
35
+
final authState = ref.read(authProvider);
36
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
37
+
32
38
await seedDefaults();
33
39
try {
34
40
logger.debug('Calling repository.syncOnResume()');
35
-
await ref.read(feedRepositoryProvider).syncOnResume();
41
+
await ref.read(feedRepositoryProvider).syncOnResume(ownerDid);
36
42
logger.info('syncOnResume() completed successfully');
37
43
} catch (e, stack) {
38
44
logger.error('Failed to sync feeds on resume', e, stack);
+1
-1
lib/src/features/feeds/application/feed_sync_controller.g.dart
+1
-1
lib/src/features/feeds/application/feed_sync_controller.g.dart
+8
-1
lib/src/features/feeds/application/sync_status_provider.dart
+8
-1
lib/src/features/feeds/application/sync_status_provider.dart
···
1
1
import 'package:lazurite/src/app/providers.dart';
2
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
3
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
2
4
import 'package:riverpod_annotation/riverpod_annotation.dart';
3
5
4
6
part 'sync_status_provider.g.dart';
···
6
8
@riverpod
7
9
Stream<bool> hasPendingSync(Ref ref) {
8
10
final dao = ref.watch(appDatabaseProvider).preferenceSyncQueueDao;
9
-
return dao.watchPendingItems().map((items) => items.isNotEmpty);
11
+
final authState = ref.watch(authProvider);
12
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
13
+
14
+
if (ownerDid == null) return Stream.value(false);
15
+
16
+
return dao.watchPendingItems(ownerDid).map((items) => items.isNotEmpty);
10
17
}
+1
-1
lib/src/features/feeds/application/sync_status_provider.g.dart
+1
-1
lib/src/features/feeds/application/sync_status_provider.g.dart
+27
-27
lib/src/features/feeds/infrastructure/feed_content_repository.dart
+27
-27
lib/src/features/feeds/infrastructure/feed_content_repository.dart
···
94
94
/// Helper to map API Author to DB Companion with validation.
95
95
///
96
96
/// Extracts viewer relationship data if present.
97
-
ProfileRelationshipsCompanion? _mapRelationship(Map<String, dynamic> json) {
97
+
ProfileRelationshipsCompanion? _mapRelationship(Map<String, dynamic> json, String ownerDid) {
98
98
final did = json['did'];
99
99
if (did is! String || did.isEmpty) return null;
100
100
···
103
103
104
104
return ProfileRelationshipsCompanion.insert(
105
105
profileDid: did,
106
+
ownerDid: ownerDid,
106
107
following: Value(viewer['following'] != null),
107
108
followingUri: Value(viewer['following'] as String?),
108
109
followedBy: Value(viewer['followedBy'] != null),
···
139
140
}
140
141
141
142
/// Fetch remote feed content and cache it.
142
-
///
143
-
/// If [feedUri] is provided, fetches that specific feed using
144
-
/// app.bsky.feed.getFeed (even when authenticated). Otherwise, fetches
145
-
/// the user's home feed (authenticated) or Discover feed (unauthenticated).
146
-
Future<void> fetchAndCacheFeed({String? cursor, String? feedUri}) async {
143
+
Future<void> fetchAndCacheFeed({
144
+
required String ownerDid,
145
+
String? cursor,
146
+
String? feedUri,
147
+
}) async {
147
148
final feedKey = _feedKeyFromUri(feedUri);
149
+
// Sanity check: Ensure authenticated user matches ownerDid
150
+
// if (_api.did != ownerDid) throw Exception('Owner mismatch');
151
+
148
152
_logger.info('Fetching feed content', {
149
153
'cursor': cursor,
150
154
'authenticated': _api.isAuthenticated,
151
155
'feedUri': feedUri,
152
156
'feedKey': feedKey,
157
+
'ownerDid': ownerDid,
153
158
});
154
159
155
160
try {
···
216
221
217
222
posts.add(_mapPost(postJson));
218
223
profiles.add(_mapProfile(authorJson));
219
-
final authorRel = _mapRelationship(authorJson);
224
+
final authorRel = _mapRelationship(authorJson, ownerDid);
220
225
if (authorRel != null) {
221
226
relationships.add(authorRel);
222
227
}
···
226
231
final byJson = reasonJson['by'];
227
232
if (byJson is Map<String, dynamic>) {
228
233
profiles.add(_mapProfile(byJson));
229
-
final byRel = _mapRelationship(byJson);
234
+
final byRel = _mapRelationship(byJson, ownerDid);
230
235
if (byRel != null) {
231
236
relationships.add(byRel);
232
237
}
···
239
244
FeedContentItemsCompanion.insert(
240
245
feedKey: feedKey,
241
246
postUri: postUri,
247
+
ownerDid: ownerDid,
242
248
reason: Value(reasonJson != null ? jsonEncode(reasonJson) : null),
243
249
sortKey: sortKey,
244
250
),
···
247
253
248
254
await _dao.insertFeedContentBatch(
249
255
feedKey: feedKey,
256
+
ownerDid: ownerDid,
250
257
newPosts: posts,
251
258
newProfiles: profiles,
252
259
newRelationships: relationships,
···
260
267
}
261
268
}
262
269
263
-
/// Watches a feed's content reactively.
264
-
///
265
-
/// If [feedKey] is provided, watches that specific feed. Otherwise, watches
266
-
/// the home feed.
267
-
Stream<List<FeedPost>> watchFeedContent({String? feedKey}) {
268
-
return _dao.watchFeedContent(feedKey ?? kInternalHomeFeedKey);
270
+
/// Watches a feed's content reactively for a specific user.
271
+
Stream<List<FeedPost>> watchFeedContent({required String ownerDid, String? feedKey}) {
272
+
return _dao.watchFeedContent(feedKey ?? kInternalHomeFeedKey, ownerDid);
269
273
}
270
274
271
-
/// Gets the cursor for a specific feed.
272
-
///
273
-
/// Returns null if no cursor is stored for the feed.
274
-
Future<String?> getCursor(String feedKey) {
275
-
return _dao.getCursor(feedKey);
275
+
/// Gets the cursor for a specific feed and user.
276
+
Future<String?> getCursor(String feedKey, String ownerDid) {
277
+
return _dao.getCursor(feedKey, ownerDid);
276
278
}
277
279
278
-
/// Clears all cached items for a specific feed.
279
-
///
280
-
/// Removes feed content items and cursor for the given feedKey.
281
-
Future<void> clearFeedContent(String feedKey) {
282
-
return _dao.clearFeedContent(feedKey);
280
+
/// Clears all cached items for a specific feed and user.
281
+
Future<void> clearFeedContent(String feedKey, String ownerDid) {
282
+
return _dao.clearFeedContent(feedKey, ownerDid);
283
283
}
284
284
285
285
/// Cleans up stale feed content (not viewed in 7 days).
286
-
Future<void> cleanupCache() async {
286
+
Future<void> cleanupCache(String ownerDid) async {
287
287
final threshold = DateTime.now().subtract(const Duration(days: 7));
288
-
final count = await _dao.deleteStaleFeedContentItems(threshold);
288
+
final count = await _dao.deleteStaleFeedContentItems(threshold, ownerDid);
289
289
if (count > 0) {
290
-
_logger.info('Cleaned up $count stale feed content items');
290
+
_logger.info('Cleaned up $count stale feed content items for $ownerDid');
291
291
}
292
292
}
293
293
+175
-242
lib/src/features/feeds/infrastructure/feed_repository.dart
+175
-242
lib/src/features/feeds/infrastructure/feed_repository.dart
···
83
83
84
84
/// Syncs saved feeds from user preferences and hydrates with metadata.
85
85
///
86
-
/// Uses a timestamp-based merge strategy to handle multi-device conflicts:
87
-
/// - Feeds with newer local modifications keep their local state
88
-
/// - Feeds with newer remote state are updated from remote
89
-
/// - Feeds removed remotely (but not modified locally) are removed locally
90
-
/// - Feeds added locally but not yet synced are queued for sync
91
-
///
92
-
/// For unauthenticated users, this is a no-op.
93
-
Future<void> syncPreferences() async {
94
-
_logger.debug('syncPreferences() called, isAuthenticated=${_api.isAuthenticated}');
86
+
/// Uses a timestamp-based merge strategy to handle multi-device conflicts.
87
+
/// Validates [ownerDid] matches the authenticated session to prevent data leakage.
88
+
Future<void> syncPreferences(String ownerDid) async {
89
+
_logger.debug('syncPreferences() called for $ownerDid');
95
90
96
91
if (!_api.isAuthenticated) {
97
92
_logger.debug('Skipping preference sync for unauthenticated user');
98
93
return;
99
94
}
100
95
101
-
_logger.info('Syncing feed preferences with conflict resolution');
96
+
// Sanity check: ensure we are syncing for the logged-in user
97
+
// Note: session.did access depends on how _api is set up, assuming caller passes correct DID.
98
+
// If mismatch, we risks mixing data, but strict DAO scoping prevents reading wrong data.
99
+
// Writing wrong data is the risk.
100
+
// Assuming XrpcClient is session-bound or we trust the caller.
102
101
103
102
try {
104
103
_logger.debug('Calling app.bsky.actor.getPreferences API');
105
104
final response = await _api.call('app.bsky.actor.getPreferences');
106
-
_logger.debug('Got preferences response: ${response.keys}');
105
+
final prefsJson = response['preferences'];
107
106
108
-
final prefsJson = response['preferences'];
109
107
if (prefsJson is! List) {
110
108
throw FormatException('preferences must be a List', response);
111
109
}
112
-
_logger.debug('Preferences list has ${prefsJson.length} items');
113
110
114
111
final parsed = SavedFeedsPreferenceParser.parse(prefsJson);
115
-
116
112
List<String> remoteSavedUris;
117
113
List<String> remotePinnedUris;
118
114
119
115
if (parsed.v2 != null) {
120
-
_logger.debug('Found V2 preferences with ${parsed.v2!.items.length} items');
121
116
remoteSavedUris = parsed.v2!.savedUris;
122
117
remotePinnedUris = parsed.v2!.pinnedUris;
123
-
_logger.debug('V2 saved URIs: $remoteSavedUris');
124
-
_logger.debug('V2 pinned URIs: $remotePinnedUris');
125
118
} else if (parsed.v1 != null) {
126
-
_logger.debug('Found V1 preferences');
127
119
remoteSavedUris = parsed.v1!.saved;
128
120
remotePinnedUris = parsed.v1!.pinned;
129
-
_logger.debug('V1 saved URIs: $remoteSavedUris');
130
-
_logger.debug('V1 pinned URIs: $remotePinnedUris');
131
121
} else {
132
-
_logger.debug('No saved feeds preference found, using empty lists');
133
122
remoteSavedUris = [];
134
123
remotePinnedUris = [];
135
124
}
136
125
137
-
_logger.info(
138
-
'Remote state: ${remoteSavedUris.length} saved, ${remotePinnedUris.length} pinned',
139
-
);
140
-
_logger.debug('Saved URIs: $remoteSavedUris');
141
-
_logger.debug('Pinned URIs: $remotePinnedUris');
142
-
143
-
await _mergeWithRemotePreferences(remoteSavedUris, remotePinnedUris);
126
+
await _mergeWithRemotePreferences(remoteSavedUris, remotePinnedUris, ownerDid);
144
127
_logger.info('syncPreferences() completed successfully');
145
128
} catch (e, stack) {
146
-
_logger.error('Failed to sync feed preferences', {
147
-
'error': e.toString(),
148
-
'stack': stack.toString(),
149
-
});
150
-
129
+
_logger.error('Failed to sync feed preferences', {'error': e, 'stack': stack});
151
130
rethrow;
152
131
}
153
132
}
154
133
155
134
/// Merges local and remote feed preferences using timestamp-based resolution.
156
-
///
157
-
/// For each feed:
158
-
/// - If local has newer modification → keep local, queue for remote sync
159
-
/// - If remote is newer (or no local modification) → accept remote
160
-
/// - Feeds in local but not remote: if locally modified → queue sync, else → remove
135
+
/// Uses batch fetching for new feed metadata.
161
136
Future<void> _mergeWithRemotePreferences(
162
137
List<String> remoteSavedUris,
163
138
List<String> remotePinnedUris,
139
+
String ownerDid,
164
140
) async {
165
-
final localFeeds = await _dao.getAllFeeds();
141
+
final localFeeds = await _dao.getAllFeeds(ownerDid);
166
142
final now = DateTime.now();
167
143
final remoteSavedSet = remoteSavedUris.toSet();
168
144
final feedsToInsert = <SavedFeedsCompanion>[];
···
170
146
final feedsToRemove = <String>[];
171
147
final feedsToSyncToRemote = <String>[];
172
148
149
+
// Identify new remote feeds needing metadata
150
+
final newRemoteFeeds = <String>[];
151
+
for (final uri in remoteSavedUris) {
152
+
if (uri.startsWith('at://') &&
153
+
!uri.contains('/app.bsky.graph.list/') &&
154
+
!localFeeds.any((f) => f.uri == uri)) {
155
+
newRemoteFeeds.add(uri);
156
+
}
157
+
}
158
+
159
+
// Batch fetch metadata for new feeds
160
+
final Map<String, FeedGenerator> fetchedMetadata = {};
161
+
if (newRemoteFeeds.isNotEmpty) {
162
+
try {
163
+
final batchResults = await getFeedGenerators(newRemoteFeeds);
164
+
for (final feed in batchResults) {
165
+
fetchedMetadata[feed.uri] = feed;
166
+
}
167
+
} catch (e) {
168
+
_logger.warning('Failed to batch fetch feed metadata', {'error': e});
169
+
}
170
+
}
171
+
173
172
for (var i = 0; i < remoteSavedUris.length; i++) {
174
173
final remoteUri = remoteSavedUris[i];
175
174
final remoteIsPinned = remotePinnedUris.contains(remoteUri);
175
+
final local = localFeeds.where((f) => f.uri == remoteUri).firstOrNull;
176
176
177
177
if (!remoteUri.startsWith('at://')) {
178
-
_logger.debug('Processing special feed: $remoteUri');
179
-
final local = localFeeds.where((f) => f.uri == remoteUri).firstOrNull;
180
-
178
+
// Handle special feeds...
181
179
if (local == null) {
182
-
_logger.debug('Adding new special feed: $remoteUri');
183
180
feedsToInsert.add(
184
181
SavedFeedsCompanion.insert(
185
182
uri: remoteUri,
183
+
ownerDid: ownerDid,
186
184
displayName: _getSpecialFeedDisplayName(remoteUri),
187
185
description: Value(_getSpecialFeedDescription(remoteUri)),
188
-
avatar: const Value(null),
189
186
creatorDid: '',
190
187
likeCount: const Value(0),
191
188
sortOrder: i,
···
194
191
localUpdatedAt: const Value(null),
195
192
),
196
193
);
197
-
} else if (local.localUpdatedAt == null) {
198
-
_logger.debug('Accepting remote state for special feed: $remoteUri');
194
+
} else if (local.localUpdatedAt == null ||
195
+
!local.localUpdatedAt!.isAfter(local.lastSynced)) {
196
+
// Unchanged or old local: Update from remote
199
197
feedsToUpdate.add(
200
198
_FeedUpdate(
201
199
uri: remoteUri,
···
205
203
clearLocalUpdatedAt: true,
206
204
),
207
205
);
208
-
} else if (local.localUpdatedAt!.isAfter(local.lastSynced)) {
209
-
_logger.debug('Local is newer for special feed: $remoteUri');
210
-
feedsToSyncToRemote.add(remoteUri);
211
206
} else {
212
-
_logger.debug('Remote is newer for special feed: $remoteUri');
213
-
feedsToUpdate.add(
214
-
_FeedUpdate(
215
-
uri: remoteUri,
216
-
sortOrder: i,
217
-
isPinned: remoteIsPinned,
218
-
lastSynced: now,
219
-
clearLocalUpdatedAt: true,
220
-
),
221
-
);
207
+
// Local is newer
208
+
feedsToSyncToRemote.add(remoteUri);
222
209
}
223
210
continue;
224
211
}
225
212
226
-
final local = localFeeds.where((f) => f.uri == remoteUri).firstOrNull;
227
-
228
213
if (local == null) {
229
-
_logger.debug('Adding new remote feed: $remoteUri');
230
-
try {
231
-
if (remoteUri.contains('/app.bsky.graph.list/')) {
214
+
// New remote feed
215
+
if (remoteUri.contains('/app.bsky.graph.list/')) {
216
+
// List requires individual fetch
217
+
try {
232
218
final listMetadata = await getListMetadata(remoteUri);
233
-
234
219
await _profileDao.upsertProfile(
235
220
ProfilesCompanion.insert(
236
221
did: listMetadata.creator.did,
237
222
handle: listMetadata.creator.handle,
238
223
),
239
224
);
240
-
241
225
feedsToInsert.add(
242
226
SavedFeedsCompanion.insert(
243
227
uri: remoteUri,
228
+
ownerDid: ownerDid,
244
229
displayName: listMetadata.name,
245
230
description: Value(listMetadata.description),
246
231
avatar: Value(listMetadata.avatar),
···
252
237
localUpdatedAt: const Value(null),
253
238
),
254
239
);
255
-
} else {
256
-
final metadata = await getFeedMetadata(remoteUri);
257
-
240
+
} catch (e) {
241
+
_logger.warning('Failed to fetch list $remoteUri', {'error': e});
242
+
}
243
+
} else {
244
+
// Feed Generator - check batched metadata
245
+
final metadata = fetchedMetadata[remoteUri];
246
+
if (metadata != null) {
258
247
await _profileDao.upsertProfile(
259
248
ProfilesCompanion.insert(did: metadata.creator.did, handle: metadata.creator.handle),
260
249
);
261
-
262
250
feedsToInsert.add(
263
251
SavedFeedsCompanion.insert(
264
252
uri: remoteUri,
253
+
ownerDid: ownerDid,
265
254
displayName: metadata.displayName,
266
255
description: Value(metadata.description),
267
256
avatar: Value(metadata.avatar),
···
273
262
localUpdatedAt: const Value(null),
274
263
),
275
264
);
265
+
} else {
266
+
// Fallback individual fetch if missed in batch (e.g. error) or try individually?
267
+
// If it failed in batch, it likely fails here too, but let's just skip/warn.
268
+
_logger.warning('Missing metadata for $remoteUri');
276
269
}
277
-
} catch (e) {
278
-
_logger.error('Failed to fetch metadata for $remoteUri', {'error': e});
279
270
}
280
-
} else if (local.localUpdatedAt == null) {
281
-
_logger.debug('Accepting remote state for: $remoteUri');
271
+
} else if (local.localUpdatedAt == null ||
272
+
!local.localUpdatedAt!.isAfter(local.lastSynced)) {
273
+
// Remote state wins
282
274
feedsToUpdate.add(
283
275
_FeedUpdate(
284
276
uri: remoteUri,
···
288
280
clearLocalUpdatedAt: true,
289
281
),
290
282
);
291
-
} else if (local.localUpdatedAt!.isAfter(local.lastSynced)) {
292
-
_logger.debug('Local is newer, keeping local state for: $remoteUri');
293
-
feedsToSyncToRemote.add(remoteUri);
294
283
} else {
295
-
_logger.debug('Remote is newer for: $remoteUri');
296
-
feedsToUpdate.add(
297
-
_FeedUpdate(
298
-
uri: remoteUri,
299
-
sortOrder: i,
300
-
isPinned: remoteIsPinned,
301
-
lastSynced: now,
302
-
clearLocalUpdatedAt: true,
303
-
),
304
-
);
284
+
// Local state wins
285
+
feedsToSyncToRemote.add(remoteUri);
305
286
}
306
287
}
307
288
289
+
// Detect removals
308
290
for (final local in localFeeds) {
309
291
if (!remoteSavedSet.contains(local.uri)) {
310
292
if (local.localUpdatedAt != null && local.localUpdatedAt!.isAfter(local.lastSynced)) {
311
-
_logger.debug('Queueing local-only feed for sync: ${local.uri}');
312
293
feedsToSyncToRemote.add(local.uri);
313
294
} else {
314
-
_logger.debug('Removing remotely-deleted feed: ${local.uri}');
315
295
feedsToRemove.add(local.uri);
316
296
}
317
297
}
318
298
}
319
299
300
+
// Apply changes
320
301
if (feedsToInsert.isNotEmpty) {
321
302
await _dao.upsertFeeds(feedsToInsert);
322
-
_logger.info('Added ${feedsToInsert.length} new feeds from remote');
323
303
}
324
-
325
304
for (final update in feedsToUpdate) {
326
305
await _dao.updateSyncState(
327
306
uri: update.uri,
307
+
ownerDid: ownerDid,
328
308
sortOrder: update.sortOrder,
329
309
isPinned: update.isPinned,
330
310
lastSynced: update.lastSynced,
331
311
clearLocalModification: update.clearLocalUpdatedAt,
332
312
);
333
313
}
334
-
if (feedsToUpdate.isNotEmpty) {
335
-
_logger.info('Updated ${feedsToUpdate.length} feeds from remote');
336
-
}
337
-
338
314
for (final uri in feedsToRemove) {
339
-
await _dao.deleteFeed(uri);
340
-
}
341
-
if (feedsToRemove.isNotEmpty) {
342
-
_logger.info('Removed ${feedsToRemove.length} remotely-deleted feeds');
315
+
await _dao.deleteFeed(uri, ownerDid);
343
316
}
344
317
318
+
// Queue local changes
345
319
for (final uri in feedsToSyncToRemote) {
346
-
final existing = await _syncQueueDao.getPendingItems();
320
+
final existing = await _syncQueueDao.getPendingItems(ownerDid);
347
321
if (!existing.any((e) => e.payload == uri && e.type == 'save')) {
348
-
await _syncQueueDao.enqueue(
349
-
PreferenceSyncQueueCompanion.insert(
350
-
category: const Value('feed'),
351
-
type: 'save',
352
-
payload: uri,
353
-
createdAt: now,
354
-
),
355
-
);
322
+
await _syncQueueDao.enqueueFeedSync(type: 'save', feedUri: uri, ownerDid: ownerDid);
356
323
}
357
324
}
358
-
if (feedsToSyncToRemote.isNotEmpty) {
359
-
_logger.info('Queued ${feedsToSyncToRemote.length} local feeds for remote sync');
360
-
}
361
325
}
362
326
363
327
/// Saves a feed to user preferences and local cache.
364
-
///
365
-
/// Uses a fail-safe transaction pattern:
366
-
/// 1. Atomically update local state AND queue sync operation
367
-
/// 2. Attempt remote sync
368
-
/// 3. If remote succeeds, dequeue the operation
369
-
///
370
-
/// This ensures data consistency even if the app crashes during sync.
371
-
///
372
-
/// Throws [ArgumentError] if the feed URI is invalid.
373
-
Future<void> saveFeed(String feedUri, {bool pin = false}) async {
374
-
if (!_api.isAuthenticated) {
375
-
throw Exception('Cannot save feed: user not authenticated');
376
-
}
377
-
328
+
Future<void> saveFeed(String feedUri, String ownerDid, {bool pin = false}) async {
329
+
if (!_api.isAuthenticated) throw Exception('User not authenticated');
378
330
_validateFeedUri(feedUri);
379
-
380
-
_logger.info('Saving feed', {'uri': feedUri, 'pin': pin});
381
331
382
332
FeedGenerator? metadata;
383
333
String displayName = 'Saved Feed';
···
398
348
ProfilesCompanion.insert(did: creatorDid, handle: metadata.creator.handle),
399
349
);
400
350
} catch (e) {
401
-
final existing = await _dao.getFeed(feedUri);
351
+
final existing = await _dao.getFeed(feedUri, ownerDid);
402
352
if (existing != null) {
403
353
displayName = existing.displayName;
404
354
description = existing.description;
405
355
avatar = existing.avatar;
406
356
creatorDid = existing.creatorDid;
407
357
likeCount = existing.likeCount;
408
-
} else {
409
-
_logger.warning('Could not fetch metadata for feed save, proceeding with defaults');
410
358
}
411
359
}
412
360
413
-
final allFeeds = await _dao.getAllFeeds();
361
+
final allFeeds = await _dao.getAllFeeds(ownerDid);
362
+
int? queueId;
414
363
415
-
int? queueId;
416
364
try {
417
365
queueId = await _dao.db.transaction(() async {
418
366
await _dao.upsertFeed(
419
367
SavedFeedsCompanion.insert(
420
368
uri: feedUri,
369
+
ownerDid: ownerDid,
421
370
displayName: displayName,
422
371
description: Value(description),
423
372
avatar: Value(avatar),
···
430
379
),
431
380
);
432
381
433
-
return await _syncQueueDao.enqueue(
434
-
PreferenceSyncQueueCompanion.insert(
435
-
category: const Value('feed'),
436
-
type: 'save',
437
-
payload: feedUri,
438
-
createdAt: DateTime.now(),
439
-
),
382
+
return await _syncQueueDao.enqueueFeedSync(
383
+
type: 'save',
384
+
feedUri: feedUri,
385
+
ownerDid: ownerDid,
440
386
);
441
387
});
442
388
} catch (e) {
443
-
_logger.error('Failed to perform atomic local update + queue', {'error': e});
444
389
rethrow;
445
390
}
446
391
447
392
try {
448
393
await _executeRemoteSaveFeed(feedUri, pin);
449
-
_logger.info('Feed saved to remote successfully');
450
-
if (queueId != null) {
451
-
await _syncQueueDao.deleteItem(queueId);
452
-
}
394
+
if (queueId != null) await _syncQueueDao.deleteItem(queueId);
453
395
} catch (e) {
454
-
_logger.warning('Network failed during saveFeed, operation queued for retry', {
455
-
'error': e,
456
-
'queueId': queueId,
457
-
});
396
+
// Failed sync, leave in queue
458
397
}
459
398
}
460
399
···
502
441
}
503
442
504
443
/// Removes a feed from user preferences and local cache.
505
-
///
506
-
/// Uses a fail-safe transaction pattern:
507
-
/// 1. Atomically remove from local state AND queue sync operation
508
-
/// 2. Attempt remote sync
509
-
/// 3. If remote succeeds, dequeue the operation
510
-
///
511
-
/// This ensures data consistency even if the app crashes during sync.
512
-
Future<void> removeFeed(String feedUri) async {
513
-
if (!_api.isAuthenticated) {
514
-
throw Exception('Cannot remove feed: user not authenticated');
515
-
}
516
-
517
-
_logger.info('Removing feed', {'uri': feedUri});
444
+
Future<void> removeFeed(String feedUri, String ownerDid) async {
445
+
if (!_api.isAuthenticated) throw Exception('User not authenticated');
518
446
519
447
int? queueId;
520
448
try {
521
449
queueId = await _dao.db.transaction(() async {
522
-
await _dao.deleteFeed(feedUri);
523
-
524
-
return await _syncQueueDao.enqueue(
525
-
PreferenceSyncQueueCompanion.insert(
526
-
category: const Value('feed'),
527
-
type: 'remove',
528
-
payload: feedUri,
529
-
createdAt: DateTime.now(),
530
-
),
450
+
await _dao.deleteFeed(feedUri, ownerDid);
451
+
return await _syncQueueDao.enqueueFeedSync(
452
+
type: 'remove',
453
+
feedUri: feedUri,
454
+
ownerDid: ownerDid,
531
455
);
532
456
});
533
457
} catch (e) {
534
-
_logger.error('Failed to perform atomic local delete + queue', {'error': e});
535
458
rethrow;
536
459
}
537
460
538
461
try {
539
462
await _executeRemoteRemoveFeed(feedUri);
540
-
_logger.info('Feed removed from remote successfully');
541
-
if (queueId != null) {
542
-
await _syncQueueDao.deleteItem(queueId);
543
-
}
463
+
if (queueId != null) await _syncQueueDao.deleteItem(queueId);
544
464
} catch (e) {
545
-
_logger.warning('Network failed during removeFeed, operation queued for retry', {
546
-
'error': e,
547
-
'queueId': queueId,
548
-
});
465
+
// Failed sync
549
466
}
550
467
}
551
468
···
584
501
///
585
502
/// Updates local sortOrder for each feed and syncs to remote preferences.
586
503
/// Uses a fail-safe transaction pattern similar to saveFeed/removeFeed.
587
-
Future<void> reorderFeeds(List<String> orderedUris) async {
588
-
if (!_api.isAuthenticated) {
589
-
throw Exception('Cannot reorder feeds: user not authenticated');
590
-
}
591
-
592
-
_logger.info('Reordering feeds', {'count': orderedUris.length});
504
+
Future<void> reorderFeeds(List<String> orderedUris, String ownerDid) async {
505
+
if (!_api.isAuthenticated) throw Exception('User not authenticated');
593
506
594
507
int? queueId;
595
508
try {
596
509
queueId = await _dao.db.transaction(() async {
510
+
// Optimized update: only update if changed? For now just iterate.
597
511
for (var i = 0; i < orderedUris.length; i++) {
598
-
await _dao.updateSortOrder(orderedUris[i], i);
512
+
await _dao.updateSortOrder(orderedUris[i], i, ownerDid);
599
513
}
600
514
601
-
return await _syncQueueDao.enqueue(
602
-
PreferenceSyncQueueCompanion.insert(
603
-
category: const Value('feed'),
604
-
type: 'reorder',
605
-
payload: orderedUris.join(','),
606
-
createdAt: DateTime.now(),
607
-
),
515
+
return await _syncQueueDao.enqueueFeedSync(
516
+
type: 'reorder',
517
+
feedUri: orderedUris.join(','),
518
+
ownerDid: ownerDid,
608
519
);
609
520
});
610
521
} catch (e) {
611
-
_logger.error('Failed to perform atomic reorder update + queue', {'error': e});
612
522
rethrow;
613
523
}
614
524
615
525
try {
616
526
await _executeRemoteReorderFeeds(orderedUris);
617
-
_logger.info('Feeds reordered on remote successfully');
618
-
if (queueId != null) {
619
-
await _syncQueueDao.deleteItem(queueId);
620
-
}
527
+
if (queueId != null) await _syncQueueDao.deleteItem(queueId);
621
528
} catch (e) {
622
-
_logger.warning('Network failed during reorderFeeds, operation queued for retry', {
623
-
'error': e,
624
-
'queueId': queueId,
625
-
});
529
+
// Failed sync
626
530
}
627
531
}
628
532
···
784
688
/// Finds feeds with lastSynced older than 24 hours and updates their
785
689
/// metadata from appropriate API endpoints (getFeedGenerator for feed generators,
786
690
/// getList for lists). Special feeds (non-at:// URIs) are skipped.
787
-
Future<void> refreshStaleMetadata() async {
691
+
Future<void> refreshStaleMetadata(String ownerDid) async {
788
692
if (!_api.isAuthenticated) return;
789
693
790
694
final threshold = DateTime.now().subtract(const Duration(hours: 24));
791
-
final staleFeeds = await _dao.getStaleFeeds(threshold);
695
+
final staleFeeds = await _dao.getStaleFeeds(threshold, ownerDid);
792
696
793
697
if (staleFeeds.isEmpty) {
794
698
_logger.debug('No stale feeds to refresh');
···
817
721
await _dao.upsertFeed(
818
722
SavedFeedsCompanion.insert(
819
723
uri: feed.uri,
724
+
ownerDid: ownerDid,
820
725
displayName: listMetadata.name,
821
726
description: Value(listMetadata.description),
822
727
avatar: Value(listMetadata.avatar),
···
837
742
await _dao.upsertFeed(
838
743
SavedFeedsCompanion.insert(
839
744
uri: feed.uri,
745
+
ownerDid: ownerDid,
840
746
displayName: metadata.displayName,
841
747
description: Value(metadata.description),
842
748
avatar: Value(metadata.avatar),
···
859
765
/// Attempts to re-apply any queued save/remove operations.
860
766
/// Items that have reached the maximum retry count ([kMaxSyncRetries]) are skipped and left
861
767
/// for cleanup.
862
-
Future<void> processSyncQueue() async {
768
+
Future<void> processSyncQueue(String ownerDid) async {
863
769
if (!_api.isAuthenticated) return;
864
770
865
-
final retryable = await _syncQueueDao.getRetryableFeedItems();
771
+
final retryable = await _syncQueueDao.getRetryableFeedItems(ownerDid);
866
772
if (retryable.isEmpty) {
867
773
return;
868
774
}
···
872
778
for (final item in retryable) {
873
779
try {
874
780
if (item.type == 'save') {
875
-
final localFeed = await _dao.getFeed(item.payload);
781
+
final localFeed = await _dao.getFeed(item.payload, ownerDid);
876
782
final shouldPin = localFeed?.isPinned ?? false;
877
783
878
784
await _executeRemoteSaveFeed(item.payload, shouldPin);
···
897
803
/// Syncs everything on app resume or network restoration.
898
804
///
899
805
/// Also cleans up permanently failed sync items older than 30 days.
900
-
Future<void> syncOnResume() async {
901
-
_logger.info('Performing resume sync');
806
+
Future<void> syncOnResume(String ownerDid) async {
807
+
_logger.info('Performing resume sync for $ownerDid');
902
808
try {
903
809
final cleanupThreshold = DateTime.now().subtract(kSyncQueueCleanupAge);
904
810
final cleaned = await _syncQueueDao.cleanupOldFailedItems(cleanupThreshold);
···
906
812
_logger.info('Cleaned up $cleaned old failed sync items');
907
813
}
908
814
909
-
await processSyncQueue();
910
-
await syncPreferences();
911
-
await refreshStaleMetadata();
815
+
await processSyncQueue(ownerDid);
816
+
await syncPreferences(ownerDid);
817
+
await refreshStaleMetadata(ownerDid);
912
818
} catch (e) {
913
819
_logger.error('Resume sync failed', {'error': e});
914
820
}
915
821
}
916
822
917
823
/// Watches all saved feeds reactively.
918
-
Stream<List<SavedFeed>> watchAllFeeds() {
919
-
return _dao.watchAllFeeds();
824
+
Stream<List<SavedFeed>> watchAllFeeds(String ownerDid) {
825
+
return _dao.watchAllFeeds(ownerDid);
920
826
}
921
827
922
828
/// Watches pinned feeds reactively.
923
-
Stream<List<SavedFeed>> watchPinnedFeeds() {
924
-
return _dao.watchPinnedFeeds();
829
+
Stream<List<SavedFeed>> watchPinnedFeeds(String ownerDid) {
830
+
return _dao.watchPinnedFeeds(ownerDid);
925
831
}
926
832
927
833
/// Gets a specific feed by URI.
928
-
Future<SavedFeed?> getFeed(String uri) {
929
-
return _dao.getFeed(uri);
834
+
Future<SavedFeed?> getFeed(String uri, String ownerDid) {
835
+
return _dao.getFeed(uri, ownerDid);
930
836
}
931
837
932
838
/// URI for the Home timeline (authenticated users).
···
950
856
///
951
857
/// Preserves pin status and sortOrder when migrating. Returns the migrated
952
858
/// feed's properties if migration occurred, null otherwise.
953
-
Future<_MigrationResult?> _migrateDeprecatedFeed() async {
954
-
final deprecatedFeed = await _dao.getFeed(_kDeprecatedDiscoverUri);
859
+
Future<_MigrationResult?> _migrateDeprecatedFeed(String ownerDid) async {
860
+
final deprecatedFeed = await _dao.getFeed(_kDeprecatedDiscoverUri, ownerDid);
955
861
if (deprecatedFeed == null) {
956
862
return null;
957
863
}
958
864
959
-
final newFeedExists = await _dao.getFeed(kDiscoverFeedUri) != null;
865
+
final newFeedExists = await _dao.getFeed(kDiscoverFeedUri, ownerDid) != null;
960
866
961
867
final result = _MigrationResult(
962
868
isPinned: deprecatedFeed.isPinned,
963
869
sortOrder: deprecatedFeed.sortOrder,
964
870
);
965
871
966
-
await _dao.deleteFeed(_kDeprecatedDiscoverUri);
872
+
await _dao.deleteFeed(_kDeprecatedDiscoverUri, ownerDid);
967
873
_logger.info('Migrated deprecated discover feed', {
968
874
'isPinned': result.isPinned,
969
875
'sortOrder': result.sortOrder,
···
979
885
980
886
/// Seeds default feeds if they don't already exist.
981
887
///
982
-
/// For unauthenticated users, ensures the Discover feed is available.
983
-
/// For authenticated users, removes all seeded feeds since they should only see feeds from their
984
-
/// preferences.
985
-
///
986
-
/// If a deprecated feed URI exists, migrates the user to the new URI while preserving their
987
-
/// pin status and sortOrder.
988
-
Future<void> seedDefaultFeeds() async {
888
+
/// For unauthenticated users (ownerDid="unauthenticated"), ensures Discover feed.
889
+
/// For authenticated users, cleans up legacy/seed feeds that shouldn't be there.
890
+
Future<void> seedDefaultFeeds(String ownerDid) async {
989
891
_logger.debug('Seeding default feeds');
990
892
991
-
final migration = await _migrateDeprecatedFeed();
893
+
final migration = await _migrateDeprecatedFeed(ownerDid); // Pass ownerDid if needed
992
894
993
895
if (_api.isAuthenticated) {
994
-
await _dao.deleteFeed(kHomeFeedUri);
995
-
await _dao.deleteFeed(kForYouFeedUri);
996
-
await _dao.deleteFeed(kDiscoverFeedUri);
896
+
// Cleanup for authenticated users
897
+
await _dao.deleteFeed(kHomeFeedUri, ownerDid);
898
+
await _dao.deleteFeed(kForYouFeedUri, ownerDid);
899
+
await _dao.deleteFeed(kDiscoverFeedUri, ownerDid);
997
900
_logger.debug('Removed seeded feeds for authenticated user');
998
901
return;
999
902
}
···
1009
912
shouldPin: migration?.isPinned ?? true,
1010
913
now: now,
1011
914
feeds: defaultFeeds,
915
+
ownerDid: ownerDid,
1012
916
);
1013
917
1014
-
if (defaultFeeds.isEmpty) {
918
+
if (defaultFeeds.isNotEmpty) {
919
+
await _dao.upsertFeeds(defaultFeeds);
920
+
_logger.info('Seeded ${defaultFeeds.length} default feeds');
921
+
} else {
1015
922
_logger.debug('Default feeds already up to date');
1016
-
return;
1017
923
}
1018
-
1019
-
await _dao.upsertFeeds(defaultFeeds);
1020
-
_logger.info('Seeded ${defaultFeeds.length} default feeds');
1021
924
}
1022
925
1023
926
Future<void> _ensureCuratedFeed({
···
1028
931
required bool shouldPin,
1029
932
required DateTime now,
1030
933
required List<SavedFeedsCompanion> feeds,
934
+
required String ownerDid,
1031
935
}) async {
1032
-
final existing = await _dao.getFeed(uri);
1033
-
if (existing != null) {
1034
-
return;
1035
-
}
936
+
final existing = await _dao.getFeed(uri, ownerDid);
937
+
if (existing != null) return;
1036
938
1037
939
FeedGenerator? metadata;
1038
940
try {
···
1044
946
feeds.add(
1045
947
SavedFeedsCompanion.insert(
1046
948
uri: uri,
949
+
ownerDid: ownerDid,
1047
950
displayName: metadata?.displayName ?? fallbackName,
1048
951
description: Value(metadata?.description ?? fallbackDescription),
1049
952
avatar: Value(metadata?.avatar),
···
1054
957
lastSynced: now,
1055
958
),
1056
959
);
960
+
}
961
+
962
+
/// Fetches metadata for multiple feed generators in batches.
963
+
Future<List<FeedGenerator>> getFeedGenerators(List<String> uris) async {
964
+
if (uris.isEmpty) return [];
965
+
966
+
// Chunk requests to avoid hitting URL length limits or API constraints
967
+
// Assuming 25 is a safe batch size
968
+
const batchSize = 25;
969
+
final results = <FeedGenerator>[];
970
+
971
+
for (var i = 0; i < uris.length; i += batchSize) {
972
+
final end = (i + batchSize < uris.length) ? i + batchSize : uris.length;
973
+
final batch = uris.sublist(i, end);
974
+
975
+
try {
976
+
final response = await _api.call(
977
+
'app.bsky.feed.getFeedGenerators',
978
+
params: {'feeds': batch},
979
+
);
980
+
981
+
final views = (response['feeds'] as List).cast<Map<String, dynamic>>();
982
+
results.addAll(views.map((v) => FeedGenerator.fromJson(v)));
983
+
} catch (e) {
984
+
_logger.error('Batch fetch failed for slice $i-$end', {'error': e});
985
+
// Don't rethrow, partial success is better
986
+
}
987
+
}
988
+
989
+
return results;
1057
990
}
1058
991
}
1059
992
+29
-8
lib/src/features/notifications/application/mark_as_seen_service.dart
+29
-8
lib/src/features/notifications/application/mark_as_seen_service.dart
···
26
26
/// Timer for batching operations.
27
27
Timer? _batchTimer;
28
28
29
-
/// Most recent notification timestamp in the current batch.
29
+
/// The owner DID for the current batch.
30
+
String? _currentOwnerDid;
31
+
32
+
/// The timestamp of the most recent seen notification in the current batch.
30
33
DateTime? _latestSeenTimestamp;
31
34
32
-
/// Whether a flush is currently in progress.
35
+
/// Flag to prevent concurrent flush operations.
33
36
bool _isFlushingseenAt = false;
34
37
35
38
/// Marks a notification as seen by adding its timestamp to the batch.
36
39
///
37
40
/// The notification will be marked locally immediately, and synced
38
41
/// to the server after [_batchDuration] or when enough notifications accumulate.
39
-
void markAsSeen(DateTime notificationTimestamp) {
42
+
void markAsSeen(DateTime notificationTimestamp, String ownerDid) {
40
43
_logger.debug('Marking notification as seen', {
41
44
'timestamp': notificationTimestamp.toIso8601String(),
45
+
'ownerDid': ownerDid,
42
46
});
43
47
48
+
if (_currentOwnerDid != null && _currentOwnerDid != ownerDid) {
49
+
// Owner changed, flush previous batch immediately
50
+
flush();
51
+
}
52
+
53
+
_currentOwnerDid = ownerDid;
54
+
44
55
if (_latestSeenTimestamp == null || notificationTimestamp.isAfter(_latestSeenTimestamp!)) {
45
56
_latestSeenTimestamp = notificationTimestamp;
46
57
}
···
60
71
61
72
/// Internal flush implementation.
62
73
Future<void> _flush() async {
63
-
if (_isFlushingseenAt || _latestSeenTimestamp == null) {
74
+
if (_isFlushingseenAt || _latestSeenTimestamp == null || _currentOwnerDid == null) {
64
75
return;
65
76
}
66
77
67
78
_isFlushingseenAt = true;
68
79
final seenAt = _latestSeenTimestamp!;
80
+
final ownerDid = _currentOwnerDid!;
81
+
82
+
// Clear state before async operation to handle re-entry or new batches
69
83
_latestSeenTimestamp = null;
84
+
_currentOwnerDid = null;
70
85
71
-
_logger.info('Flushing mark as seen batch', {'seenAt': seenAt.toIso8601String()});
86
+
_logger.info('Flushing mark as seen batch', {
87
+
'seenAt': seenAt.toIso8601String(),
88
+
'ownerDid': ownerDid,
89
+
});
72
90
73
91
try {
74
-
await _repository.markAsSeenLocally(seenAt);
92
+
await _repository.markAsSeenLocally(seenAt, ownerDid);
75
93
await _repository.updateSeen(seenAt);
76
94
77
95
_logger.debug('Successfully flushed mark as seen batch', {});
···
79
97
_logger.error('Failed to flush mark as seen batch', error, stack);
80
98
81
99
try {
82
-
await _syncQueue.enqueueMarkSeen(seenAt);
83
-
_logger.info('Queued mark as seen for offline sync', {'seenAt': seenAt.toIso8601String()});
100
+
await _syncQueue.enqueueMarkSeen(seenAt, ownerDid);
101
+
_logger.info('Queued mark as seen for offline sync', {
102
+
'seenAt': seenAt.toIso8601String(),
103
+
'ownerDid': ownerDid,
104
+
});
84
105
} catch (queueError, queueStack) {
85
106
_logger.error('Failed to queue mark as seen for offline sync', queueError, queueStack);
86
107
}
+30
-18
lib/src/features/notifications/application/notifications_notifier.dart
+30
-18
lib/src/features/notifications/application/notifications_notifier.dart
···
18
18
class NotificationsNotifier extends _$NotificationsNotifier {
19
19
Logger get _logger => ref.read(loggerProvider('NotificationsNotifier'));
20
20
21
-
bool get _isAuthenticated => ref.read(authProvider) is AuthStateAuthenticated;
22
-
23
21
@override
24
22
Stream<List<GroupedNotification>> build() {
25
23
final repository = ref.watch(notificationsRepositoryProvider);
26
-
_logger.debug('Building grouped notifications stream', {});
27
-
return repository.watchNotifications().map(GroupedNotification.groupNotifications);
24
+
final authState = ref.watch(authProvider);
25
+
26
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
27
+
28
+
if (ownerDid == null) {
29
+
return const Stream.empty();
30
+
}
31
+
32
+
_logger.debug('Building grouped notifications stream', {'ownerDid': ownerDid});
33
+
return repository.watchNotifications(ownerDid).map(GroupedNotification.groupNotifications);
28
34
}
29
35
30
36
/// Refreshes notifications from the API.
31
-
///
32
-
/// Fetches the latest notifications and updates the local cache.
33
37
Future<void> refresh() async {
34
-
if (!_isAuthenticated) {
35
-
_logger.debug('Skipping refresh: not authenticated', {});
38
+
final authState = ref.read(authProvider);
39
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
40
+
41
+
if (ownerDid == null) {
42
+
_logger.debug('Skipping refresh: not authenticated or unknown owner', {});
36
43
return;
37
44
}
38
45
39
46
final repository = ref.read(notificationsRepositoryProvider);
40
47
41
48
try {
42
-
await repository.fetchNotifications();
43
-
_logger.info('Notifications refreshed', {});
49
+
await repository.fetchNotifications(ownerDid: ownerDid);
50
+
_logger.info('Notifications refreshed', {'ownerDid': ownerDid});
44
51
} catch (error, stack) {
45
52
_logger.error('Failed to refresh notifications', error, stack);
46
53
rethrow;
···
49
56
50
57
/// Loads more notifications using cursor-based pagination.
51
58
Future<void> loadMore() async {
52
-
if (!_isAuthenticated) {
59
+
final authState = ref.read(authProvider);
60
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
61
+
62
+
if (ownerDid == null) {
53
63
_logger.debug('Skipping loadMore: not authenticated', {});
54
64
return;
55
65
}
56
66
57
67
final repository = ref.read(notificationsRepositoryProvider);
58
-
final cursor = await repository.getCursor();
68
+
final cursor = await repository.getCursor(ownerDid);
59
69
60
70
if (cursor == null) {
61
71
_logger.debug('No cursor available for loadMore', {});
···
63
73
}
64
74
65
75
try {
66
-
await repository.fetchNotifications(cursor: cursor);
76
+
await repository.fetchNotifications(ownerDid: ownerDid, cursor: cursor);
67
77
_logger.info('Loaded more notifications', {'cursor': cursor});
68
78
} catch (error, stack) {
69
79
_logger.error('Failed to load more notifications', error, stack);
···
72
82
}
73
83
74
84
/// Marks all notifications as read.
75
-
///
76
-
/// This flushes any pending mark as seen operations, then marks all
77
-
/// notifications as read locally and syncs with the server.
78
85
Future<void> markAllAsRead() async {
86
+
final authState = ref.read(authProvider);
87
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
88
+
89
+
if (ownerDid == null) return;
90
+
79
91
final repository = ref.read(notificationsRepositoryProvider);
80
92
final markAsSeenService = ref.read(markAsSeenServiceProvider);
81
93
82
94
try {
83
95
await markAsSeenService.flush();
84
96
85
-
await repository.markAllAsRead();
97
+
await repository.markAllAsRead(ownerDid);
86
98
87
-
await repository.updateSeen(DateTime.now());
99
+
await repository.markAsSeenLocally(DateTime.now(), ownerDid);
88
100
89
101
_logger.info('Marked all notifications as read', {});
90
102
} catch (error, stack) {
+1
-1
lib/src/features/notifications/application/notifications_notifier.g.dart
+1
-1
lib/src/features/notifications/application/notifications_notifier.g.dart
···
48
48
NotificationsNotifier create() => NotificationsNotifier();
49
49
}
50
50
51
-
String _$notificationsNotifierHash() => r'287578fad2ea840c95e2dac8998c4763146fb8cd';
51
+
String _$notificationsNotifierHash() => r'650ebb9034f46b19f7440fa0b1ceb5aff49d4431';
52
52
53
53
/// Notifier for managing notification list state.
54
54
///
+8
-9
lib/src/features/notifications/application/unread_count_notifier.dart
+8
-9
lib/src/features/notifications/application/unread_count_notifier.dart
···
1
1
import 'package:riverpod_annotation/riverpod_annotation.dart';
2
2
3
-
import '../../../core/utils/logger.dart';
4
3
import '../../../core/utils/logger_provider.dart';
5
4
import '../../../features/auth/application/auth_providers.dart';
6
5
import '../../../features/auth/domain/auth_state.dart';
···
14
13
/// reactive updates when notifications are marked as read.
15
14
@riverpod
16
15
class UnreadCountNotifier extends _$UnreadCountNotifier {
17
-
Logger get _logger => ref.read(loggerProvider('UnreadCountNotifier'));
18
-
19
-
bool get _isAuthenticated => ref.read(authProvider) is AuthStateAuthenticated;
20
-
21
16
@override
22
17
Stream<int> build() {
23
-
if (!_isAuthenticated) {
24
-
_logger.debug('Not authenticated, returning 0 unread count', {});
18
+
final logger = ref.read(loggerProvider('UnreadCountNotifier'));
19
+
final authState = ref.watch(authProvider);
20
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
21
+
22
+
if (ownerDid == null) {
23
+
logger.debug('Not authenticated, returning 0 unread count', {});
25
24
return Stream.value(0);
26
25
}
27
26
28
27
final repository = ref.watch(notificationsRepositoryProvider);
29
-
_logger.debug('Building unread count stream', {});
30
-
return repository.watchUnreadCount();
28
+
logger.debug('Building unread count stream', {});
29
+
return repository.watchUnreadCount(ownerDid);
31
30
}
32
31
}
+1
-1
lib/src/features/notifications/application/unread_count_notifier.g.dart
+1
-1
lib/src/features/notifications/application/unread_count_notifier.g.dart
···
44
44
UnreadCountNotifier create() => UnreadCountNotifier();
45
45
}
46
46
47
-
String _$unreadCountNotifierHash() => r'd92a51e1927057acc715ca32c4371e16e56b8707';
47
+
String _$unreadCountNotifierHash() => r'cc920f31355bc1a2fc031bdf693fcb8c5ad7a4dc';
48
48
49
49
/// Notifier for managing unread notification count.
50
50
///
+44
-50
lib/src/features/notifications/infrastructure/notifications_repository.dart
+44
-50
lib/src/features/notifications/infrastructure/notifications_repository.dart
···
26
26
static const kNotificationsFeedKey = 'notifications';
27
27
28
28
/// Fetches notifications from the API and caches them locally.
29
-
///
30
-
/// [cursor] - Pagination cursor for fetching older notifications.
31
-
/// [limit] - Maximum number of notifications to fetch (default 50, max 100).
32
-
Future<void> fetchNotifications({String? cursor, int limit = 50}) async {
33
-
_logger.info('Fetching notifications', {'cursor': cursor, 'limit': limit});
29
+
Future<void> fetchNotifications({
30
+
required String ownerDid,
31
+
String? cursor,
32
+
int limit = 50,
33
+
}) async {
34
+
_logger.info('Fetching notifications', {
35
+
'cursor': cursor,
36
+
'limit': limit,
37
+
'ownerDid': ownerDid,
38
+
});
34
39
35
40
try {
36
41
final params = <String, dynamic>{'limit': limit.clamp(1, 100)};
···
94
99
NotificationsCompanion.insert(
95
100
uri: notificationMap['uri'] as String,
96
101
actorDid: author['did'] as String,
102
+
ownerDid: ownerDid,
97
103
type: type.name,
98
104
reasonSubjectUri: Value(notificationMap['reasonSubject'] as String?),
99
105
recordJson: Value(recordJson),
···
107
113
await _dao.insertNotificationsBatch(
108
114
newNotifications: notifications,
109
115
newProfiles: profiles,
116
+
ownerDid: ownerDid,
110
117
newCursor: newCursor,
111
118
);
112
119
···
120
127
}
121
128
}
122
129
123
-
/// Returns a stream of notifications from the local cache.
124
-
///
125
-
/// Notifications are joined with actor profiles for complete display data.
126
-
Stream<List<AppNotification>> watchNotifications() {
127
-
return _dao.watchNotifications().map((items) {
130
+
/// Returns a stream of notifications from the local cache for a specific user.
131
+
Stream<List<AppNotification>> watchNotifications(String ownerDid) {
132
+
return _dao.watchNotifications(ownerDid).map((items) {
128
133
return items
129
134
.map((item) {
130
135
final type = NotificationType.fromString(item.notification.type);
···
145
150
});
146
151
}
147
152
148
-
/// Gets the pagination cursor for loading more notifications.
149
-
Future<String?> getCursor() {
150
-
return _dao.getCursor();
153
+
/// Gets the pagination cursor for loading more notifications for a specific user.
154
+
Future<String?> getCursor(String ownerDid) {
155
+
return _dao.getCursor(ownerDid);
151
156
}
152
157
153
-
/// Clears all cached notifications.
154
-
Future<void> clearNotifications() {
155
-
return _dao.clearNotifications();
158
+
/// Clears all cached notifications for a specific user.
159
+
Future<void> clearNotifications(String ownerDid) {
160
+
return _dao.clearNotifications(ownerDid);
156
161
}
157
162
158
-
/// Deletes notifications older than the specified threshold.
159
-
Future<int> deleteStaleNotifications(DateTime threshold) {
160
-
return _dao.deleteStaleNotifications(threshold);
163
+
/// Deletes notifications older than the specified threshold for a specific user.
164
+
Future<int> deleteStaleNotifications(DateTime threshold, String ownerDid) {
165
+
return _dao.deleteStaleNotifications(threshold, ownerDid);
161
166
}
162
167
163
-
/// Marks all notifications as read locally.
164
-
Future<void> markAllAsRead() {
165
-
return _dao.markAllAsRead();
168
+
/// Marks all notifications as read locally for a specific user.
169
+
Future<void> markAllAsRead(String ownerDid) {
170
+
return _dao.markAllAsRead(ownerDid);
166
171
}
167
172
168
-
/// Returns a stream of the unread notification count.
169
-
///
170
-
/// Emits updates whenever notifications are inserted, updated, or deleted.
171
-
Stream<int> watchUnreadCount() {
172
-
return _dao.watchUnreadCount();
173
+
/// Returns a stream of the unread notification count for a specific user.
174
+
Stream<int> watchUnreadCount(String ownerDid) {
175
+
return _dao.watchUnreadCount(ownerDid);
173
176
}
174
177
175
178
/// Fetches the current unread count from the API.
176
-
///
177
-
/// Returns the number of unread notifications according to the server.
178
179
Future<int> getUnreadCount() async {
179
180
_logger.info('Fetching unread count from API', {});
180
181
···
191
192
}
192
193
193
194
/// Marks notifications as seen on the server.
194
-
///
195
-
/// All notifications before [seenAt] timestamp will be marked as seen.
196
-
/// This updates the server state and should be followed by local cache updates.
197
195
Future<void> updateSeen(DateTime seenAt) async {
198
196
_logger.info('Updating seen state', {'seenAt': seenAt.toIso8601String()});
199
197
···
210
208
}
211
209
}
212
210
213
-
/// Marks specific notifications as seen locally.
214
-
///
215
-
/// Updates the local cache to mark notifications before [seenAt] as read.
216
-
Future<void> markAsSeenLocally(DateTime seenAt) async {
217
-
_logger.debug('Marking notifications as seen locally', {'seenAt': seenAt.toIso8601String()});
211
+
/// Marks specific notifications as seen locally for a specific user.
212
+
Future<void> markAsSeenLocally(DateTime seenAt, String ownerDid) async {
213
+
_logger.debug('Marking notifications as seen locally', {
214
+
'seenAt': seenAt.toIso8601String(),
215
+
'ownerDid': ownerDid,
216
+
});
218
217
219
-
await _dao.markAsSeenBefore(seenAt);
218
+
await _dao.markAsSeenBefore(seenAt, ownerDid);
220
219
}
221
220
222
-
/// Processes the sync queue to retry failed mark-as-seen operations.
223
-
///
224
-
/// Cleans up old failed items, then attempts to sync the latest queued
225
-
/// timestamp. Since marking at timestamp T marks all notifications before T,
226
-
/// we only need to sync the most recent timestamp and can then clear all
227
-
/// older queue items.
228
-
Future<void> processSyncQueue() async {
229
-
_logger.debug('Processing notification sync queue', {});
221
+
/// Processes the sync queue to retry failed mark-as-seen operations for a specific user.
222
+
Future<void> processSyncQueue(String ownerDid) async {
223
+
_logger.debug('Processing notification sync queue', {'ownerDid': ownerDid});
230
224
231
225
final threshold = DateTime.now().subtract(const Duration(days: 30));
232
226
final cleanedCount = await _syncQueue.cleanupOldFailedItems(threshold);
···
234
228
_logger.info('Cleaned up old failed sync items', {'count': cleanedCount});
235
229
}
236
230
237
-
final latestSeenAt = await _syncQueue.getLatestSeenAt();
231
+
final latestSeenAt = await _syncQueue.getLatestSeenAt(ownerDid);
238
232
if (latestSeenAt == null) {
239
233
_logger.debug('No items in sync queue', {});
240
234
return;
···
242
236
243
237
_logger.info('Processing sync queue', {'latestSeenAt': latestSeenAt.toIso8601String()});
244
238
245
-
final retryableItems = await _syncQueue.getRetryableItems();
239
+
final retryableItems = await _syncQueue.getRetryableItems(ownerDid);
246
240
247
241
try {
248
-
await markAsSeenLocally(latestSeenAt);
242
+
await markAsSeenLocally(latestSeenAt, ownerDid);
249
243
await updateSeen(latestSeenAt);
250
244
251
-
final deletedCount = await _syncQueue.deleteItemsUpTo(latestSeenAt);
245
+
final deletedCount = await _syncQueue.deleteItemsUpTo(latestSeenAt, ownerDid);
252
246
_logger.info('Successfully synced notifications', {
253
247
'seenAt': latestSeenAt.toIso8601String(),
254
248
'clearedQueueItems': deletedCount,
+7
-2
lib/src/features/notifications/presentation/widgets/grouped_notification_item.dart
+7
-2
lib/src/features/notifications/presentation/widgets/grouped_notification_item.dart
···
3
3
import 'package:go_router/go_router.dart';
4
4
import 'package:lazurite/src/core/utils/date_formatter.dart';
5
5
import 'package:lazurite/src/core/widgets/widgets.dart';
6
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
7
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
6
8
import 'package:lazurite/src/features/notifications/application/notifications_providers.dart';
7
9
import 'package:lazurite/src/features/notifications/domain/grouped_notification.dart';
8
10
import 'package:lazurite/src/features/notifications/domain/notification_type.dart';
···
216
218
}
217
219
218
220
if (group.subjectUri != null) {
219
-
final service = ref.read(markAsSeenServiceProvider);
220
-
service.markAsSeen(group.mostRecentTimestamp);
221
+
final authState = ref.read(authProvider);
222
+
if (authState is AuthStateAuthenticated) {
223
+
final service = ref.read(markAsSeenServiceProvider);
224
+
service.markAsSeen(group.mostRecentTimestamp, authState.session.did);
225
+
}
221
226
222
227
final encodedUri = Uri.encodeComponent(group.subjectUri!);
223
228
GoRouter.of(context).push('/home/t/$encodedUri');
+7
-2
lib/src/features/notifications/presentation/widgets/notification_list_item.dart
+7
-2
lib/src/features/notifications/presentation/widgets/notification_list_item.dart
···
1
1
import 'package:flutter/material.dart';
2
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
3
import 'package:go_router/go_router.dart';
4
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
5
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
4
6
5
7
import '../../../../core/utils/date_formatter.dart';
6
8
import '../../../../core/widgets/avatar.dart';
···
111
113
112
114
return VisibilityDetector(
113
115
onVisible: () {
114
-
final service = ref.read(markAsSeenServiceProvider);
115
-
service.markAsSeen(notification.indexedAt);
116
+
final authState = ref.read(authProvider);
117
+
if (authState is AuthStateAuthenticated) {
118
+
final service = ref.read(markAsSeenServiceProvider);
119
+
service.markAsSeen(notification.indexedAt, authState.session.did);
120
+
}
116
121
},
117
122
child: card,
118
123
);
+3
-1
lib/src/features/profile/application/profile_providers.dart
+3
-1
lib/src/features/profile/application/profile_providers.dart
···
31
31
32
32
Future<ProfileData> _fetchProfile(String actor) async {
33
33
final repository = ref.read(profileRepositoryProvider);
34
-
return repository.getProfile(actor);
34
+
final authState = ref.read(authProvider);
35
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
36
+
return repository.getProfile(actor, ownerDid);
35
37
}
36
38
37
39
Future<void> refresh() async {
+1
-1
lib/src/features/profile/application/profile_providers.g.dart
+1
-1
lib/src/features/profile/application/profile_providers.g.dart
+8
-7
lib/src/features/profile/infrastructure/profile_repository.dart
+8
-7
lib/src/features/profile/infrastructure/profile_repository.dart
···
21
21
/// Fetches a profile from the API and caches it.
22
22
///
23
23
/// [actor] can be a DID or handle.
24
-
Future<ProfileData> getProfile(String actor) async {
25
-
_logger.info('Fetching profile', {'actor': actor});
24
+
Future<ProfileData> getProfile(String actor, String ownerDid) async {
25
+
_logger.info('Fetching profile', {'actor': actor, 'ownerDid': ownerDid});
26
26
try {
27
27
final response = await _api.call('app.bsky.actor.getProfile', params: {'actor': actor});
28
28
···
58
58
59
59
await _relationshipsDao.upsertRelationship(
60
60
ProfileRelationshipsCompanion.insert(
61
+
ownerDid: ownerDid,
61
62
profileDid: profile.did,
62
63
following: Value(profile.viewerFollowing),
63
64
followingUri: Value(profile.viewerFollowUri),
···
184
185
185
186
await _followsDao.upsertFollow(
186
187
FollowsCompanion.insert(
187
-
actorDid: actorDid,
188
+
actorDid: actorDid, // actorDid is the owner
188
189
subjectDid: subjectDid,
189
190
uri: uri,
190
191
createdAt: Value(DateTime.now()),
···
241
242
try {
242
243
await _api.call('app.bsky.graph.muteActor', body: {'actor': subjectDid});
243
244
244
-
await _relationshipsDao.updateMuteStatus(subjectDid, true);
245
+
await _relationshipsDao.updateMuteStatus(subjectDid, true, actorDid);
245
246
_logger.debug('Muted user', {'subject': subjectDid});
246
247
} catch (e, stack) {
247
248
_logger.error('Failed to mute user', e, stack);
···
257
258
try {
258
259
await _api.call('app.bsky.graph.unmuteActor', body: {'actor': subjectDid});
259
260
260
-
await _relationshipsDao.updateMuteStatus(subjectDid, false);
261
+
await _relationshipsDao.updateMuteStatus(subjectDid, false, actorDid);
261
262
_logger.debug('Unmuted user', {'subject': subjectDid});
262
263
} catch (e, stack) {
263
264
_logger.error('Failed to unmute user', e, stack);
···
286
287
287
288
final uri = response['uri'] as String;
288
289
289
-
await _relationshipsDao.updateBlockStatus(subjectDid, true, blockingUri: uri);
290
+
await _relationshipsDao.updateBlockStatus(subjectDid, true, actorDid, blockingUri: uri);
290
291
_logger.debug('Blocked user', {'subject': subjectDid, 'uri': uri});
291
292
return uri;
292
293
} catch (e, stack) {
···
314
315
);
315
316
316
317
if (subjectDid != null) {
317
-
await _relationshipsDao.updateBlockStatus(subjectDid, false);
318
+
await _relationshipsDao.updateBlockStatus(subjectDid, false, actorDid);
318
319
}
319
320
320
321
_logger.debug('Deleted block record', {'uri': blockUri});
+28
-9
lib/src/features/settings/application/preference_sync_controller.dart
+28
-9
lib/src/features/settings/application/preference_sync_controller.dart
···
19
19
final logger = ref.watch(loggerProvider('PreferenceSync'));
20
20
var hasInitialized = false;
21
21
22
-
Future<void> runSync() async {
23
-
logger.debug('runSync() called');
22
+
Future<void> runSync([String? ownerDid]) async {
23
+
logger.debug('runSync() called', {'ownerDid': ownerDid});
24
+
25
+
// If ownerDid is not provided, try to get it from current state
26
+
final authState = ref.read(authProvider);
27
+
final effectiveOwnerDid =
28
+
ownerDid ?? ((authState is AuthStateAuthenticated) ? authState.session.did : null);
29
+
30
+
if (effectiveOwnerDid == null) {
31
+
logger.debug('Skipping sync: no ownerDid available');
32
+
return;
33
+
}
34
+
24
35
try {
25
36
final repo = ref.read(blueskyPreferencesRepositoryProvider);
26
37
logger.debug('Syncing preferences from remote');
27
-
await repo.syncPreferencesFromRemote();
38
+
await repo.syncPreferencesFromRemote(effectiveOwnerDid);
28
39
logger.info('Preferences synced successfully');
29
40
30
41
logger.debug('Processing preference sync queue');
31
-
await repo.processSyncQueue();
42
+
await repo.processSyncQueue(effectiveOwnerDid);
32
43
logger.info('Preference sync queue processed');
33
44
} catch (e, stack) {
34
45
logger.error('Failed to sync preferences on resume', e, stack);
···
52
63
if (wasAuthed != isAuthed) {
53
64
if (isAuthed) {
54
65
logger.info('User logged in - triggering preference sync');
55
-
unawaited(runSync());
66
+
final newDid = next.session.did;
67
+
unawaited(runSync(newDid));
56
68
} else {
57
69
logger.info('User logged out - clearing cached preferences');
58
-
unawaited(ref.read(blueskyPreferencesRepositoryProvider).clearAll());
70
+
final oldDid = (previous as AuthStateAuthenticated).session.did;
71
+
unawaited(ref.read(blueskyPreferencesRepositoryProvider).clearAll(oldDid));
59
72
}
60
73
} else if (isAuthed && wasAuthed) {
61
74
final prevSession = previous.session;
62
75
final nextSession = next.session;
63
-
if (prevSession.accessJwt != nextSession.accessJwt) {
76
+
if (prevSession.accessJwt != nextSession.accessJwt && prevSession.did == nextSession.did) {
77
+
// Same user, session refresh
64
78
logger.debug('Session refreshed - triggering sync to fetch preferences');
65
-
unawaited(runSync());
79
+
unawaited(runSync(nextSession.did));
80
+
} else if (prevSession.did != nextSession.did) {
81
+
// User switched without full logout/login cycle (e.g. account switcher)
82
+
logger.info('User switched - clearing old prefs and syncing new');
83
+
unawaited(ref.read(blueskyPreferencesRepositoryProvider).clearAll(prevSession.did));
84
+
unawaited(runSync(nextSession.did));
66
85
}
67
86
}
68
87
} else {
···
77
96
78
97
if (authState is AuthStateAuthenticated) {
79
98
logger.info('User is authenticated, running initial sync');
80
-
await runSync();
99
+
await runSync(authState.session.did);
81
100
} else {
82
101
logger.info('User not authenticated, skipping initial sync');
83
102
}
+1
-1
lib/src/features/settings/application/preference_sync_controller.g.dart
+1
-1
lib/src/features/settings/application/preference_sync_controller.g.dart
+44
-6
lib/src/features/settings/application/settings_providers.dart
+44
-6
lib/src/features/settings/application/settings_providers.dart
···
1
1
import 'package:lazurite/src/app/providers.dart';
2
2
import 'package:lazurite/src/core/utils/logger_provider.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
import 'package:lazurite/src/features/settings/domain/bluesky_preferences.dart';
4
6
import 'package:lazurite/src/infrastructure/network/providers.dart';
5
7
import 'package:lazurite/src/infrastructure/preferences/bluesky_preferences_repository.dart';
···
26
28
@riverpod
27
29
Stream<AdultContentPref> adultContentPref(Ref ref) {
28
30
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
29
-
return repo.watchAdultContentPref();
31
+
final authState = ref.watch(authProvider);
32
+
33
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
34
+
35
+
if (ownerDid == null) return Stream.value(const AdultContentPref(enabled: false));
36
+
37
+
return repo.watchAdultContentPref(ownerDid);
30
38
}
31
39
32
40
/// Watches content label preferences.
33
41
@riverpod
34
42
Stream<ContentLabelPrefs> contentLabelPrefs(Ref ref) {
35
43
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
36
-
return repo.watchContentLabelPrefs();
44
+
final authState = ref.watch(authProvider);
45
+
46
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
47
+
48
+
if (ownerDid == null) return Stream.value(ContentLabelPrefs.empty);
49
+
50
+
return repo.watchContentLabelPrefs(ownerDid);
37
51
}
38
52
39
53
/// Watches the labelers preference.
40
54
@riverpod
41
55
Stream<LabelersPref> labelersPref(Ref ref) {
42
56
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
43
-
return repo.watchLabelersPref();
57
+
final authState = ref.watch(authProvider);
58
+
59
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
60
+
61
+
if (ownerDid == null) return Stream.value(LabelersPref.empty);
62
+
63
+
return repo.watchLabelersPref(ownerDid);
44
64
}
45
65
46
66
/// Watches the feed view preference.
47
67
@riverpod
48
68
Stream<FeedViewPref> feedViewPref(Ref ref) {
49
69
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
50
-
return repo.watchFeedViewPref();
70
+
final authState = ref.watch(authProvider);
71
+
72
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
73
+
74
+
if (ownerDid == null) return Stream.value(FeedViewPref.defaultPref);
75
+
76
+
return repo.watchFeedViewPref(ownerDid);
51
77
}
52
78
53
79
/// Watches the thread view preference.
54
80
@riverpod
55
81
Stream<ThreadViewPref> threadViewPref(Ref ref) {
56
82
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
57
-
return repo.watchThreadViewPref();
83
+
final authState = ref.watch(authProvider);
84
+
85
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
86
+
87
+
if (ownerDid == null) return Stream.value(ThreadViewPref.defaultPref);
88
+
89
+
return repo.watchThreadViewPref(ownerDid);
58
90
}
59
91
60
92
/// Watches the muted words preference.
61
93
@riverpod
62
94
Stream<MutedWordsPref> mutedWordsPref(Ref ref) {
63
95
final repo = ref.watch(blueskyPreferencesRepositoryProvider);
64
-
return repo.watchMutedWordsPref();
96
+
final authState = ref.watch(authProvider);
97
+
98
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : null;
99
+
100
+
if (ownerDid == null) return Stream.value(MutedWordsPref.empty);
101
+
102
+
return repo.watchMutedWordsPref(ownerDid);
65
103
}
+6
-6
lib/src/features/settings/application/settings_providers.g.dart
+6
-6
lib/src/features/settings/application/settings_providers.g.dart
···
100
100
}
101
101
}
102
102
103
-
String _$adultContentPrefHash() => r'cdc458780da7c351c2fced97f1b3b03aa298340c';
103
+
String _$adultContentPrefHash() => r'6175521bfb2e3f424fabc4f455b711f5acd2c30a';
104
104
105
105
/// Watches content label preferences.
106
106
···
143
143
}
144
144
}
145
145
146
-
String _$contentLabelPrefsHash() => r'2fc2f175331482fb8f05119519be263e04223440';
146
+
String _$contentLabelPrefsHash() => r'64a76573483e6a43e306451a16ec7251396d3197';
147
147
148
148
/// Watches the labelers preference.
149
149
···
181
181
}
182
182
}
183
183
184
-
String _$labelersPrefHash() => r'ee7f90859d0d27d3bfc9dae105f62f64f85e2624';
184
+
String _$labelersPrefHash() => r'683933f67c7717d800c5f563b2f23e33e89ba904';
185
185
186
186
/// Watches the feed view preference.
187
187
···
219
219
}
220
220
}
221
221
222
-
String _$feedViewPrefHash() => r'65ea4ae206e707cc70ca28091a39ba2d8b748c9c';
222
+
String _$feedViewPrefHash() => r'6cbee61f1a6e3bb8170f5394539d29ecb6023f5b';
223
223
224
224
/// Watches the thread view preference.
225
225
···
257
257
}
258
258
}
259
259
260
-
String _$threadViewPrefHash() => r'4c5a060b06421d062c4905a3ea6ecf6662ecc75f';
260
+
String _$threadViewPrefHash() => r'c75e582075530962cb5683ae0035e73c1c9570c0';
261
261
262
262
/// Watches the muted words preference.
263
263
···
295
295
}
296
296
}
297
297
298
-
String _$mutedWordsPrefHash() => r'aa031687ba6f0bfef3aaf1212c382611f0a74a9e';
298
+
String _$mutedWordsPrefHash() => r'9052fd9aaba1911f28a132c9ed46467fd80ac132';
+13
-2
lib/src/features/settings/presentation/screens/content_moderation_screen.dart
+13
-2
lib/src/features/settings/presentation/screens/content_moderation_screen.dart
···
1
1
import 'package:flutter/material.dart';
2
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
4
6
import '../../application/settings_providers.dart';
5
7
import '../../domain/bluesky_preferences.dart';
···
202
204
203
205
Future<void> _updateAdultContent(WidgetRef ref, bool enabled) async {
204
206
final repo = ref.read(blueskyPreferencesRepositoryProvider);
205
-
await repo.updateAdultContentPref(AdultContentPref(enabled: enabled));
207
+
final authState = ref.read(authProvider);
208
+
if (authState is AuthStateAuthenticated) {
209
+
await repo.updateAdultContentPref(AdultContentPref(enabled: enabled), authState.session.did);
210
+
}
206
211
}
207
212
208
213
Future<void> _updateLabelVisibility(
···
222
227
updatedItems.add(ContentLabelPref(label: labelId, visibility: visibility));
223
228
224
229
final repo = ref.read(blueskyPreferencesRepositoryProvider);
225
-
await repo.updateContentLabelPrefs(ContentLabelPrefs(items: updatedItems));
230
+
final authState = ref.read(authProvider);
231
+
if (authState is AuthStateAuthenticated) {
232
+
await repo.updateContentLabelPrefs(
233
+
ContentLabelPrefs(items: updatedItems),
234
+
authState.session.did,
235
+
);
236
+
}
226
237
}
227
238
}
228
239
+10
-2
lib/src/features/settings/presentation/screens/feed_preferences_screen.dart
+10
-2
lib/src/features/settings/presentation/screens/feed_preferences_screen.dart
···
1
1
import 'package:flutter/material.dart';
2
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
4
6
import '../../application/settings_providers.dart';
5
7
import '../../domain/bluesky_preferences.dart';
···
192
194
193
195
Future<void> _updateFeedPref(WidgetRef ref, FeedViewPref pref) async {
194
196
final repo = ref.read(blueskyPreferencesRepositoryProvider);
195
-
await repo.updateFeedViewPref(pref);
197
+
final authState = ref.read(authProvider);
198
+
if (authState is AuthStateAuthenticated) {
199
+
await repo.updateFeedViewPref(pref, authState.session.did);
200
+
}
196
201
}
197
202
198
203
Future<void> _updateThreadPref(WidgetRef ref, ThreadViewPref pref) async {
199
204
final repo = ref.read(blueskyPreferencesRepositoryProvider);
200
-
await repo.updateThreadViewPref(pref);
205
+
final authState = ref.read(authProvider);
206
+
if (authState is AuthStateAuthenticated) {
207
+
await repo.updateThreadViewPref(pref, authState.session.did);
208
+
}
201
209
}
202
210
}
+13
-2
lib/src/features/settings/presentation/screens/muted_words_screen.dart
+13
-2
lib/src/features/settings/presentation/screens/muted_words_screen.dart
···
1
1
import 'package:flutter/material.dart';
2
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
3
import 'package:intl/intl.dart';
4
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
5
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
4
6
5
7
import '../../application/settings_providers.dart';
6
8
import '../../domain/bluesky_preferences.dart';
···
220
222
final updatedItems = [...pref.items, word];
221
223
222
224
final repo = ref.read(blueskyPreferencesRepositoryProvider);
223
-
await repo.updateMutedWordsPref(MutedWordsPref(items: updatedItems));
225
+
final authState = ref.read(authProvider);
226
+
if (authState is AuthStateAuthenticated) {
227
+
await repo.updateMutedWordsPref(MutedWordsPref(items: updatedItems), authState.session.did);
228
+
}
224
229
}
225
230
226
231
Future<void> _removeMutedWord(MutedWord word, MutedWordsPref pref) async {
···
245
250
if (confirmed == true) {
246
251
final updatedItems = pref.items.where((w) => w.id != word.id).toList();
247
252
final repo = ref.read(blueskyPreferencesRepositoryProvider);
248
-
await repo.updateMutedWordsPref(MutedWordsPref(items: updatedItems));
253
+
final authState = ref.read(authProvider);
254
+
if (authState is AuthStateAuthenticated) {
255
+
await repo.updateMutedWordsPref(
256
+
MutedWordsPref(items: updatedItems),
257
+
authState.session.did,
258
+
);
259
+
}
249
260
}
250
261
}
251
262
}
+5
-1
lib/src/features/thread/application/thread_notifier.dart
+5
-1
lib/src/features/thread/application/thread_notifier.dart
···
1
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
2
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
1
3
import 'package:riverpod_annotation/riverpod_annotation.dart';
2
4
3
5
import '../infrastructure/thread_repository.dart';
···
14
16
15
17
Future<ThreadViewPost> _fetchThread(String postUri) async {
16
18
final repository = ref.read(threadRepositoryProvider);
17
-
return repository.getPostThread(postUri);
19
+
final authState = ref.read(authProvider);
20
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
21
+
return repository.getPostThread(postUri, ownerDid);
18
22
}
19
23
20
24
Future<void> refresh() async {
+1
-1
lib/src/features/thread/application/thread_notifier.g.dart
+1
-1
lib/src/features/thread/application/thread_notifier.g.dart
+5
-1
lib/src/features/thread/application/thread_providers.dart
+5
-1
lib/src/features/thread/application/thread_providers.dart
···
1
1
import 'package:lazurite/src/app/providers.dart';
2
2
import 'package:lazurite/src/core/utils/logger_provider.dart';
3
+
import 'package:lazurite/src/features/auth/application/auth_providers.dart';
4
+
import 'package:lazurite/src/features/auth/domain/auth_state.dart';
3
5
import 'package:lazurite/src/infrastructure/db/daos/feed_content_dao.dart';
4
6
import 'package:lazurite/src/infrastructure/network/providers.dart';
5
7
import 'package:riverpod_annotation/riverpod_annotation.dart';
···
19
21
@riverpod
20
22
Stream<List<FeedPost>> threadCache(Ref ref, String postUri) {
21
23
final db = ref.watch(appDatabaseProvider);
22
-
return db.feedContentDao.watchFeedContent('thread:$postUri');
24
+
final authState = ref.watch(authProvider);
25
+
final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
26
+
return db.feedContentDao.watchFeedContent('thread:$postUri', ownerDid);
23
27
}
+1
-1
lib/src/features/thread/application/thread_providers.g.dart
+1
-1
lib/src/features/thread/application/thread_providers.g.dart
···
97
97
}
98
98
}
99
99
100
-
String _$threadCacheHash() => r'310780d1a52f20ec5d8752ce4ec6c687c8eb2226';
100
+
String _$threadCacheHash() => r'100536b7b20ab801be85d2cdf02a5701aab1a2c7';
101
101
102
102
final class ThreadCacheFamily extends $Family
103
103
with $FunctionalFamilyOverride<Stream<List<FeedPost>>, String> {
+9
-6
lib/src/features/thread/infrastructure/thread_repository.dart
+9
-6
lib/src/features/thread/infrastructure/thread_repository.dart
···
13
13
final FeedContentDao _dao;
14
14
final Logger _logger;
15
15
16
-
Future<ThreadViewPost> getPostThread(String uri) async {
17
-
_logger.info('Fetching thread', {'uri': uri});
16
+
Future<ThreadViewPost> getPostThread(String uri, String ownerDid) async {
17
+
_logger.info('Fetching thread', {'uri': uri, 'ownerDid': ownerDid});
18
18
try {
19
19
final response = await _api.call('app.bsky.feed.getPostThread', params: {'uri': uri});
20
20
21
21
final threadJson = response['thread'] as Map<String, dynamic>;
22
22
final thread = ThreadViewPost.fromJson(threadJson);
23
23
24
-
await _cacheThread(thread);
24
+
await _cacheThread(thread, ownerDid);
25
25
_logger.debug('Cached thread posts and participants');
26
26
27
27
return thread;
···
31
31
}
32
32
}
33
33
34
-
Future<void> _cacheThread(ThreadViewPost thread) async {
34
+
Future<void> _cacheThread(ThreadViewPost thread, String ownerDid) async {
35
35
final posts = <PostsCompanion>[];
36
36
final profiles = <ProfilesCompanion>[];
37
37
final relationships = <ProfileRelationshipsCompanion>[];
···
45
45
void visit(ThreadViewPost node) {
46
46
final postCompanion = node.post.toPostsCompanion();
47
47
final profileCompanion = node.post.toProfilesCompanion();
48
-
final relationshipCompanion = node.post.toRelationshipCompanion();
48
+
final relationshipCompanion = node.post.toRelationshipCompanion(ownerDid);
49
49
50
50
if (seenPosts.add(node.post.uri)) {
51
51
posts.add(postCompanion);
···
61
61
FeedContentItemsCompanion.insert(
62
62
feedKey: feedKey,
63
63
postUri: node.post.uri,
64
+
ownerDid: ownerDid,
64
65
sortKey: order.toString().padLeft(6, '0'),
65
66
),
66
67
);
···
79
80
if (posts.isNotEmpty) {
80
81
await _dao.insertFeedContentBatch(
81
82
feedKey: feedKey,
83
+
ownerDid: ownerDid,
82
84
newPosts: posts,
83
85
newProfiles: profiles,
84
86
newRelationships: relationships,
···
282
284
);
283
285
}
284
286
285
-
ProfileRelationshipsCompanion? toRelationshipCompanion() {
287
+
ProfileRelationshipsCompanion? toRelationshipCompanion(String ownerDid) {
286
288
final viewer = author.viewer;
287
289
if (viewer == null) return null;
288
290
289
291
return ProfileRelationshipsCompanion.insert(
292
+
ownerDid: ownerDid,
290
293
profileDid: author.did,
291
294
following: Value(viewer['following'] != null),
292
295
followingUri: Value(viewer['following'] as String?),
+7
-7
lib/src/infrastructure/db/app_database.dart
+7
-7
lib/src/infrastructure/db/app_database.dart
···
5
5
import 'package:path/path.dart' as p;
6
6
import 'package:path_provider/path_provider.dart';
7
7
8
-
import 'daos/bluesky_preferences_dao.dart';
9
8
import 'daos/animation_preferences_dao.dart';
9
+
import 'daos/bluesky_preferences_dao.dart';
10
10
import 'daos/custom_theme_dao.dart';
11
+
import 'daos/dm_convos_dao.dart';
12
+
import 'daos/dm_messages_dao.dart';
13
+
import 'daos/dm_outbox_dao.dart';
11
14
import 'daos/drafts_dao.dart';
12
15
import 'daos/feed_content_dao.dart';
13
16
import 'daos/follows_dao.dart';
14
17
import 'daos/local_settings_dao.dart';
18
+
import 'daos/notifications_dao.dart';
19
+
import 'daos/notifications_sync_queue_dao.dart';
15
20
import 'daos/post_interactions_dao.dart';
16
21
import 'daos/preference_sync_queue_dao.dart';
17
22
import 'daos/profile_dao.dart';
···
19
24
import 'daos/saved_feeds_dao.dart';
20
25
import 'daos/search_cache_dao.dart';
21
26
import 'daos/search_dao.dart';
22
-
import 'daos/notifications_dao.dart';
23
-
import 'daos/notifications_sync_queue_dao.dart';
24
-
import 'daos/dm_convos_dao.dart';
25
-
import 'daos/dm_messages_dao.dart';
26
-
import 'daos/dm_outbox_dao.dart';
27
27
import 'tables.dart';
28
28
29
29
part 'app_database.g.dart';
···
82
82
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
83
83
84
84
@override
85
-
int get schemaVersion => 1;
85
+
int get schemaVersion => 3;
86
86
87
87
@override
88
88
MigrationStrategy get migration => MigrationStrategy(
+796
-34
lib/src/infrastructure/db/app_database.g.dart
+796
-34
lib/src/infrastructure/db/app_database.g.dart
···
1631
1631
requiredDuringInsert: true,
1632
1632
defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES posts (uri)'),
1633
1633
);
1634
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
1635
+
@override
1636
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
1637
+
'owner_did',
1638
+
aliasedName,
1639
+
false,
1640
+
type: DriftSqlType.string,
1641
+
requiredDuringInsert: true,
1642
+
);
1634
1643
static const VerificationMeta _reasonMeta = const VerificationMeta('reason');
1635
1644
@override
1636
1645
late final GeneratedColumn<String> reason = GeneratedColumn<String>(
···
1650
1659
requiredDuringInsert: true,
1651
1660
);
1652
1661
@override
1653
-
List<GeneratedColumn> get $columns => [feedKey, postUri, reason, sortKey];
1662
+
List<GeneratedColumn> get $columns => [feedKey, postUri, ownerDid, reason, sortKey];
1654
1663
@override
1655
1664
String get aliasedName => _alias ?? actualTableName;
1656
1665
@override
···
1673
1682
} else if (isInserting) {
1674
1683
context.missing(_postUriMeta);
1675
1684
}
1685
+
if (data.containsKey('owner_did')) {
1686
+
context.handle(
1687
+
_ownerDidMeta,
1688
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
1689
+
);
1690
+
} else if (isInserting) {
1691
+
context.missing(_ownerDidMeta);
1692
+
}
1676
1693
if (data.containsKey('reason')) {
1677
1694
context.handle(_reasonMeta, reason.isAcceptableOrUnknown(data['reason']!, _reasonMeta));
1678
1695
}
···
1685
1702
}
1686
1703
1687
1704
@override
1688
-
Set<GeneratedColumn> get $primaryKey => {feedKey, postUri};
1705
+
Set<GeneratedColumn> get $primaryKey => {feedKey, postUri, ownerDid};
1689
1706
@override
1690
1707
FeedContentItem map(Map<String, dynamic> data, {String? tablePrefix}) {
1691
1708
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
1697
1714
postUri: attachedDatabase.typeMapping.read(
1698
1715
DriftSqlType.string,
1699
1716
data['${effectivePrefix}post_uri'],
1717
+
)!,
1718
+
ownerDid: attachedDatabase.typeMapping.read(
1719
+
DriftSqlType.string,
1720
+
data['${effectivePrefix}owner_did'],
1700
1721
)!,
1701
1722
reason: attachedDatabase.typeMapping.read(
1702
1723
DriftSqlType.string,
···
1718
1739
class FeedContentItem extends DataClass implements Insertable<FeedContentItem> {
1719
1740
final String feedKey;
1720
1741
final String postUri;
1742
+
final String ownerDid;
1721
1743
final String? reason;
1722
1744
final String sortKey;
1723
1745
const FeedContentItem({
1724
1746
required this.feedKey,
1725
1747
required this.postUri,
1748
+
required this.ownerDid,
1726
1749
this.reason,
1727
1750
required this.sortKey,
1728
1751
});
···
1731
1754
final map = <String, Expression>{};
1732
1755
map['feed_key'] = Variable<String>(feedKey);
1733
1756
map['post_uri'] = Variable<String>(postUri);
1757
+
map['owner_did'] = Variable<String>(ownerDid);
1734
1758
if (!nullToAbsent || reason != null) {
1735
1759
map['reason'] = Variable<String>(reason);
1736
1760
}
···
1742
1766
return FeedContentItemsCompanion(
1743
1767
feedKey: Value(feedKey),
1744
1768
postUri: Value(postUri),
1769
+
ownerDid: Value(ownerDid),
1745
1770
reason: reason == null && nullToAbsent ? const Value.absent() : Value(reason),
1746
1771
sortKey: Value(sortKey),
1747
1772
);
···
1752
1777
return FeedContentItem(
1753
1778
feedKey: serializer.fromJson<String>(json['feedKey']),
1754
1779
postUri: serializer.fromJson<String>(json['postUri']),
1780
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
1755
1781
reason: serializer.fromJson<String?>(json['reason']),
1756
1782
sortKey: serializer.fromJson<String>(json['sortKey']),
1757
1783
);
···
1762
1788
return <String, dynamic>{
1763
1789
'feedKey': serializer.toJson<String>(feedKey),
1764
1790
'postUri': serializer.toJson<String>(postUri),
1791
+
'ownerDid': serializer.toJson<String>(ownerDid),
1765
1792
'reason': serializer.toJson<String?>(reason),
1766
1793
'sortKey': serializer.toJson<String>(sortKey),
1767
1794
};
···
1770
1797
FeedContentItem copyWith({
1771
1798
String? feedKey,
1772
1799
String? postUri,
1800
+
String? ownerDid,
1773
1801
Value<String?> reason = const Value.absent(),
1774
1802
String? sortKey,
1775
1803
}) => FeedContentItem(
1776
1804
feedKey: feedKey ?? this.feedKey,
1777
1805
postUri: postUri ?? this.postUri,
1806
+
ownerDid: ownerDid ?? this.ownerDid,
1778
1807
reason: reason.present ? reason.value : this.reason,
1779
1808
sortKey: sortKey ?? this.sortKey,
1780
1809
);
···
1782
1811
return FeedContentItem(
1783
1812
feedKey: data.feedKey.present ? data.feedKey.value : this.feedKey,
1784
1813
postUri: data.postUri.present ? data.postUri.value : this.postUri,
1814
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
1785
1815
reason: data.reason.present ? data.reason.value : this.reason,
1786
1816
sortKey: data.sortKey.present ? data.sortKey.value : this.sortKey,
1787
1817
);
···
1792
1822
return (StringBuffer('FeedContentItem(')
1793
1823
..write('feedKey: $feedKey, ')
1794
1824
..write('postUri: $postUri, ')
1825
+
..write('ownerDid: $ownerDid, ')
1795
1826
..write('reason: $reason, ')
1796
1827
..write('sortKey: $sortKey')
1797
1828
..write(')'))
···
1799
1830
}
1800
1831
1801
1832
@override
1802
-
int get hashCode => Object.hash(feedKey, postUri, reason, sortKey);
1833
+
int get hashCode => Object.hash(feedKey, postUri, ownerDid, reason, sortKey);
1803
1834
@override
1804
1835
bool operator ==(Object other) =>
1805
1836
identical(this, other) ||
1806
1837
(other is FeedContentItem &&
1807
1838
other.feedKey == this.feedKey &&
1808
1839
other.postUri == this.postUri &&
1840
+
other.ownerDid == this.ownerDid &&
1809
1841
other.reason == this.reason &&
1810
1842
other.sortKey == this.sortKey);
1811
1843
}
···
1813
1845
class FeedContentItemsCompanion extends UpdateCompanion<FeedContentItem> {
1814
1846
final Value<String> feedKey;
1815
1847
final Value<String> postUri;
1848
+
final Value<String> ownerDid;
1816
1849
final Value<String?> reason;
1817
1850
final Value<String> sortKey;
1818
1851
final Value<int> rowid;
1819
1852
const FeedContentItemsCompanion({
1820
1853
this.feedKey = const Value.absent(),
1821
1854
this.postUri = const Value.absent(),
1855
+
this.ownerDid = const Value.absent(),
1822
1856
this.reason = const Value.absent(),
1823
1857
this.sortKey = const Value.absent(),
1824
1858
this.rowid = const Value.absent(),
···
1826
1860
FeedContentItemsCompanion.insert({
1827
1861
required String feedKey,
1828
1862
required String postUri,
1863
+
required String ownerDid,
1829
1864
this.reason = const Value.absent(),
1830
1865
required String sortKey,
1831
1866
this.rowid = const Value.absent(),
1832
1867
}) : feedKey = Value(feedKey),
1833
1868
postUri = Value(postUri),
1869
+
ownerDid = Value(ownerDid),
1834
1870
sortKey = Value(sortKey);
1835
1871
static Insertable<FeedContentItem> custom({
1836
1872
Expression<String>? feedKey,
1837
1873
Expression<String>? postUri,
1874
+
Expression<String>? ownerDid,
1838
1875
Expression<String>? reason,
1839
1876
Expression<String>? sortKey,
1840
1877
Expression<int>? rowid,
···
1842
1879
return RawValuesInsertable({
1843
1880
if (feedKey != null) 'feed_key': feedKey,
1844
1881
if (postUri != null) 'post_uri': postUri,
1882
+
if (ownerDid != null) 'owner_did': ownerDid,
1845
1883
if (reason != null) 'reason': reason,
1846
1884
if (sortKey != null) 'sort_key': sortKey,
1847
1885
if (rowid != null) 'rowid': rowid,
···
1851
1889
FeedContentItemsCompanion copyWith({
1852
1890
Value<String>? feedKey,
1853
1891
Value<String>? postUri,
1892
+
Value<String>? ownerDid,
1854
1893
Value<String?>? reason,
1855
1894
Value<String>? sortKey,
1856
1895
Value<int>? rowid,
···
1858
1897
return FeedContentItemsCompanion(
1859
1898
feedKey: feedKey ?? this.feedKey,
1860
1899
postUri: postUri ?? this.postUri,
1900
+
ownerDid: ownerDid ?? this.ownerDid,
1861
1901
reason: reason ?? this.reason,
1862
1902
sortKey: sortKey ?? this.sortKey,
1863
1903
rowid: rowid ?? this.rowid,
···
1873
1913
if (postUri.present) {
1874
1914
map['post_uri'] = Variable<String>(postUri.value);
1875
1915
}
1916
+
if (ownerDid.present) {
1917
+
map['owner_did'] = Variable<String>(ownerDid.value);
1918
+
}
1876
1919
if (reason.present) {
1877
1920
map['reason'] = Variable<String>(reason.value);
1878
1921
}
···
1890
1933
return (StringBuffer('FeedContentItemsCompanion(')
1891
1934
..write('feedKey: $feedKey, ')
1892
1935
..write('postUri: $postUri, ')
1936
+
..write('ownerDid: $ownerDid, ')
1893
1937
..write('reason: $reason, ')
1894
1938
..write('sortKey: $sortKey, ')
1895
1939
..write('rowid: $rowid')
···
2141
2185
type: DriftSqlType.string,
2142
2186
requiredDuringInsert: true,
2143
2187
);
2188
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
2189
+
@override
2190
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
2191
+
'owner_did',
2192
+
aliasedName,
2193
+
false,
2194
+
type: DriftSqlType.string,
2195
+
requiredDuringInsert: true,
2196
+
);
2144
2197
static const VerificationMeta _cursorMeta = const VerificationMeta('cursor');
2145
2198
@override
2146
2199
late final GeneratedColumn<String> cursor = GeneratedColumn<String>(
···
2160
2213
requiredDuringInsert: false,
2161
2214
);
2162
2215
@override
2163
-
List<GeneratedColumn> get $columns => [feedKey, cursor, lastUpdated];
2216
+
List<GeneratedColumn> get $columns => [feedKey, ownerDid, cursor, lastUpdated];
2164
2217
@override
2165
2218
String get aliasedName => _alias ?? actualTableName;
2166
2219
@override
···
2178
2231
} else if (isInserting) {
2179
2232
context.missing(_feedKeyMeta);
2180
2233
}
2234
+
if (data.containsKey('owner_did')) {
2235
+
context.handle(
2236
+
_ownerDidMeta,
2237
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
2238
+
);
2239
+
} else if (isInserting) {
2240
+
context.missing(_ownerDidMeta);
2241
+
}
2181
2242
if (data.containsKey('cursor')) {
2182
2243
context.handle(_cursorMeta, cursor.isAcceptableOrUnknown(data['cursor']!, _cursorMeta));
2183
2244
} else if (isInserting) {
···
2193
2254
}
2194
2255
2195
2256
@override
2196
-
Set<GeneratedColumn> get $primaryKey => {feedKey};
2257
+
Set<GeneratedColumn> get $primaryKey => {feedKey, ownerDid};
2197
2258
@override
2198
2259
FeedCursor map(Map<String, dynamic> data, {String? tablePrefix}) {
2199
2260
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
2202
2263
DriftSqlType.string,
2203
2264
data['${effectivePrefix}feed_key'],
2204
2265
)!,
2266
+
ownerDid: attachedDatabase.typeMapping.read(
2267
+
DriftSqlType.string,
2268
+
data['${effectivePrefix}owner_did'],
2269
+
)!,
2205
2270
cursor: attachedDatabase.typeMapping.read(
2206
2271
DriftSqlType.string,
2207
2272
data['${effectivePrefix}cursor'],
···
2221
2286
2222
2287
class FeedCursor extends DataClass implements Insertable<FeedCursor> {
2223
2288
final String feedKey;
2289
+
final String ownerDid;
2224
2290
final String cursor;
2225
2291
final DateTime? lastUpdated;
2226
-
const FeedCursor({required this.feedKey, required this.cursor, this.lastUpdated});
2292
+
const FeedCursor({
2293
+
required this.feedKey,
2294
+
required this.ownerDid,
2295
+
required this.cursor,
2296
+
this.lastUpdated,
2297
+
});
2227
2298
@override
2228
2299
Map<String, Expression> toColumns(bool nullToAbsent) {
2229
2300
final map = <String, Expression>{};
2230
2301
map['feed_key'] = Variable<String>(feedKey);
2302
+
map['owner_did'] = Variable<String>(ownerDid);
2231
2303
map['cursor'] = Variable<String>(cursor);
2232
2304
if (!nullToAbsent || lastUpdated != null) {
2233
2305
map['last_updated'] = Variable<DateTime>(lastUpdated);
···
2238
2310
FeedCursorsCompanion toCompanion(bool nullToAbsent) {
2239
2311
return FeedCursorsCompanion(
2240
2312
feedKey: Value(feedKey),
2313
+
ownerDid: Value(ownerDid),
2241
2314
cursor: Value(cursor),
2242
2315
lastUpdated: lastUpdated == null && nullToAbsent ? const Value.absent() : Value(lastUpdated),
2243
2316
);
···
2247
2320
serializer ??= driftRuntimeOptions.defaultSerializer;
2248
2321
return FeedCursor(
2249
2322
feedKey: serializer.fromJson<String>(json['feedKey']),
2323
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
2250
2324
cursor: serializer.fromJson<String>(json['cursor']),
2251
2325
lastUpdated: serializer.fromJson<DateTime?>(json['lastUpdated']),
2252
2326
);
···
2256
2330
serializer ??= driftRuntimeOptions.defaultSerializer;
2257
2331
return <String, dynamic>{
2258
2332
'feedKey': serializer.toJson<String>(feedKey),
2333
+
'ownerDid': serializer.toJson<String>(ownerDid),
2259
2334
'cursor': serializer.toJson<String>(cursor),
2260
2335
'lastUpdated': serializer.toJson<DateTime?>(lastUpdated),
2261
2336
};
···
2263
2338
2264
2339
FeedCursor copyWith({
2265
2340
String? feedKey,
2341
+
String? ownerDid,
2266
2342
String? cursor,
2267
2343
Value<DateTime?> lastUpdated = const Value.absent(),
2268
2344
}) => FeedCursor(
2269
2345
feedKey: feedKey ?? this.feedKey,
2346
+
ownerDid: ownerDid ?? this.ownerDid,
2270
2347
cursor: cursor ?? this.cursor,
2271
2348
lastUpdated: lastUpdated.present ? lastUpdated.value : this.lastUpdated,
2272
2349
);
2273
2350
FeedCursor copyWithCompanion(FeedCursorsCompanion data) {
2274
2351
return FeedCursor(
2275
2352
feedKey: data.feedKey.present ? data.feedKey.value : this.feedKey,
2353
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
2276
2354
cursor: data.cursor.present ? data.cursor.value : this.cursor,
2277
2355
lastUpdated: data.lastUpdated.present ? data.lastUpdated.value : this.lastUpdated,
2278
2356
);
···
2282
2360
String toString() {
2283
2361
return (StringBuffer('FeedCursor(')
2284
2362
..write('feedKey: $feedKey, ')
2363
+
..write('ownerDid: $ownerDid, ')
2285
2364
..write('cursor: $cursor, ')
2286
2365
..write('lastUpdated: $lastUpdated')
2287
2366
..write(')'))
···
2289
2368
}
2290
2369
2291
2370
@override
2292
-
int get hashCode => Object.hash(feedKey, cursor, lastUpdated);
2371
+
int get hashCode => Object.hash(feedKey, ownerDid, cursor, lastUpdated);
2293
2372
@override
2294
2373
bool operator ==(Object other) =>
2295
2374
identical(this, other) ||
2296
2375
(other is FeedCursor &&
2297
2376
other.feedKey == this.feedKey &&
2377
+
other.ownerDid == this.ownerDid &&
2298
2378
other.cursor == this.cursor &&
2299
2379
other.lastUpdated == this.lastUpdated);
2300
2380
}
2301
2381
2302
2382
class FeedCursorsCompanion extends UpdateCompanion<FeedCursor> {
2303
2383
final Value<String> feedKey;
2384
+
final Value<String> ownerDid;
2304
2385
final Value<String> cursor;
2305
2386
final Value<DateTime?> lastUpdated;
2306
2387
final Value<int> rowid;
2307
2388
const FeedCursorsCompanion({
2308
2389
this.feedKey = const Value.absent(),
2390
+
this.ownerDid = const Value.absent(),
2309
2391
this.cursor = const Value.absent(),
2310
2392
this.lastUpdated = const Value.absent(),
2311
2393
this.rowid = const Value.absent(),
2312
2394
});
2313
2395
FeedCursorsCompanion.insert({
2314
2396
required String feedKey,
2397
+
required String ownerDid,
2315
2398
required String cursor,
2316
2399
this.lastUpdated = const Value.absent(),
2317
2400
this.rowid = const Value.absent(),
2318
2401
}) : feedKey = Value(feedKey),
2402
+
ownerDid = Value(ownerDid),
2319
2403
cursor = Value(cursor);
2320
2404
static Insertable<FeedCursor> custom({
2321
2405
Expression<String>? feedKey,
2406
+
Expression<String>? ownerDid,
2322
2407
Expression<String>? cursor,
2323
2408
Expression<DateTime>? lastUpdated,
2324
2409
Expression<int>? rowid,
2325
2410
}) {
2326
2411
return RawValuesInsertable({
2327
2412
if (feedKey != null) 'feed_key': feedKey,
2413
+
if (ownerDid != null) 'owner_did': ownerDid,
2328
2414
if (cursor != null) 'cursor': cursor,
2329
2415
if (lastUpdated != null) 'last_updated': lastUpdated,
2330
2416
if (rowid != null) 'rowid': rowid,
···
2333
2419
2334
2420
FeedCursorsCompanion copyWith({
2335
2421
Value<String>? feedKey,
2422
+
Value<String>? ownerDid,
2336
2423
Value<String>? cursor,
2337
2424
Value<DateTime?>? lastUpdated,
2338
2425
Value<int>? rowid,
2339
2426
}) {
2340
2427
return FeedCursorsCompanion(
2341
2428
feedKey: feedKey ?? this.feedKey,
2429
+
ownerDid: ownerDid ?? this.ownerDid,
2342
2430
cursor: cursor ?? this.cursor,
2343
2431
lastUpdated: lastUpdated ?? this.lastUpdated,
2344
2432
rowid: rowid ?? this.rowid,
···
2351
2439
if (feedKey.present) {
2352
2440
map['feed_key'] = Variable<String>(feedKey.value);
2353
2441
}
2442
+
if (ownerDid.present) {
2443
+
map['owner_did'] = Variable<String>(ownerDid.value);
2444
+
}
2354
2445
if (cursor.present) {
2355
2446
map['cursor'] = Variable<String>(cursor.value);
2356
2447
}
···
2367
2458
String toString() {
2368
2459
return (StringBuffer('FeedCursorsCompanion(')
2369
2460
..write('feedKey: $feedKey, ')
2461
+
..write('ownerDid: $ownerDid, ')
2370
2462
..write('cursor: $cursor, ')
2371
2463
..write('lastUpdated: $lastUpdated, ')
2372
2464
..write('rowid: $rowid')
···
3431
3523
type: DriftSqlType.string,
3432
3524
requiredDuringInsert: true,
3433
3525
);
3526
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
3527
+
@override
3528
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
3529
+
'owner_did',
3530
+
aliasedName,
3531
+
false,
3532
+
type: DriftSqlType.string,
3533
+
requiredDuringInsert: true,
3534
+
);
3434
3535
static const VerificationMeta _displayNameMeta = const VerificationMeta('displayName');
3435
3536
@override
3436
3537
late final GeneratedColumn<String> displayName = GeneratedColumn<String>(
···
3519
3620
@override
3520
3621
List<GeneratedColumn> get $columns => [
3521
3622
uri,
3623
+
ownerDid,
3522
3624
displayName,
3523
3625
description,
3524
3626
avatar,
···
3546
3648
} else if (isInserting) {
3547
3649
context.missing(_uriMeta);
3548
3650
}
3651
+
if (data.containsKey('owner_did')) {
3652
+
context.handle(
3653
+
_ownerDidMeta,
3654
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
3655
+
);
3656
+
} else if (isInserting) {
3657
+
context.missing(_ownerDidMeta);
3658
+
}
3549
3659
if (data.containsKey('display_name')) {
3550
3660
context.handle(
3551
3661
_displayNameMeta,
···
3609
3719
}
3610
3720
3611
3721
@override
3612
-
Set<GeneratedColumn> get $primaryKey => {uri};
3722
+
Set<GeneratedColumn> get $primaryKey => {uri, ownerDid};
3613
3723
@override
3614
3724
SavedFeed map(Map<String, dynamic> data, {String? tablePrefix}) {
3615
3725
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
3616
3726
return SavedFeed(
3617
3727
uri: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}uri'])!,
3728
+
ownerDid: attachedDatabase.typeMapping.read(
3729
+
DriftSqlType.string,
3730
+
data['${effectivePrefix}owner_did'],
3731
+
)!,
3618
3732
displayName: attachedDatabase.typeMapping.read(
3619
3733
DriftSqlType.string,
3620
3734
data['${effectivePrefix}display_name'],
···
3664
3778
/// Feed generator AT URI (at://did:plc:xxx/app.bsky.feed.generator/yyy).
3665
3779
final String uri;
3666
3780
3781
+
/// The DID of the user who saved this feed.
3782
+
final String ownerDid;
3783
+
3667
3784
/// Display name of the feed.
3668
3785
final String displayName;
3669
3786
···
3693
3810
final DateTime? localUpdatedAt;
3694
3811
const SavedFeed({
3695
3812
required this.uri,
3813
+
required this.ownerDid,
3696
3814
required this.displayName,
3697
3815
this.description,
3698
3816
this.avatar,
···
3707
3825
Map<String, Expression> toColumns(bool nullToAbsent) {
3708
3826
final map = <String, Expression>{};
3709
3827
map['uri'] = Variable<String>(uri);
3828
+
map['owner_did'] = Variable<String>(ownerDid);
3710
3829
map['display_name'] = Variable<String>(displayName);
3711
3830
if (!nullToAbsent || description != null) {
3712
3831
map['description'] = Variable<String>(description);
···
3728
3847
SavedFeedsCompanion toCompanion(bool nullToAbsent) {
3729
3848
return SavedFeedsCompanion(
3730
3849
uri: Value(uri),
3850
+
ownerDid: Value(ownerDid),
3731
3851
displayName: Value(displayName),
3732
3852
description: description == null && nullToAbsent ? const Value.absent() : Value(description),
3733
3853
avatar: avatar == null && nullToAbsent ? const Value.absent() : Value(avatar),
···
3746
3866
serializer ??= driftRuntimeOptions.defaultSerializer;
3747
3867
return SavedFeed(
3748
3868
uri: serializer.fromJson<String>(json['uri']),
3869
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
3749
3870
displayName: serializer.fromJson<String>(json['displayName']),
3750
3871
description: serializer.fromJson<String?>(json['description']),
3751
3872
avatar: serializer.fromJson<String?>(json['avatar']),
···
3762
3883
serializer ??= driftRuntimeOptions.defaultSerializer;
3763
3884
return <String, dynamic>{
3764
3885
'uri': serializer.toJson<String>(uri),
3886
+
'ownerDid': serializer.toJson<String>(ownerDid),
3765
3887
'displayName': serializer.toJson<String>(displayName),
3766
3888
'description': serializer.toJson<String?>(description),
3767
3889
'avatar': serializer.toJson<String?>(avatar),
···
3776
3898
3777
3899
SavedFeed copyWith({
3778
3900
String? uri,
3901
+
String? ownerDid,
3779
3902
String? displayName,
3780
3903
Value<String?> description = const Value.absent(),
3781
3904
Value<String?> avatar = const Value.absent(),
···
3787
3910
Value<DateTime?> localUpdatedAt = const Value.absent(),
3788
3911
}) => SavedFeed(
3789
3912
uri: uri ?? this.uri,
3913
+
ownerDid: ownerDid ?? this.ownerDid,
3790
3914
displayName: displayName ?? this.displayName,
3791
3915
description: description.present ? description.value : this.description,
3792
3916
avatar: avatar.present ? avatar.value : this.avatar,
···
3800
3924
SavedFeed copyWithCompanion(SavedFeedsCompanion data) {
3801
3925
return SavedFeed(
3802
3926
uri: data.uri.present ? data.uri.value : this.uri,
3927
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
3803
3928
displayName: data.displayName.present ? data.displayName.value : this.displayName,
3804
3929
description: data.description.present ? data.description.value : this.description,
3805
3930
avatar: data.avatar.present ? data.avatar.value : this.avatar,
···
3818
3943
String toString() {
3819
3944
return (StringBuffer('SavedFeed(')
3820
3945
..write('uri: $uri, ')
3946
+
..write('ownerDid: $ownerDid, ')
3821
3947
..write('displayName: $displayName, ')
3822
3948
..write('description: $description, ')
3823
3949
..write('avatar: $avatar, ')
···
3834
3960
@override
3835
3961
int get hashCode => Object.hash(
3836
3962
uri,
3963
+
ownerDid,
3837
3964
displayName,
3838
3965
description,
3839
3966
avatar,
···
3849
3976
identical(this, other) ||
3850
3977
(other is SavedFeed &&
3851
3978
other.uri == this.uri &&
3979
+
other.ownerDid == this.ownerDid &&
3852
3980
other.displayName == this.displayName &&
3853
3981
other.description == this.description &&
3854
3982
other.avatar == this.avatar &&
···
3862
3990
3863
3991
class SavedFeedsCompanion extends UpdateCompanion<SavedFeed> {
3864
3992
final Value<String> uri;
3993
+
final Value<String> ownerDid;
3865
3994
final Value<String> displayName;
3866
3995
final Value<String?> description;
3867
3996
final Value<String?> avatar;
···
3874
4003
final Value<int> rowid;
3875
4004
const SavedFeedsCompanion({
3876
4005
this.uri = const Value.absent(),
4006
+
this.ownerDid = const Value.absent(),
3877
4007
this.displayName = const Value.absent(),
3878
4008
this.description = const Value.absent(),
3879
4009
this.avatar = const Value.absent(),
···
3887
4017
});
3888
4018
SavedFeedsCompanion.insert({
3889
4019
required String uri,
4020
+
required String ownerDid,
3890
4021
required String displayName,
3891
4022
this.description = const Value.absent(),
3892
4023
this.avatar = const Value.absent(),
···
3898
4029
this.localUpdatedAt = const Value.absent(),
3899
4030
this.rowid = const Value.absent(),
3900
4031
}) : uri = Value(uri),
4032
+
ownerDid = Value(ownerDid),
3901
4033
displayName = Value(displayName),
3902
4034
creatorDid = Value(creatorDid),
3903
4035
sortOrder = Value(sortOrder),
3904
4036
lastSynced = Value(lastSynced);
3905
4037
static Insertable<SavedFeed> custom({
3906
4038
Expression<String>? uri,
4039
+
Expression<String>? ownerDid,
3907
4040
Expression<String>? displayName,
3908
4041
Expression<String>? description,
3909
4042
Expression<String>? avatar,
···
3917
4050
}) {
3918
4051
return RawValuesInsertable({
3919
4052
if (uri != null) 'uri': uri,
4053
+
if (ownerDid != null) 'owner_did': ownerDid,
3920
4054
if (displayName != null) 'display_name': displayName,
3921
4055
if (description != null) 'description': description,
3922
4056
if (avatar != null) 'avatar': avatar,
···
3932
4066
3933
4067
SavedFeedsCompanion copyWith({
3934
4068
Value<String>? uri,
4069
+
Value<String>? ownerDid,
3935
4070
Value<String>? displayName,
3936
4071
Value<String?>? description,
3937
4072
Value<String?>? avatar,
···
3945
4080
}) {
3946
4081
return SavedFeedsCompanion(
3947
4082
uri: uri ?? this.uri,
4083
+
ownerDid: ownerDid ?? this.ownerDid,
3948
4084
displayName: displayName ?? this.displayName,
3949
4085
description: description ?? this.description,
3950
4086
avatar: avatar ?? this.avatar,
···
3964
4100
if (uri.present) {
3965
4101
map['uri'] = Variable<String>(uri.value);
3966
4102
}
4103
+
if (ownerDid.present) {
4104
+
map['owner_did'] = Variable<String>(ownerDid.value);
4105
+
}
3967
4106
if (displayName.present) {
3968
4107
map['display_name'] = Variable<String>(displayName.value);
3969
4108
}
···
4001
4140
String toString() {
4002
4141
return (StringBuffer('SavedFeedsCompanion(')
4003
4142
..write('uri: $uri, ')
4143
+
..write('ownerDid: $ownerDid, ')
4004
4144
..write('displayName: $displayName, ')
4005
4145
..write('description: $description, ')
4006
4146
..write('avatar: $avatar, ')
···
4033
4173
requiredDuringInsert: false,
4034
4174
defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'),
4035
4175
);
4176
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
4177
+
@override
4178
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
4179
+
'owner_did',
4180
+
aliasedName,
4181
+
false,
4182
+
type: DriftSqlType.string,
4183
+
requiredDuringInsert: true,
4184
+
);
4036
4185
static const VerificationMeta _categoryMeta = const VerificationMeta('category');
4037
4186
@override
4038
4187
late final GeneratedColumn<String> category = GeneratedColumn<String>(
···
4081
4230
defaultValue: const Constant(0),
4082
4231
);
4083
4232
@override
4084
-
List<GeneratedColumn> get $columns => [id, category, type, payload, createdAt, retryCount];
4233
+
List<GeneratedColumn> get $columns => [
4234
+
id,
4235
+
ownerDid,
4236
+
category,
4237
+
type,
4238
+
payload,
4239
+
createdAt,
4240
+
retryCount,
4241
+
];
4085
4242
@override
4086
4243
String get aliasedName => _alias ?? actualTableName;
4087
4244
@override
···
4096
4253
final data = instance.toColumns(true);
4097
4254
if (data.containsKey('id')) {
4098
4255
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
4256
+
}
4257
+
if (data.containsKey('owner_did')) {
4258
+
context.handle(
4259
+
_ownerDidMeta,
4260
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
4261
+
);
4262
+
} else if (isInserting) {
4263
+
context.missing(_ownerDidMeta);
4099
4264
}
4100
4265
if (data.containsKey('category')) {
4101
4266
context.handle(
···
4137
4302
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
4138
4303
return PreferenceSyncQueueData(
4139
4304
id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
4305
+
ownerDid: attachedDatabase.typeMapping.read(
4306
+
DriftSqlType.string,
4307
+
data['${effectivePrefix}owner_did'],
4308
+
)!,
4140
4309
category: attachedDatabase.typeMapping.read(
4141
4310
DriftSqlType.string,
4142
4311
data['${effectivePrefix}category'],
···
4168
4337
4169
4338
class PreferenceSyncQueueData extends DataClass implements Insertable<PreferenceSyncQueueData> {
4170
4339
final int id;
4340
+
4341
+
/// The DID of the user who owns this action.
4342
+
final String ownerDid;
4171
4343
4172
4344
/// Category of preference being synced: 'feed' or 'bluesky_pref'.
4173
4345
final String category;
···
4191
4363
final int retryCount;
4192
4364
const PreferenceSyncQueueData({
4193
4365
required this.id,
4366
+
required this.ownerDid,
4194
4367
required this.category,
4195
4368
required this.type,
4196
4369
required this.payload,
···
4201
4374
Map<String, Expression> toColumns(bool nullToAbsent) {
4202
4375
final map = <String, Expression>{};
4203
4376
map['id'] = Variable<int>(id);
4377
+
map['owner_did'] = Variable<String>(ownerDid);
4204
4378
map['category'] = Variable<String>(category);
4205
4379
map['type'] = Variable<String>(type);
4206
4380
map['payload'] = Variable<String>(payload);
···
4212
4386
PreferenceSyncQueueCompanion toCompanion(bool nullToAbsent) {
4213
4387
return PreferenceSyncQueueCompanion(
4214
4388
id: Value(id),
4389
+
ownerDid: Value(ownerDid),
4215
4390
category: Value(category),
4216
4391
type: Value(type),
4217
4392
payload: Value(payload),
···
4227
4402
serializer ??= driftRuntimeOptions.defaultSerializer;
4228
4403
return PreferenceSyncQueueData(
4229
4404
id: serializer.fromJson<int>(json['id']),
4405
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
4230
4406
category: serializer.fromJson<String>(json['category']),
4231
4407
type: serializer.fromJson<String>(json['type']),
4232
4408
payload: serializer.fromJson<String>(json['payload']),
···
4239
4415
serializer ??= driftRuntimeOptions.defaultSerializer;
4240
4416
return <String, dynamic>{
4241
4417
'id': serializer.toJson<int>(id),
4418
+
'ownerDid': serializer.toJson<String>(ownerDid),
4242
4419
'category': serializer.toJson<String>(category),
4243
4420
'type': serializer.toJson<String>(type),
4244
4421
'payload': serializer.toJson<String>(payload),
···
4249
4426
4250
4427
PreferenceSyncQueueData copyWith({
4251
4428
int? id,
4429
+
String? ownerDid,
4252
4430
String? category,
4253
4431
String? type,
4254
4432
String? payload,
···
4256
4434
int? retryCount,
4257
4435
}) => PreferenceSyncQueueData(
4258
4436
id: id ?? this.id,
4437
+
ownerDid: ownerDid ?? this.ownerDid,
4259
4438
category: category ?? this.category,
4260
4439
type: type ?? this.type,
4261
4440
payload: payload ?? this.payload,
···
4265
4444
PreferenceSyncQueueData copyWithCompanion(PreferenceSyncQueueCompanion data) {
4266
4445
return PreferenceSyncQueueData(
4267
4446
id: data.id.present ? data.id.value : this.id,
4447
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
4268
4448
category: data.category.present ? data.category.value : this.category,
4269
4449
type: data.type.present ? data.type.value : this.type,
4270
4450
payload: data.payload.present ? data.payload.value : this.payload,
···
4277
4457
String toString() {
4278
4458
return (StringBuffer('PreferenceSyncQueueData(')
4279
4459
..write('id: $id, ')
4460
+
..write('ownerDid: $ownerDid, ')
4280
4461
..write('category: $category, ')
4281
4462
..write('type: $type, ')
4282
4463
..write('payload: $payload, ')
···
4287
4468
}
4288
4469
4289
4470
@override
4290
-
int get hashCode => Object.hash(id, category, type, payload, createdAt, retryCount);
4471
+
int get hashCode => Object.hash(id, ownerDid, category, type, payload, createdAt, retryCount);
4291
4472
@override
4292
4473
bool operator ==(Object other) =>
4293
4474
identical(this, other) ||
4294
4475
(other is PreferenceSyncQueueData &&
4295
4476
other.id == this.id &&
4477
+
other.ownerDid == this.ownerDid &&
4296
4478
other.category == this.category &&
4297
4479
other.type == this.type &&
4298
4480
other.payload == this.payload &&
···
4302
4484
4303
4485
class PreferenceSyncQueueCompanion extends UpdateCompanion<PreferenceSyncQueueData> {
4304
4486
final Value<int> id;
4487
+
final Value<String> ownerDid;
4305
4488
final Value<String> category;
4306
4489
final Value<String> type;
4307
4490
final Value<String> payload;
···
4309
4492
final Value<int> retryCount;
4310
4493
const PreferenceSyncQueueCompanion({
4311
4494
this.id = const Value.absent(),
4495
+
this.ownerDid = const Value.absent(),
4312
4496
this.category = const Value.absent(),
4313
4497
this.type = const Value.absent(),
4314
4498
this.payload = const Value.absent(),
···
4317
4501
});
4318
4502
PreferenceSyncQueueCompanion.insert({
4319
4503
this.id = const Value.absent(),
4504
+
required String ownerDid,
4320
4505
this.category = const Value.absent(),
4321
4506
required String type,
4322
4507
required String payload,
4323
4508
required DateTime createdAt,
4324
4509
this.retryCount = const Value.absent(),
4325
-
}) : type = Value(type),
4510
+
}) : ownerDid = Value(ownerDid),
4511
+
type = Value(type),
4326
4512
payload = Value(payload),
4327
4513
createdAt = Value(createdAt);
4328
4514
static Insertable<PreferenceSyncQueueData> custom({
4329
4515
Expression<int>? id,
4516
+
Expression<String>? ownerDid,
4330
4517
Expression<String>? category,
4331
4518
Expression<String>? type,
4332
4519
Expression<String>? payload,
···
4335
4522
}) {
4336
4523
return RawValuesInsertable({
4337
4524
if (id != null) 'id': id,
4525
+
if (ownerDid != null) 'owner_did': ownerDid,
4338
4526
if (category != null) 'category': category,
4339
4527
if (type != null) 'type': type,
4340
4528
if (payload != null) 'payload': payload,
···
4345
4533
4346
4534
PreferenceSyncQueueCompanion copyWith({
4347
4535
Value<int>? id,
4536
+
Value<String>? ownerDid,
4348
4537
Value<String>? category,
4349
4538
Value<String>? type,
4350
4539
Value<String>? payload,
···
4353
4542
}) {
4354
4543
return PreferenceSyncQueueCompanion(
4355
4544
id: id ?? this.id,
4545
+
ownerDid: ownerDid ?? this.ownerDid,
4356
4546
category: category ?? this.category,
4357
4547
type: type ?? this.type,
4358
4548
payload: payload ?? this.payload,
···
4367
4557
if (id.present) {
4368
4558
map['id'] = Variable<int>(id.value);
4369
4559
}
4560
+
if (ownerDid.present) {
4561
+
map['owner_did'] = Variable<String>(ownerDid.value);
4562
+
}
4370
4563
if (category.present) {
4371
4564
map['category'] = Variable<String>(category.value);
4372
4565
}
···
4389
4582
String toString() {
4390
4583
return (StringBuffer('PreferenceSyncQueueCompanion(')
4391
4584
..write('id: $id, ')
4585
+
..write('ownerDid: $ownerDid, ')
4392
4586
..write('category: $category, ')
4393
4587
..write('type: $type, ')
4394
4588
..write('payload: $payload, ')
···
5885
6079
final GeneratedDatabase attachedDatabase;
5886
6080
final String? _alias;
5887
6081
$ProfileRelationshipsTable(this.attachedDatabase, [this._alias]);
6082
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
6083
+
@override
6084
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
6085
+
'owner_did',
6086
+
aliasedName,
6087
+
false,
6088
+
type: DriftSqlType.string,
6089
+
requiredDuringInsert: true,
6090
+
);
5888
6091
static const VerificationMeta _profileDidMeta = const VerificationMeta('profileDid');
5889
6092
@override
5890
6093
late final GeneratedColumn<String> profileDid = GeneratedColumn<String>(
···
5997
6200
);
5998
6201
@override
5999
6202
List<GeneratedColumn> get $columns => [
6203
+
ownerDid,
6000
6204
profileDid,
6001
6205
following,
6002
6206
followingUri,
···
6021
6225
}) {
6022
6226
final context = VerificationContext();
6023
6227
final data = instance.toColumns(true);
6228
+
if (data.containsKey('owner_did')) {
6229
+
context.handle(
6230
+
_ownerDidMeta,
6231
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
6232
+
);
6233
+
} else if (isInserting) {
6234
+
context.missing(_ownerDidMeta);
6235
+
}
6024
6236
if (data.containsKey('profile_did')) {
6025
6237
context.handle(
6026
6238
_profileDidMeta,
···
6089
6301
}
6090
6302
6091
6303
@override
6092
-
Set<GeneratedColumn> get $primaryKey => {profileDid};
6304
+
Set<GeneratedColumn> get $primaryKey => {ownerDid, profileDid};
6093
6305
@override
6094
6306
ProfileRelationship map(Map<String, dynamic> data, {String? tablePrefix}) {
6095
6307
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
6096
6308
return ProfileRelationship(
6309
+
ownerDid: attachedDatabase.typeMapping.read(
6310
+
DriftSqlType.string,
6311
+
data['${effectivePrefix}owner_did'],
6312
+
)!,
6097
6313
profileDid: attachedDatabase.typeMapping.read(
6098
6314
DriftSqlType.string,
6099
6315
data['${effectivePrefix}profile_did'],
···
6148
6364
}
6149
6365
6150
6366
class ProfileRelationship extends DataClass implements Insertable<ProfileRelationship> {
6367
+
/// The DID of the owner (the user who sees these relationships).
6368
+
final String ownerDid;
6369
+
6151
6370
/// The DID of the profile this relationship applies to (subject).
6152
6371
final String profileDid;
6153
6372
···
6181
6400
/// When this relationship was last updated.
6182
6401
final DateTime updatedAt;
6183
6402
const ProfileRelationship({
6403
+
required this.ownerDid,
6184
6404
required this.profileDid,
6185
6405
required this.following,
6186
6406
this.followingUri,
···
6196
6416
@override
6197
6417
Map<String, Expression> toColumns(bool nullToAbsent) {
6198
6418
final map = <String, Expression>{};
6419
+
map['owner_did'] = Variable<String>(ownerDid);
6199
6420
map['profile_did'] = Variable<String>(profileDid);
6200
6421
map['following'] = Variable<bool>(following);
6201
6422
if (!nullToAbsent || followingUri != null) {
···
6220
6441
6221
6442
ProfileRelationshipsCompanion toCompanion(bool nullToAbsent) {
6222
6443
return ProfileRelationshipsCompanion(
6444
+
ownerDid: Value(ownerDid),
6223
6445
profileDid: Value(profileDid),
6224
6446
following: Value(following),
6225
6447
followingUri: followingUri == null && nullToAbsent
···
6241
6463
factory ProfileRelationship.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) {
6242
6464
serializer ??= driftRuntimeOptions.defaultSerializer;
6243
6465
return ProfileRelationship(
6466
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
6244
6467
profileDid: serializer.fromJson<String>(json['profileDid']),
6245
6468
following: serializer.fromJson<bool>(json['following']),
6246
6469
followingUri: serializer.fromJson<String?>(json['followingUri']),
···
6258
6481
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
6259
6482
serializer ??= driftRuntimeOptions.defaultSerializer;
6260
6483
return <String, dynamic>{
6484
+
'ownerDid': serializer.toJson<String>(ownerDid),
6261
6485
'profileDid': serializer.toJson<String>(profileDid),
6262
6486
'following': serializer.toJson<bool>(following),
6263
6487
'followingUri': serializer.toJson<String?>(followingUri),
···
6273
6497
}
6274
6498
6275
6499
ProfileRelationship copyWith({
6500
+
String? ownerDid,
6276
6501
String? profileDid,
6277
6502
bool? following,
6278
6503
Value<String?> followingUri = const Value.absent(),
···
6285
6510
Value<String?> blockingByList = const Value.absent(),
6286
6511
DateTime? updatedAt,
6287
6512
}) => ProfileRelationship(
6513
+
ownerDid: ownerDid ?? this.ownerDid,
6288
6514
profileDid: profileDid ?? this.profileDid,
6289
6515
following: following ?? this.following,
6290
6516
followingUri: followingUri.present ? followingUri.value : this.followingUri,
···
6299
6525
);
6300
6526
ProfileRelationship copyWithCompanion(ProfileRelationshipsCompanion data) {
6301
6527
return ProfileRelationship(
6528
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
6302
6529
profileDid: data.profileDid.present ? data.profileDid.value : this.profileDid,
6303
6530
following: data.following.present ? data.following.value : this.following,
6304
6531
followingUri: data.followingUri.present ? data.followingUri.value : this.followingUri,
···
6318
6545
@override
6319
6546
String toString() {
6320
6547
return (StringBuffer('ProfileRelationship(')
6548
+
..write('ownerDid: $ownerDid, ')
6321
6549
..write('profileDid: $profileDid, ')
6322
6550
..write('following: $following, ')
6323
6551
..write('followingUri: $followingUri, ')
···
6335
6563
6336
6564
@override
6337
6565
int get hashCode => Object.hash(
6566
+
ownerDid,
6338
6567
profileDid,
6339
6568
following,
6340
6569
followingUri,
···
6351
6580
bool operator ==(Object other) =>
6352
6581
identical(this, other) ||
6353
6582
(other is ProfileRelationship &&
6583
+
other.ownerDid == this.ownerDid &&
6354
6584
other.profileDid == this.profileDid &&
6355
6585
other.following == this.following &&
6356
6586
other.followingUri == this.followingUri &&
···
6365
6595
}
6366
6596
6367
6597
class ProfileRelationshipsCompanion extends UpdateCompanion<ProfileRelationship> {
6598
+
final Value<String> ownerDid;
6368
6599
final Value<String> profileDid;
6369
6600
final Value<bool> following;
6370
6601
final Value<String?> followingUri;
···
6378
6609
final Value<DateTime> updatedAt;
6379
6610
final Value<int> rowid;
6380
6611
const ProfileRelationshipsCompanion({
6612
+
this.ownerDid = const Value.absent(),
6381
6613
this.profileDid = const Value.absent(),
6382
6614
this.following = const Value.absent(),
6383
6615
this.followingUri = const Value.absent(),
···
6392
6624
this.rowid = const Value.absent(),
6393
6625
});
6394
6626
ProfileRelationshipsCompanion.insert({
6627
+
required String ownerDid,
6395
6628
required String profileDid,
6396
6629
this.following = const Value.absent(),
6397
6630
this.followingUri = const Value.absent(),
···
6404
6637
this.blockingByList = const Value.absent(),
6405
6638
required DateTime updatedAt,
6406
6639
this.rowid = const Value.absent(),
6407
-
}) : profileDid = Value(profileDid),
6640
+
}) : ownerDid = Value(ownerDid),
6641
+
profileDid = Value(profileDid),
6408
6642
updatedAt = Value(updatedAt);
6409
6643
static Insertable<ProfileRelationship> custom({
6644
+
Expression<String>? ownerDid,
6410
6645
Expression<String>? profileDid,
6411
6646
Expression<bool>? following,
6412
6647
Expression<String>? followingUri,
···
6421
6656
Expression<int>? rowid,
6422
6657
}) {
6423
6658
return RawValuesInsertable({
6659
+
if (ownerDid != null) 'owner_did': ownerDid,
6424
6660
if (profileDid != null) 'profile_did': profileDid,
6425
6661
if (following != null) 'following': following,
6426
6662
if (followingUri != null) 'following_uri': followingUri,
···
6437
6673
}
6438
6674
6439
6675
ProfileRelationshipsCompanion copyWith({
6676
+
Value<String>? ownerDid,
6440
6677
Value<String>? profileDid,
6441
6678
Value<bool>? following,
6442
6679
Value<String?>? followingUri,
···
6451
6688
Value<int>? rowid,
6452
6689
}) {
6453
6690
return ProfileRelationshipsCompanion(
6691
+
ownerDid: ownerDid ?? this.ownerDid,
6454
6692
profileDid: profileDid ?? this.profileDid,
6455
6693
following: following ?? this.following,
6456
6694
followingUri: followingUri ?? this.followingUri,
···
6469
6707
@override
6470
6708
Map<String, Expression> toColumns(bool nullToAbsent) {
6471
6709
final map = <String, Expression>{};
6710
+
if (ownerDid.present) {
6711
+
map['owner_did'] = Variable<String>(ownerDid.value);
6712
+
}
6472
6713
if (profileDid.present) {
6473
6714
map['profile_did'] = Variable<String>(profileDid.value);
6474
6715
}
···
6511
6752
@override
6512
6753
String toString() {
6513
6754
return (StringBuffer('ProfileRelationshipsCompanion(')
6755
+
..write('ownerDid: $ownerDid, ')
6514
6756
..write('profileDid: $profileDid, ')
6515
6757
..write('following: $following, ')
6516
6758
..write('followingUri: $followingUri, ')
···
7191
7433
type: DriftSqlType.string,
7192
7434
requiredDuringInsert: true,
7193
7435
);
7436
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
7437
+
@override
7438
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
7439
+
'owner_did',
7440
+
aliasedName,
7441
+
false,
7442
+
type: DriftSqlType.string,
7443
+
requiredDuringInsert: true,
7444
+
);
7194
7445
static const VerificationMeta _dataMeta = const VerificationMeta('data');
7195
7446
@override
7196
7447
late final GeneratedColumn<String> data = GeneratedColumn<String>(
···
7210
7461
requiredDuringInsert: true,
7211
7462
);
7212
7463
@override
7213
-
List<GeneratedColumn> get $columns => [type, data, lastSynced];
7464
+
List<GeneratedColumn> get $columns => [type, ownerDid, data, lastSynced];
7214
7465
@override
7215
7466
String get aliasedName => _alias ?? actualTableName;
7216
7467
@override
···
7228
7479
} else if (isInserting) {
7229
7480
context.missing(_typeMeta);
7230
7481
}
7482
+
if (data.containsKey('owner_did')) {
7483
+
context.handle(
7484
+
_ownerDidMeta,
7485
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
7486
+
);
7487
+
} else if (isInserting) {
7488
+
context.missing(_ownerDidMeta);
7489
+
}
7231
7490
if (data.containsKey('data')) {
7232
7491
context.handle(_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta));
7233
7492
} else if (isInserting) {
···
7245
7504
}
7246
7505
7247
7506
@override
7248
-
Set<GeneratedColumn> get $primaryKey => {type};
7507
+
Set<GeneratedColumn> get $primaryKey => {type, ownerDid};
7249
7508
@override
7250
7509
BlueskyPreference map(Map<String, dynamic> data, {String? tablePrefix}) {
7251
7510
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
7254
7513
DriftSqlType.string,
7255
7514
data['${effectivePrefix}type'],
7256
7515
)!,
7516
+
ownerDid: attachedDatabase.typeMapping.read(
7517
+
DriftSqlType.string,
7518
+
data['${effectivePrefix}owner_did'],
7519
+
)!,
7257
7520
data: attachedDatabase.typeMapping.read(
7258
7521
DriftSqlType.string,
7259
7522
data['${effectivePrefix}data'],
···
7275
7538
/// The preference type identifier (e.g., 'contentLabel', 'adultContent').
7276
7539
final String type;
7277
7540
7541
+
/// The DID of the owner of these preferences.
7542
+
final String ownerDid;
7543
+
7278
7544
/// The preference data serialized as JSON.
7279
7545
final String data;
7280
7546
7281
7547
/// When this preference was last synced from the remote server.
7282
7548
final DateTime lastSynced;
7283
-
const BlueskyPreference({required this.type, required this.data, required this.lastSynced});
7549
+
const BlueskyPreference({
7550
+
required this.type,
7551
+
required this.ownerDid,
7552
+
required this.data,
7553
+
required this.lastSynced,
7554
+
});
7284
7555
@override
7285
7556
Map<String, Expression> toColumns(bool nullToAbsent) {
7286
7557
final map = <String, Expression>{};
7287
7558
map['type'] = Variable<String>(type);
7559
+
map['owner_did'] = Variable<String>(ownerDid);
7288
7560
map['data'] = Variable<String>(data);
7289
7561
map['last_synced'] = Variable<DateTime>(lastSynced);
7290
7562
return map;
···
7293
7565
BlueskyPreferencesCompanion toCompanion(bool nullToAbsent) {
7294
7566
return BlueskyPreferencesCompanion(
7295
7567
type: Value(type),
7568
+
ownerDid: Value(ownerDid),
7296
7569
data: Value(data),
7297
7570
lastSynced: Value(lastSynced),
7298
7571
);
···
7302
7575
serializer ??= driftRuntimeOptions.defaultSerializer;
7303
7576
return BlueskyPreference(
7304
7577
type: serializer.fromJson<String>(json['type']),
7578
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
7305
7579
data: serializer.fromJson<String>(json['data']),
7306
7580
lastSynced: serializer.fromJson<DateTime>(json['lastSynced']),
7307
7581
);
···
7311
7585
serializer ??= driftRuntimeOptions.defaultSerializer;
7312
7586
return <String, dynamic>{
7313
7587
'type': serializer.toJson<String>(type),
7588
+
'ownerDid': serializer.toJson<String>(ownerDid),
7314
7589
'data': serializer.toJson<String>(data),
7315
7590
'lastSynced': serializer.toJson<DateTime>(lastSynced),
7316
7591
};
7317
7592
}
7318
7593
7319
-
BlueskyPreference copyWith({String? type, String? data, DateTime? lastSynced}) =>
7320
-
BlueskyPreference(
7321
-
type: type ?? this.type,
7322
-
data: data ?? this.data,
7323
-
lastSynced: lastSynced ?? this.lastSynced,
7324
-
);
7594
+
BlueskyPreference copyWith({
7595
+
String? type,
7596
+
String? ownerDid,
7597
+
String? data,
7598
+
DateTime? lastSynced,
7599
+
}) => BlueskyPreference(
7600
+
type: type ?? this.type,
7601
+
ownerDid: ownerDid ?? this.ownerDid,
7602
+
data: data ?? this.data,
7603
+
lastSynced: lastSynced ?? this.lastSynced,
7604
+
);
7325
7605
BlueskyPreference copyWithCompanion(BlueskyPreferencesCompanion data) {
7326
7606
return BlueskyPreference(
7327
7607
type: data.type.present ? data.type.value : this.type,
7608
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
7328
7609
data: data.data.present ? data.data.value : this.data,
7329
7610
lastSynced: data.lastSynced.present ? data.lastSynced.value : this.lastSynced,
7330
7611
);
···
7334
7615
String toString() {
7335
7616
return (StringBuffer('BlueskyPreference(')
7336
7617
..write('type: $type, ')
7618
+
..write('ownerDid: $ownerDid, ')
7337
7619
..write('data: $data, ')
7338
7620
..write('lastSynced: $lastSynced')
7339
7621
..write(')'))
···
7341
7623
}
7342
7624
7343
7625
@override
7344
-
int get hashCode => Object.hash(type, data, lastSynced);
7626
+
int get hashCode => Object.hash(type, ownerDid, data, lastSynced);
7345
7627
@override
7346
7628
bool operator ==(Object other) =>
7347
7629
identical(this, other) ||
7348
7630
(other is BlueskyPreference &&
7349
7631
other.type == this.type &&
7632
+
other.ownerDid == this.ownerDid &&
7350
7633
other.data == this.data &&
7351
7634
other.lastSynced == this.lastSynced);
7352
7635
}
7353
7636
7354
7637
class BlueskyPreferencesCompanion extends UpdateCompanion<BlueskyPreference> {
7355
7638
final Value<String> type;
7639
+
final Value<String> ownerDid;
7356
7640
final Value<String> data;
7357
7641
final Value<DateTime> lastSynced;
7358
7642
final Value<int> rowid;
7359
7643
const BlueskyPreferencesCompanion({
7360
7644
this.type = const Value.absent(),
7645
+
this.ownerDid = const Value.absent(),
7361
7646
this.data = const Value.absent(),
7362
7647
this.lastSynced = const Value.absent(),
7363
7648
this.rowid = const Value.absent(),
7364
7649
});
7365
7650
BlueskyPreferencesCompanion.insert({
7366
7651
required String type,
7652
+
required String ownerDid,
7367
7653
required String data,
7368
7654
required DateTime lastSynced,
7369
7655
this.rowid = const Value.absent(),
7370
7656
}) : type = Value(type),
7657
+
ownerDid = Value(ownerDid),
7371
7658
data = Value(data),
7372
7659
lastSynced = Value(lastSynced);
7373
7660
static Insertable<BlueskyPreference> custom({
7374
7661
Expression<String>? type,
7662
+
Expression<String>? ownerDid,
7375
7663
Expression<String>? data,
7376
7664
Expression<DateTime>? lastSynced,
7377
7665
Expression<int>? rowid,
7378
7666
}) {
7379
7667
return RawValuesInsertable({
7380
7668
if (type != null) 'type': type,
7669
+
if (ownerDid != null) 'owner_did': ownerDid,
7381
7670
if (data != null) 'data': data,
7382
7671
if (lastSynced != null) 'last_synced': lastSynced,
7383
7672
if (rowid != null) 'rowid': rowid,
···
7386
7675
7387
7676
BlueskyPreferencesCompanion copyWith({
7388
7677
Value<String>? type,
7678
+
Value<String>? ownerDid,
7389
7679
Value<String>? data,
7390
7680
Value<DateTime>? lastSynced,
7391
7681
Value<int>? rowid,
7392
7682
}) {
7393
7683
return BlueskyPreferencesCompanion(
7394
7684
type: type ?? this.type,
7685
+
ownerDid: ownerDid ?? this.ownerDid,
7395
7686
data: data ?? this.data,
7396
7687
lastSynced: lastSynced ?? this.lastSynced,
7397
7688
rowid: rowid ?? this.rowid,
···
7404
7695
if (type.present) {
7405
7696
map['type'] = Variable<String>(type.value);
7406
7697
}
7698
+
if (ownerDid.present) {
7699
+
map['owner_did'] = Variable<String>(ownerDid.value);
7700
+
}
7407
7701
if (data.present) {
7408
7702
map['data'] = Variable<String>(data.value);
7409
7703
}
···
7420
7714
String toString() {
7421
7715
return (StringBuffer('BlueskyPreferencesCompanion(')
7422
7716
..write('type: $type, ')
7717
+
..write('ownerDid: $ownerDid, ')
7423
7718
..write('data: $data, ')
7424
7719
..write('lastSynced: $lastSynced, ')
7425
7720
..write('rowid: $rowid')
···
8147
8442
type: DriftSqlType.string,
8148
8443
requiredDuringInsert: true,
8149
8444
);
8445
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
8446
+
@override
8447
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
8448
+
'owner_did',
8449
+
aliasedName,
8450
+
false,
8451
+
type: DriftSqlType.string,
8452
+
requiredDuringInsert: true,
8453
+
);
8150
8454
static const VerificationMeta _actorDidMeta = const VerificationMeta('actorDid');
8151
8455
@override
8152
8456
late final GeneratedColumn<String> actorDid = GeneratedColumn<String>(
···
8225
8529
@override
8226
8530
List<GeneratedColumn> get $columns => [
8227
8531
uri,
8532
+
ownerDid,
8228
8533
actorDid,
8229
8534
type,
8230
8535
reasonSubjectUri,
···
8251
8556
} else if (isInserting) {
8252
8557
context.missing(_uriMeta);
8253
8558
}
8559
+
if (data.containsKey('owner_did')) {
8560
+
context.handle(
8561
+
_ownerDidMeta,
8562
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
8563
+
);
8564
+
} else if (isInserting) {
8565
+
context.missing(_ownerDidMeta);
8566
+
}
8254
8567
if (data.containsKey('actor_did')) {
8255
8568
context.handle(
8256
8569
_actorDidMeta,
···
8302
8615
}
8303
8616
8304
8617
@override
8305
-
Set<GeneratedColumn> get $primaryKey => {uri};
8618
+
Set<GeneratedColumn> get $primaryKey => {uri, ownerDid};
8306
8619
@override
8307
8620
Notification map(Map<String, dynamic> data, {String? tablePrefix}) {
8308
8621
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
8309
8622
return Notification(
8310
8623
uri: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}uri'])!,
8624
+
ownerDid: attachedDatabase.typeMapping.read(
8625
+
DriftSqlType.string,
8626
+
data['${effectivePrefix}owner_did'],
8627
+
)!,
8311
8628
actorDid: attachedDatabase.typeMapping.read(
8312
8629
DriftSqlType.string,
8313
8630
data['${effectivePrefix}actor_did'],
···
8352
8669
class Notification extends DataClass implements Insertable<Notification> {
8353
8670
/// Notification AT URI (primary key).
8354
8671
final String uri;
8672
+
8673
+
/// The DID of the user receiving the notification.
8674
+
final String ownerDid;
8355
8675
8356
8676
/// DID of the user who triggered the notification.
8357
8677
final String actorDid;
···
8378
8698
final DateTime cachedAt;
8379
8699
const Notification({
8380
8700
required this.uri,
8701
+
required this.ownerDid,
8381
8702
required this.actorDid,
8382
8703
required this.type,
8383
8704
this.reasonSubjectUri,
···
8391
8712
Map<String, Expression> toColumns(bool nullToAbsent) {
8392
8713
final map = <String, Expression>{};
8393
8714
map['uri'] = Variable<String>(uri);
8715
+
map['owner_did'] = Variable<String>(ownerDid);
8394
8716
map['actor_did'] = Variable<String>(actorDid);
8395
8717
map['type'] = Variable<String>(type);
8396
8718
if (!nullToAbsent || reasonSubjectUri != null) {
···
8411
8733
NotificationsCompanion toCompanion(bool nullToAbsent) {
8412
8734
return NotificationsCompanion(
8413
8735
uri: Value(uri),
8736
+
ownerDid: Value(ownerDid),
8414
8737
actorDid: Value(actorDid),
8415
8738
type: Value(type),
8416
8739
reasonSubjectUri: reasonSubjectUri == null && nullToAbsent
···
8428
8751
serializer ??= driftRuntimeOptions.defaultSerializer;
8429
8752
return Notification(
8430
8753
uri: serializer.fromJson<String>(json['uri']),
8754
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
8431
8755
actorDid: serializer.fromJson<String>(json['actorDid']),
8432
8756
type: serializer.fromJson<String>(json['type']),
8433
8757
reasonSubjectUri: serializer.fromJson<String?>(json['reasonSubjectUri']),
···
8443
8767
serializer ??= driftRuntimeOptions.defaultSerializer;
8444
8768
return <String, dynamic>{
8445
8769
'uri': serializer.toJson<String>(uri),
8770
+
'ownerDid': serializer.toJson<String>(ownerDid),
8446
8771
'actorDid': serializer.toJson<String>(actorDid),
8447
8772
'type': serializer.toJson<String>(type),
8448
8773
'reasonSubjectUri': serializer.toJson<String?>(reasonSubjectUri),
···
8456
8781
8457
8782
Notification copyWith({
8458
8783
String? uri,
8784
+
String? ownerDid,
8459
8785
String? actorDid,
8460
8786
String? type,
8461
8787
Value<String?> reasonSubjectUri = const Value.absent(),
···
8466
8792
DateTime? cachedAt,
8467
8793
}) => Notification(
8468
8794
uri: uri ?? this.uri,
8795
+
ownerDid: ownerDid ?? this.ownerDid,
8469
8796
actorDid: actorDid ?? this.actorDid,
8470
8797
type: type ?? this.type,
8471
8798
reasonSubjectUri: reasonSubjectUri.present ? reasonSubjectUri.value : this.reasonSubjectUri,
···
8478
8805
Notification copyWithCompanion(NotificationsCompanion data) {
8479
8806
return Notification(
8480
8807
uri: data.uri.present ? data.uri.value : this.uri,
8808
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
8481
8809
actorDid: data.actorDid.present ? data.actorDid.value : this.actorDid,
8482
8810
type: data.type.present ? data.type.value : this.type,
8483
8811
reasonSubjectUri: data.reasonSubjectUri.present
···
8495
8823
String toString() {
8496
8824
return (StringBuffer('Notification(')
8497
8825
..write('uri: $uri, ')
8826
+
..write('ownerDid: $ownerDid, ')
8498
8827
..write('actorDid: $actorDid, ')
8499
8828
..write('type: $type, ')
8500
8829
..write('reasonSubjectUri: $reasonSubjectUri, ')
···
8510
8839
@override
8511
8840
int get hashCode => Object.hash(
8512
8841
uri,
8842
+
ownerDid,
8513
8843
actorDid,
8514
8844
type,
8515
8845
reasonSubjectUri,
···
8524
8854
identical(this, other) ||
8525
8855
(other is Notification &&
8526
8856
other.uri == this.uri &&
8857
+
other.ownerDid == this.ownerDid &&
8527
8858
other.actorDid == this.actorDid &&
8528
8859
other.type == this.type &&
8529
8860
other.reasonSubjectUri == this.reasonSubjectUri &&
···
8536
8867
8537
8868
class NotificationsCompanion extends UpdateCompanion<Notification> {
8538
8869
final Value<String> uri;
8870
+
final Value<String> ownerDid;
8539
8871
final Value<String> actorDid;
8540
8872
final Value<String> type;
8541
8873
final Value<String?> reasonSubjectUri;
···
8547
8879
final Value<int> rowid;
8548
8880
const NotificationsCompanion({
8549
8881
this.uri = const Value.absent(),
8882
+
this.ownerDid = const Value.absent(),
8550
8883
this.actorDid = const Value.absent(),
8551
8884
this.type = const Value.absent(),
8552
8885
this.reasonSubjectUri = const Value.absent(),
···
8559
8892
});
8560
8893
NotificationsCompanion.insert({
8561
8894
required String uri,
8895
+
required String ownerDid,
8562
8896
required String actorDid,
8563
8897
required String type,
8564
8898
this.reasonSubjectUri = const Value.absent(),
···
8569
8903
required DateTime cachedAt,
8570
8904
this.rowid = const Value.absent(),
8571
8905
}) : uri = Value(uri),
8906
+
ownerDid = Value(ownerDid),
8572
8907
actorDid = Value(actorDid),
8573
8908
type = Value(type),
8574
8909
indexedAt = Value(indexedAt),
8575
8910
cachedAt = Value(cachedAt);
8576
8911
static Insertable<Notification> custom({
8577
8912
Expression<String>? uri,
8913
+
Expression<String>? ownerDid,
8578
8914
Expression<String>? actorDid,
8579
8915
Expression<String>? type,
8580
8916
Expression<String>? reasonSubjectUri,
···
8587
8923
}) {
8588
8924
return RawValuesInsertable({
8589
8925
if (uri != null) 'uri': uri,
8926
+
if (ownerDid != null) 'owner_did': ownerDid,
8590
8927
if (actorDid != null) 'actor_did': actorDid,
8591
8928
if (type != null) 'type': type,
8592
8929
if (reasonSubjectUri != null) 'reason_subject_uri': reasonSubjectUri,
···
8601
8938
8602
8939
NotificationsCompanion copyWith({
8603
8940
Value<String>? uri,
8941
+
Value<String>? ownerDid,
8604
8942
Value<String>? actorDid,
8605
8943
Value<String>? type,
8606
8944
Value<String?>? reasonSubjectUri,
···
8613
8951
}) {
8614
8952
return NotificationsCompanion(
8615
8953
uri: uri ?? this.uri,
8954
+
ownerDid: ownerDid ?? this.ownerDid,
8616
8955
actorDid: actorDid ?? this.actorDid,
8617
8956
type: type ?? this.type,
8618
8957
reasonSubjectUri: reasonSubjectUri ?? this.reasonSubjectUri,
···
8630
8969
final map = <String, Expression>{};
8631
8970
if (uri.present) {
8632
8971
map['uri'] = Variable<String>(uri.value);
8972
+
}
8973
+
if (ownerDid.present) {
8974
+
map['owner_did'] = Variable<String>(ownerDid.value);
8633
8975
}
8634
8976
if (actorDid.present) {
8635
8977
map['actor_did'] = Variable<String>(actorDid.value);
···
8665
9007
String toString() {
8666
9008
return (StringBuffer('NotificationsCompanion(')
8667
9009
..write('uri: $uri, ')
9010
+
..write('ownerDid: $ownerDid, ')
8668
9011
..write('actorDid: $actorDid, ')
8669
9012
..write('type: $type, ')
8670
9013
..write('reasonSubjectUri: $reasonSubjectUri, ')
···
8694
9037
type: DriftSqlType.string,
8695
9038
requiredDuringInsert: true,
8696
9039
);
9040
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
9041
+
@override
9042
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
9043
+
'owner_did',
9044
+
aliasedName,
9045
+
false,
9046
+
type: DriftSqlType.string,
9047
+
requiredDuringInsert: true,
9048
+
);
8697
9049
static const VerificationMeta _cursorMeta = const VerificationMeta('cursor');
8698
9050
@override
8699
9051
late final GeneratedColumn<String> cursor = GeneratedColumn<String>(
···
8713
9065
requiredDuringInsert: false,
8714
9066
);
8715
9067
@override
8716
-
List<GeneratedColumn> get $columns => [feedKey, cursor, lastUpdated];
9068
+
List<GeneratedColumn> get $columns => [feedKey, ownerDid, cursor, lastUpdated];
8717
9069
@override
8718
9070
String get aliasedName => _alias ?? actualTableName;
8719
9071
@override
···
8731
9083
} else if (isInserting) {
8732
9084
context.missing(_feedKeyMeta);
8733
9085
}
9086
+
if (data.containsKey('owner_did')) {
9087
+
context.handle(
9088
+
_ownerDidMeta,
9089
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
9090
+
);
9091
+
} else if (isInserting) {
9092
+
context.missing(_ownerDidMeta);
9093
+
}
8734
9094
if (data.containsKey('cursor')) {
8735
9095
context.handle(_cursorMeta, cursor.isAcceptableOrUnknown(data['cursor']!, _cursorMeta));
8736
9096
} else if (isInserting) {
···
8746
9106
}
8747
9107
8748
9108
@override
8749
-
Set<GeneratedColumn> get $primaryKey => {feedKey};
9109
+
Set<GeneratedColumn> get $primaryKey => {feedKey, ownerDid};
8750
9110
@override
8751
9111
NotificationCursor map(Map<String, dynamic> data, {String? tablePrefix}) {
8752
9112
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
8755
9115
DriftSqlType.string,
8756
9116
data['${effectivePrefix}feed_key'],
8757
9117
)!,
9118
+
ownerDid: attachedDatabase.typeMapping.read(
9119
+
DriftSqlType.string,
9120
+
data['${effectivePrefix}owner_did'],
9121
+
)!,
8758
9122
cursor: attachedDatabase.typeMapping.read(
8759
9123
DriftSqlType.string,
8760
9124
data['${effectivePrefix}cursor'],
···
8776
9140
/// Feed key identifier (e.g., 'notifications').
8777
9141
final String feedKey;
8778
9142
9143
+
/// The DID of the user this cursor belongs to.
9144
+
final String ownerDid;
9145
+
8779
9146
/// Pagination cursor from API.
8780
9147
final String cursor;
8781
9148
8782
9149
/// When the cursor was last updated.
8783
9150
final DateTime? lastUpdated;
8784
-
const NotificationCursor({required this.feedKey, required this.cursor, this.lastUpdated});
9151
+
const NotificationCursor({
9152
+
required this.feedKey,
9153
+
required this.ownerDid,
9154
+
required this.cursor,
9155
+
this.lastUpdated,
9156
+
});
8785
9157
@override
8786
9158
Map<String, Expression> toColumns(bool nullToAbsent) {
8787
9159
final map = <String, Expression>{};
8788
9160
map['feed_key'] = Variable<String>(feedKey);
9161
+
map['owner_did'] = Variable<String>(ownerDid);
8789
9162
map['cursor'] = Variable<String>(cursor);
8790
9163
if (!nullToAbsent || lastUpdated != null) {
8791
9164
map['last_updated'] = Variable<DateTime>(lastUpdated);
···
8796
9169
NotificationCursorsCompanion toCompanion(bool nullToAbsent) {
8797
9170
return NotificationCursorsCompanion(
8798
9171
feedKey: Value(feedKey),
9172
+
ownerDid: Value(ownerDid),
8799
9173
cursor: Value(cursor),
8800
9174
lastUpdated: lastUpdated == null && nullToAbsent ? const Value.absent() : Value(lastUpdated),
8801
9175
);
···
8805
9179
serializer ??= driftRuntimeOptions.defaultSerializer;
8806
9180
return NotificationCursor(
8807
9181
feedKey: serializer.fromJson<String>(json['feedKey']),
9182
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
8808
9183
cursor: serializer.fromJson<String>(json['cursor']),
8809
9184
lastUpdated: serializer.fromJson<DateTime?>(json['lastUpdated']),
8810
9185
);
···
8814
9189
serializer ??= driftRuntimeOptions.defaultSerializer;
8815
9190
return <String, dynamic>{
8816
9191
'feedKey': serializer.toJson<String>(feedKey),
9192
+
'ownerDid': serializer.toJson<String>(ownerDid),
8817
9193
'cursor': serializer.toJson<String>(cursor),
8818
9194
'lastUpdated': serializer.toJson<DateTime?>(lastUpdated),
8819
9195
};
···
8821
9197
8822
9198
NotificationCursor copyWith({
8823
9199
String? feedKey,
9200
+
String? ownerDid,
8824
9201
String? cursor,
8825
9202
Value<DateTime?> lastUpdated = const Value.absent(),
8826
9203
}) => NotificationCursor(
8827
9204
feedKey: feedKey ?? this.feedKey,
9205
+
ownerDid: ownerDid ?? this.ownerDid,
8828
9206
cursor: cursor ?? this.cursor,
8829
9207
lastUpdated: lastUpdated.present ? lastUpdated.value : this.lastUpdated,
8830
9208
);
8831
9209
NotificationCursor copyWithCompanion(NotificationCursorsCompanion data) {
8832
9210
return NotificationCursor(
8833
9211
feedKey: data.feedKey.present ? data.feedKey.value : this.feedKey,
9212
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
8834
9213
cursor: data.cursor.present ? data.cursor.value : this.cursor,
8835
9214
lastUpdated: data.lastUpdated.present ? data.lastUpdated.value : this.lastUpdated,
8836
9215
);
···
8840
9219
String toString() {
8841
9220
return (StringBuffer('NotificationCursor(')
8842
9221
..write('feedKey: $feedKey, ')
9222
+
..write('ownerDid: $ownerDid, ')
8843
9223
..write('cursor: $cursor, ')
8844
9224
..write('lastUpdated: $lastUpdated')
8845
9225
..write(')'))
···
8847
9227
}
8848
9228
8849
9229
@override
8850
-
int get hashCode => Object.hash(feedKey, cursor, lastUpdated);
9230
+
int get hashCode => Object.hash(feedKey, ownerDid, cursor, lastUpdated);
8851
9231
@override
8852
9232
bool operator ==(Object other) =>
8853
9233
identical(this, other) ||
8854
9234
(other is NotificationCursor &&
8855
9235
other.feedKey == this.feedKey &&
9236
+
other.ownerDid == this.ownerDid &&
8856
9237
other.cursor == this.cursor &&
8857
9238
other.lastUpdated == this.lastUpdated);
8858
9239
}
8859
9240
8860
9241
class NotificationCursorsCompanion extends UpdateCompanion<NotificationCursor> {
8861
9242
final Value<String> feedKey;
9243
+
final Value<String> ownerDid;
8862
9244
final Value<String> cursor;
8863
9245
final Value<DateTime?> lastUpdated;
8864
9246
final Value<int> rowid;
8865
9247
const NotificationCursorsCompanion({
8866
9248
this.feedKey = const Value.absent(),
9249
+
this.ownerDid = const Value.absent(),
8867
9250
this.cursor = const Value.absent(),
8868
9251
this.lastUpdated = const Value.absent(),
8869
9252
this.rowid = const Value.absent(),
8870
9253
});
8871
9254
NotificationCursorsCompanion.insert({
8872
9255
required String feedKey,
9256
+
required String ownerDid,
8873
9257
required String cursor,
8874
9258
this.lastUpdated = const Value.absent(),
8875
9259
this.rowid = const Value.absent(),
8876
9260
}) : feedKey = Value(feedKey),
9261
+
ownerDid = Value(ownerDid),
8877
9262
cursor = Value(cursor);
8878
9263
static Insertable<NotificationCursor> custom({
8879
9264
Expression<String>? feedKey,
9265
+
Expression<String>? ownerDid,
8880
9266
Expression<String>? cursor,
8881
9267
Expression<DateTime>? lastUpdated,
8882
9268
Expression<int>? rowid,
8883
9269
}) {
8884
9270
return RawValuesInsertable({
8885
9271
if (feedKey != null) 'feed_key': feedKey,
9272
+
if (ownerDid != null) 'owner_did': ownerDid,
8886
9273
if (cursor != null) 'cursor': cursor,
8887
9274
if (lastUpdated != null) 'last_updated': lastUpdated,
8888
9275
if (rowid != null) 'rowid': rowid,
···
8891
9278
8892
9279
NotificationCursorsCompanion copyWith({
8893
9280
Value<String>? feedKey,
9281
+
Value<String>? ownerDid,
8894
9282
Value<String>? cursor,
8895
9283
Value<DateTime?>? lastUpdated,
8896
9284
Value<int>? rowid,
8897
9285
}) {
8898
9286
return NotificationCursorsCompanion(
8899
9287
feedKey: feedKey ?? this.feedKey,
9288
+
ownerDid: ownerDid ?? this.ownerDid,
8900
9289
cursor: cursor ?? this.cursor,
8901
9290
lastUpdated: lastUpdated ?? this.lastUpdated,
8902
9291
rowid: rowid ?? this.rowid,
···
8908
9297
final map = <String, Expression>{};
8909
9298
if (feedKey.present) {
8910
9299
map['feed_key'] = Variable<String>(feedKey.value);
9300
+
}
9301
+
if (ownerDid.present) {
9302
+
map['owner_did'] = Variable<String>(ownerDid.value);
8911
9303
}
8912
9304
if (cursor.present) {
8913
9305
map['cursor'] = Variable<String>(cursor.value);
···
8925
9317
String toString() {
8926
9318
return (StringBuffer('NotificationCursorsCompanion(')
8927
9319
..write('feedKey: $feedKey, ')
9320
+
..write('ownerDid: $ownerDid, ')
8928
9321
..write('cursor: $cursor, ')
8929
9322
..write('lastUpdated: $lastUpdated, ')
8930
9323
..write('rowid: $rowid')
···
8950
9343
requiredDuringInsert: false,
8951
9344
defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'),
8952
9345
);
9346
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
9347
+
@override
9348
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
9349
+
'owner_did',
9350
+
aliasedName,
9351
+
false,
9352
+
type: DriftSqlType.string,
9353
+
requiredDuringInsert: true,
9354
+
);
8953
9355
static const VerificationMeta _typeMeta = const VerificationMeta('type');
8954
9356
@override
8955
9357
late final GeneratedColumn<String> type = GeneratedColumn<String>(
···
8988
9390
defaultValue: const Constant(0),
8989
9391
);
8990
9392
@override
8991
-
List<GeneratedColumn> get $columns => [id, type, seenAt, createdAt, retryCount];
9393
+
List<GeneratedColumn> get $columns => [id, ownerDid, type, seenAt, createdAt, retryCount];
8992
9394
@override
8993
9395
String get aliasedName => _alias ?? actualTableName;
8994
9396
@override
···
9004
9406
if (data.containsKey('id')) {
9005
9407
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
9006
9408
}
9409
+
if (data.containsKey('owner_did')) {
9410
+
context.handle(
9411
+
_ownerDidMeta,
9412
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
9413
+
);
9414
+
} else if (isInserting) {
9415
+
context.missing(_ownerDidMeta);
9416
+
}
9007
9417
if (data.containsKey('type')) {
9008
9418
context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta));
9009
9419
} else if (isInserting) {
···
9038
9448
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
9039
9449
return NotificationsSyncQueueData(
9040
9450
id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
9451
+
ownerDid: attachedDatabase.typeMapping.read(
9452
+
DriftSqlType.string,
9453
+
data['${effectivePrefix}owner_did'],
9454
+
)!,
9041
9455
type: attachedDatabase.typeMapping.read(
9042
9456
DriftSqlType.string,
9043
9457
data['${effectivePrefix}type'],
···
9067
9481
implements Insertable<NotificationsSyncQueueData> {
9068
9482
final int id;
9069
9483
9484
+
/// The DID of the user who owns this action.
9485
+
final String ownerDid;
9486
+
9070
9487
/// Type of operation: 'mark_seen'.
9071
9488
final String type;
9072
9489
···
9080
9497
final int retryCount;
9081
9498
const NotificationsSyncQueueData({
9082
9499
required this.id,
9500
+
required this.ownerDid,
9083
9501
required this.type,
9084
9502
required this.seenAt,
9085
9503
required this.createdAt,
···
9089
9507
Map<String, Expression> toColumns(bool nullToAbsent) {
9090
9508
final map = <String, Expression>{};
9091
9509
map['id'] = Variable<int>(id);
9510
+
map['owner_did'] = Variable<String>(ownerDid);
9092
9511
map['type'] = Variable<String>(type);
9093
9512
map['seen_at'] = Variable<String>(seenAt);
9094
9513
map['created_at'] = Variable<DateTime>(createdAt);
···
9099
9518
NotificationsSyncQueueCompanion toCompanion(bool nullToAbsent) {
9100
9519
return NotificationsSyncQueueCompanion(
9101
9520
id: Value(id),
9521
+
ownerDid: Value(ownerDid),
9102
9522
type: Value(type),
9103
9523
seenAt: Value(seenAt),
9104
9524
createdAt: Value(createdAt),
···
9113
9533
serializer ??= driftRuntimeOptions.defaultSerializer;
9114
9534
return NotificationsSyncQueueData(
9115
9535
id: serializer.fromJson<int>(json['id']),
9536
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
9116
9537
type: serializer.fromJson<String>(json['type']),
9117
9538
seenAt: serializer.fromJson<String>(json['seenAt']),
9118
9539
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
···
9124
9545
serializer ??= driftRuntimeOptions.defaultSerializer;
9125
9546
return <String, dynamic>{
9126
9547
'id': serializer.toJson<int>(id),
9548
+
'ownerDid': serializer.toJson<String>(ownerDid),
9127
9549
'type': serializer.toJson<String>(type),
9128
9550
'seenAt': serializer.toJson<String>(seenAt),
9129
9551
'createdAt': serializer.toJson<DateTime>(createdAt),
···
9133
9555
9134
9556
NotificationsSyncQueueData copyWith({
9135
9557
int? id,
9558
+
String? ownerDid,
9136
9559
String? type,
9137
9560
String? seenAt,
9138
9561
DateTime? createdAt,
9139
9562
int? retryCount,
9140
9563
}) => NotificationsSyncQueueData(
9141
9564
id: id ?? this.id,
9565
+
ownerDid: ownerDid ?? this.ownerDid,
9142
9566
type: type ?? this.type,
9143
9567
seenAt: seenAt ?? this.seenAt,
9144
9568
createdAt: createdAt ?? this.createdAt,
···
9147
9571
NotificationsSyncQueueData copyWithCompanion(NotificationsSyncQueueCompanion data) {
9148
9572
return NotificationsSyncQueueData(
9149
9573
id: data.id.present ? data.id.value : this.id,
9574
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
9150
9575
type: data.type.present ? data.type.value : this.type,
9151
9576
seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt,
9152
9577
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
···
9158
9583
String toString() {
9159
9584
return (StringBuffer('NotificationsSyncQueueData(')
9160
9585
..write('id: $id, ')
9586
+
..write('ownerDid: $ownerDid, ')
9161
9587
..write('type: $type, ')
9162
9588
..write('seenAt: $seenAt, ')
9163
9589
..write('createdAt: $createdAt, ')
···
9167
9593
}
9168
9594
9169
9595
@override
9170
-
int get hashCode => Object.hash(id, type, seenAt, createdAt, retryCount);
9596
+
int get hashCode => Object.hash(id, ownerDid, type, seenAt, createdAt, retryCount);
9171
9597
@override
9172
9598
bool operator ==(Object other) =>
9173
9599
identical(this, other) ||
9174
9600
(other is NotificationsSyncQueueData &&
9175
9601
other.id == this.id &&
9602
+
other.ownerDid == this.ownerDid &&
9176
9603
other.type == this.type &&
9177
9604
other.seenAt == this.seenAt &&
9178
9605
other.createdAt == this.createdAt &&
···
9181
9608
9182
9609
class NotificationsSyncQueueCompanion extends UpdateCompanion<NotificationsSyncQueueData> {
9183
9610
final Value<int> id;
9611
+
final Value<String> ownerDid;
9184
9612
final Value<String> type;
9185
9613
final Value<String> seenAt;
9186
9614
final Value<DateTime> createdAt;
9187
9615
final Value<int> retryCount;
9188
9616
const NotificationsSyncQueueCompanion({
9189
9617
this.id = const Value.absent(),
9618
+
this.ownerDid = const Value.absent(),
9190
9619
this.type = const Value.absent(),
9191
9620
this.seenAt = const Value.absent(),
9192
9621
this.createdAt = const Value.absent(),
···
9194
9623
});
9195
9624
NotificationsSyncQueueCompanion.insert({
9196
9625
this.id = const Value.absent(),
9626
+
required String ownerDid,
9197
9627
required String type,
9198
9628
required String seenAt,
9199
9629
required DateTime createdAt,
9200
9630
this.retryCount = const Value.absent(),
9201
-
}) : type = Value(type),
9631
+
}) : ownerDid = Value(ownerDid),
9632
+
type = Value(type),
9202
9633
seenAt = Value(seenAt),
9203
9634
createdAt = Value(createdAt);
9204
9635
static Insertable<NotificationsSyncQueueData> custom({
9205
9636
Expression<int>? id,
9637
+
Expression<String>? ownerDid,
9206
9638
Expression<String>? type,
9207
9639
Expression<String>? seenAt,
9208
9640
Expression<DateTime>? createdAt,
···
9210
9642
}) {
9211
9643
return RawValuesInsertable({
9212
9644
if (id != null) 'id': id,
9645
+
if (ownerDid != null) 'owner_did': ownerDid,
9213
9646
if (type != null) 'type': type,
9214
9647
if (seenAt != null) 'seen_at': seenAt,
9215
9648
if (createdAt != null) 'created_at': createdAt,
···
9219
9652
9220
9653
NotificationsSyncQueueCompanion copyWith({
9221
9654
Value<int>? id,
9655
+
Value<String>? ownerDid,
9222
9656
Value<String>? type,
9223
9657
Value<String>? seenAt,
9224
9658
Value<DateTime>? createdAt,
···
9226
9660
}) {
9227
9661
return NotificationsSyncQueueCompanion(
9228
9662
id: id ?? this.id,
9663
+
ownerDid: ownerDid ?? this.ownerDid,
9229
9664
type: type ?? this.type,
9230
9665
seenAt: seenAt ?? this.seenAt,
9231
9666
createdAt: createdAt ?? this.createdAt,
···
9238
9673
final map = <String, Expression>{};
9239
9674
if (id.present) {
9240
9675
map['id'] = Variable<int>(id.value);
9676
+
}
9677
+
if (ownerDid.present) {
9678
+
map['owner_did'] = Variable<String>(ownerDid.value);
9241
9679
}
9242
9680
if (type.present) {
9243
9681
map['type'] = Variable<String>(type.value);
···
9258
9696
String toString() {
9259
9697
return (StringBuffer('NotificationsSyncQueueCompanion(')
9260
9698
..write('id: $id, ')
9699
+
..write('ownerDid: $ownerDid, ')
9261
9700
..write('type: $type, ')
9262
9701
..write('seenAt: $seenAt, ')
9263
9702
..write('createdAt: $createdAt, ')
···
9281
9720
type: DriftSqlType.string,
9282
9721
requiredDuringInsert: true,
9283
9722
);
9723
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
9724
+
@override
9725
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
9726
+
'owner_did',
9727
+
aliasedName,
9728
+
false,
9729
+
type: DriftSqlType.string,
9730
+
requiredDuringInsert: true,
9731
+
);
9284
9732
static const VerificationMeta _membersJsonMeta = const VerificationMeta('membersJson');
9285
9733
@override
9286
9734
late final GeneratedColumn<String> membersJson = GeneratedColumn<String>(
···
9363
9811
@override
9364
9812
List<GeneratedColumn> get $columns => [
9365
9813
convoId,
9814
+
ownerDid,
9366
9815
membersJson,
9367
9816
lastMessageText,
9368
9817
lastMessageAt,
···
9385
9834
context.handle(_convoIdMeta, convoId.isAcceptableOrUnknown(data['convo_id']!, _convoIdMeta));
9386
9835
} else if (isInserting) {
9387
9836
context.missing(_convoIdMeta);
9837
+
}
9838
+
if (data.containsKey('owner_did')) {
9839
+
context.handle(
9840
+
_ownerDidMeta,
9841
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
9842
+
);
9843
+
} else if (isInserting) {
9844
+
context.missing(_ownerDidMeta);
9388
9845
}
9389
9846
if (data.containsKey('members_json')) {
9390
9847
context.handle(
···
9442
9899
}
9443
9900
9444
9901
@override
9445
-
Set<GeneratedColumn> get $primaryKey => {convoId};
9902
+
Set<GeneratedColumn> get $primaryKey => {convoId, ownerDid};
9446
9903
@override
9447
9904
DmConvo map(Map<String, dynamic> data, {String? tablePrefix}) {
9448
9905
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
9450
9907
convoId: attachedDatabase.typeMapping.read(
9451
9908
DriftSqlType.string,
9452
9909
data['${effectivePrefix}convo_id'],
9910
+
)!,
9911
+
ownerDid: attachedDatabase.typeMapping.read(
9912
+
DriftSqlType.string,
9913
+
data['${effectivePrefix}owner_did'],
9453
9914
)!,
9454
9915
membersJson: attachedDatabase.typeMapping.read(
9455
9916
DriftSqlType.string,
···
9496
9957
/// Conversation ID (unique identifier from API).
9497
9958
final String convoId;
9498
9959
9960
+
/// The DID of the user who owns this conversation view.
9961
+
final String ownerDid;
9962
+
9499
9963
/// JSON array of participant DIDs.
9500
9964
final String membersJson;
9501
9965
···
9521
9985
final DateTime cachedAt;
9522
9986
const DmConvo({
9523
9987
required this.convoId,
9988
+
required this.ownerDid,
9524
9989
required this.membersJson,
9525
9990
this.lastMessageText,
9526
9991
this.lastMessageAt,
···
9534
9999
Map<String, Expression> toColumns(bool nullToAbsent) {
9535
10000
final map = <String, Expression>{};
9536
10001
map['convo_id'] = Variable<String>(convoId);
10002
+
map['owner_did'] = Variable<String>(ownerDid);
9537
10003
map['members_json'] = Variable<String>(membersJson);
9538
10004
if (!nullToAbsent || lastMessageText != null) {
9539
10005
map['last_message_text'] = Variable<String>(lastMessageText);
···
9554
10020
DmConvosCompanion toCompanion(bool nullToAbsent) {
9555
10021
return DmConvosCompanion(
9556
10022
convoId: Value(convoId),
10023
+
ownerDid: Value(ownerDid),
9557
10024
membersJson: Value(membersJson),
9558
10025
lastMessageText: lastMessageText == null && nullToAbsent
9559
10026
? const Value.absent()
···
9575
10042
serializer ??= driftRuntimeOptions.defaultSerializer;
9576
10043
return DmConvo(
9577
10044
convoId: serializer.fromJson<String>(json['convoId']),
10045
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
9578
10046
membersJson: serializer.fromJson<String>(json['membersJson']),
9579
10047
lastMessageText: serializer.fromJson<String?>(json['lastMessageText']),
9580
10048
lastMessageAt: serializer.fromJson<DateTime?>(json['lastMessageAt']),
···
9590
10058
serializer ??= driftRuntimeOptions.defaultSerializer;
9591
10059
return <String, dynamic>{
9592
10060
'convoId': serializer.toJson<String>(convoId),
10061
+
'ownerDid': serializer.toJson<String>(ownerDid),
9593
10062
'membersJson': serializer.toJson<String>(membersJson),
9594
10063
'lastMessageText': serializer.toJson<String?>(lastMessageText),
9595
10064
'lastMessageAt': serializer.toJson<DateTime?>(lastMessageAt),
···
9603
10072
9604
10073
DmConvo copyWith({
9605
10074
String? convoId,
10075
+
String? ownerDid,
9606
10076
String? membersJson,
9607
10077
Value<String?> lastMessageText = const Value.absent(),
9608
10078
Value<DateTime?> lastMessageAt = const Value.absent(),
···
9613
10083
DateTime? cachedAt,
9614
10084
}) => DmConvo(
9615
10085
convoId: convoId ?? this.convoId,
10086
+
ownerDid: ownerDid ?? this.ownerDid,
9616
10087
membersJson: membersJson ?? this.membersJson,
9617
10088
lastMessageText: lastMessageText.present ? lastMessageText.value : this.lastMessageText,
9618
10089
lastMessageAt: lastMessageAt.present ? lastMessageAt.value : this.lastMessageAt,
···
9627
10098
DmConvo copyWithCompanion(DmConvosCompanion data) {
9628
10099
return DmConvo(
9629
10100
convoId: data.convoId.present ? data.convoId.value : this.convoId,
10101
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
9630
10102
membersJson: data.membersJson.present ? data.membersJson.value : this.membersJson,
9631
10103
lastMessageText: data.lastMessageText.present
9632
10104
? data.lastMessageText.value
···
9646
10118
String toString() {
9647
10119
return (StringBuffer('DmConvo(')
9648
10120
..write('convoId: $convoId, ')
10121
+
..write('ownerDid: $ownerDid, ')
9649
10122
..write('membersJson: $membersJson, ')
9650
10123
..write('lastMessageText: $lastMessageText, ')
9651
10124
..write('lastMessageAt: $lastMessageAt, ')
···
9661
10134
@override
9662
10135
int get hashCode => Object.hash(
9663
10136
convoId,
10137
+
ownerDid,
9664
10138
membersJson,
9665
10139
lastMessageText,
9666
10140
lastMessageAt,
···
9675
10149
identical(this, other) ||
9676
10150
(other is DmConvo &&
9677
10151
other.convoId == this.convoId &&
10152
+
other.ownerDid == this.ownerDid &&
9678
10153
other.membersJson == this.membersJson &&
9679
10154
other.lastMessageText == this.lastMessageText &&
9680
10155
other.lastMessageAt == this.lastMessageAt &&
···
9687
10162
9688
10163
class DmConvosCompanion extends UpdateCompanion<DmConvo> {
9689
10164
final Value<String> convoId;
10165
+
final Value<String> ownerDid;
9690
10166
final Value<String> membersJson;
9691
10167
final Value<String?> lastMessageText;
9692
10168
final Value<DateTime?> lastMessageAt;
···
9698
10174
final Value<int> rowid;
9699
10175
const DmConvosCompanion({
9700
10176
this.convoId = const Value.absent(),
10177
+
this.ownerDid = const Value.absent(),
9701
10178
this.membersJson = const Value.absent(),
9702
10179
this.lastMessageText = const Value.absent(),
9703
10180
this.lastMessageAt = const Value.absent(),
···
9710
10187
});
9711
10188
DmConvosCompanion.insert({
9712
10189
required String convoId,
10190
+
required String ownerDid,
9713
10191
required String membersJson,
9714
10192
this.lastMessageText = const Value.absent(),
9715
10193
this.lastMessageAt = const Value.absent(),
···
9720
10198
required DateTime cachedAt,
9721
10199
this.rowid = const Value.absent(),
9722
10200
}) : convoId = Value(convoId),
10201
+
ownerDid = Value(ownerDid),
9723
10202
membersJson = Value(membersJson),
9724
10203
cachedAt = Value(cachedAt);
9725
10204
static Insertable<DmConvo> custom({
9726
10205
Expression<String>? convoId,
10206
+
Expression<String>? ownerDid,
9727
10207
Expression<String>? membersJson,
9728
10208
Expression<String>? lastMessageText,
9729
10209
Expression<DateTime>? lastMessageAt,
···
9736
10216
}) {
9737
10217
return RawValuesInsertable({
9738
10218
if (convoId != null) 'convo_id': convoId,
10219
+
if (ownerDid != null) 'owner_did': ownerDid,
9739
10220
if (membersJson != null) 'members_json': membersJson,
9740
10221
if (lastMessageText != null) 'last_message_text': lastMessageText,
9741
10222
if (lastMessageAt != null) 'last_message_at': lastMessageAt,
···
9750
10231
9751
10232
DmConvosCompanion copyWith({
9752
10233
Value<String>? convoId,
10234
+
Value<String>? ownerDid,
9753
10235
Value<String>? membersJson,
9754
10236
Value<String?>? lastMessageText,
9755
10237
Value<DateTime?>? lastMessageAt,
···
9762
10244
}) {
9763
10245
return DmConvosCompanion(
9764
10246
convoId: convoId ?? this.convoId,
10247
+
ownerDid: ownerDid ?? this.ownerDid,
9765
10248
membersJson: membersJson ?? this.membersJson,
9766
10249
lastMessageText: lastMessageText ?? this.lastMessageText,
9767
10250
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
···
9780
10263
if (convoId.present) {
9781
10264
map['convo_id'] = Variable<String>(convoId.value);
9782
10265
}
10266
+
if (ownerDid.present) {
10267
+
map['owner_did'] = Variable<String>(ownerDid.value);
10268
+
}
9783
10269
if (membersJson.present) {
9784
10270
map['members_json'] = Variable<String>(membersJson.value);
9785
10271
}
···
9814
10300
String toString() {
9815
10301
return (StringBuffer('DmConvosCompanion(')
9816
10302
..write('convoId: $convoId, ')
10303
+
..write('ownerDid: $ownerDid, ')
9817
10304
..write('membersJson: $membersJson, ')
9818
10305
..write('lastMessageText: $lastMessageText, ')
9819
10306
..write('lastMessageAt: $lastMessageAt, ')
···
9842
10329
type: DriftSqlType.string,
9843
10330
requiredDuringInsert: true,
9844
10331
);
10332
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
10333
+
@override
10334
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
10335
+
'owner_did',
10336
+
aliasedName,
10337
+
false,
10338
+
type: DriftSqlType.string,
10339
+
requiredDuringInsert: true,
10340
+
);
9845
10341
static const VerificationMeta _convoIdMeta = const VerificationMeta('convoId');
9846
10342
@override
9847
10343
late final GeneratedColumn<String> convoId = GeneratedColumn<String>(
···
9900
10396
@override
9901
10397
List<GeneratedColumn> get $columns => [
9902
10398
messageId,
10399
+
ownerDid,
9903
10400
convoId,
9904
10401
senderDid,
9905
10402
content,
···
9927
10424
} else if (isInserting) {
9928
10425
context.missing(_messageIdMeta);
9929
10426
}
10427
+
if (data.containsKey('owner_did')) {
10428
+
context.handle(
10429
+
_ownerDidMeta,
10430
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
10431
+
);
10432
+
} else if (isInserting) {
10433
+
context.missing(_ownerDidMeta);
10434
+
}
9930
10435
if (data.containsKey('convo_id')) {
9931
10436
context.handle(_convoIdMeta, convoId.isAcceptableOrUnknown(data['convo_id']!, _convoIdMeta));
9932
10437
} else if (isInserting) {
···
9967
10472
}
9968
10473
9969
10474
@override
9970
-
Set<GeneratedColumn> get $primaryKey => {messageId};
10475
+
Set<GeneratedColumn> get $primaryKey => {messageId, ownerDid};
9971
10476
@override
9972
10477
DmMessage map(Map<String, dynamic> data, {String? tablePrefix}) {
9973
10478
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
···
9976
10481
DriftSqlType.string,
9977
10482
data['${effectivePrefix}message_id'],
9978
10483
)!,
10484
+
ownerDid: attachedDatabase.typeMapping.read(
10485
+
DriftSqlType.string,
10486
+
data['${effectivePrefix}owner_did'],
10487
+
)!,
9979
10488
convoId: attachedDatabase.typeMapping.read(
9980
10489
DriftSqlType.string,
9981
10490
data['${effectivePrefix}convo_id'],
···
10012
10521
class DmMessage extends DataClass implements Insertable<DmMessage> {
10013
10522
/// Message ID (unique identifier from API).
10014
10523
final String messageId;
10524
+
10525
+
/// The DID of the user who owns this message view.
10526
+
final String ownerDid;
10015
10527
10016
10528
/// Conversation this message belongs to.
10017
10529
final String convoId;
···
10032
10544
final DateTime cachedAt;
10033
10545
const DmMessage({
10034
10546
required this.messageId,
10547
+
required this.ownerDid,
10035
10548
required this.convoId,
10036
10549
required this.senderDid,
10037
10550
required this.content,
···
10043
10556
Map<String, Expression> toColumns(bool nullToAbsent) {
10044
10557
final map = <String, Expression>{};
10045
10558
map['message_id'] = Variable<String>(messageId);
10559
+
map['owner_did'] = Variable<String>(ownerDid);
10046
10560
map['convo_id'] = Variable<String>(convoId);
10047
10561
map['sender_did'] = Variable<String>(senderDid);
10048
10562
map['content'] = Variable<String>(content);
···
10055
10569
DmMessagesCompanion toCompanion(bool nullToAbsent) {
10056
10570
return DmMessagesCompanion(
10057
10571
messageId: Value(messageId),
10572
+
ownerDid: Value(ownerDid),
10058
10573
convoId: Value(convoId),
10059
10574
senderDid: Value(senderDid),
10060
10575
content: Value(content),
···
10068
10583
serializer ??= driftRuntimeOptions.defaultSerializer;
10069
10584
return DmMessage(
10070
10585
messageId: serializer.fromJson<String>(json['messageId']),
10586
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
10071
10587
convoId: serializer.fromJson<String>(json['convoId']),
10072
10588
senderDid: serializer.fromJson<String>(json['senderDid']),
10073
10589
content: serializer.fromJson<String>(json['content']),
···
10081
10597
serializer ??= driftRuntimeOptions.defaultSerializer;
10082
10598
return <String, dynamic>{
10083
10599
'messageId': serializer.toJson<String>(messageId),
10600
+
'ownerDid': serializer.toJson<String>(ownerDid),
10084
10601
'convoId': serializer.toJson<String>(convoId),
10085
10602
'senderDid': serializer.toJson<String>(senderDid),
10086
10603
'content': serializer.toJson<String>(content),
···
10092
10609
10093
10610
DmMessage copyWith({
10094
10611
String? messageId,
10612
+
String? ownerDid,
10095
10613
String? convoId,
10096
10614
String? senderDid,
10097
10615
String? content,
···
10100
10618
DateTime? cachedAt,
10101
10619
}) => DmMessage(
10102
10620
messageId: messageId ?? this.messageId,
10621
+
ownerDid: ownerDid ?? this.ownerDid,
10103
10622
convoId: convoId ?? this.convoId,
10104
10623
senderDid: senderDid ?? this.senderDid,
10105
10624
content: content ?? this.content,
···
10110
10629
DmMessage copyWithCompanion(DmMessagesCompanion data) {
10111
10630
return DmMessage(
10112
10631
messageId: data.messageId.present ? data.messageId.value : this.messageId,
10632
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
10113
10633
convoId: data.convoId.present ? data.convoId.value : this.convoId,
10114
10634
senderDid: data.senderDid.present ? data.senderDid.value : this.senderDid,
10115
10635
content: data.content.present ? data.content.value : this.content,
···
10123
10643
String toString() {
10124
10644
return (StringBuffer('DmMessage(')
10125
10645
..write('messageId: $messageId, ')
10646
+
..write('ownerDid: $ownerDid, ')
10126
10647
..write('convoId: $convoId, ')
10127
10648
..write('senderDid: $senderDid, ')
10128
10649
..write('content: $content, ')
···
10135
10656
10136
10657
@override
10137
10658
int get hashCode =>
10138
-
Object.hash(messageId, convoId, senderDid, content, sentAt, status, cachedAt);
10659
+
Object.hash(messageId, ownerDid, convoId, senderDid, content, sentAt, status, cachedAt);
10139
10660
@override
10140
10661
bool operator ==(Object other) =>
10141
10662
identical(this, other) ||
10142
10663
(other is DmMessage &&
10143
10664
other.messageId == this.messageId &&
10665
+
other.ownerDid == this.ownerDid &&
10144
10666
other.convoId == this.convoId &&
10145
10667
other.senderDid == this.senderDid &&
10146
10668
other.content == this.content &&
···
10151
10673
10152
10674
class DmMessagesCompanion extends UpdateCompanion<DmMessage> {
10153
10675
final Value<String> messageId;
10676
+
final Value<String> ownerDid;
10154
10677
final Value<String> convoId;
10155
10678
final Value<String> senderDid;
10156
10679
final Value<String> content;
···
10160
10683
final Value<int> rowid;
10161
10684
const DmMessagesCompanion({
10162
10685
this.messageId = const Value.absent(),
10686
+
this.ownerDid = const Value.absent(),
10163
10687
this.convoId = const Value.absent(),
10164
10688
this.senderDid = const Value.absent(),
10165
10689
this.content = const Value.absent(),
···
10170
10694
});
10171
10695
DmMessagesCompanion.insert({
10172
10696
required String messageId,
10697
+
required String ownerDid,
10173
10698
required String convoId,
10174
10699
required String senderDid,
10175
10700
required String content,
···
10178
10703
required DateTime cachedAt,
10179
10704
this.rowid = const Value.absent(),
10180
10705
}) : messageId = Value(messageId),
10706
+
ownerDid = Value(ownerDid),
10181
10707
convoId = Value(convoId),
10182
10708
senderDid = Value(senderDid),
10183
10709
content = Value(content),
···
10186
10712
cachedAt = Value(cachedAt);
10187
10713
static Insertable<DmMessage> custom({
10188
10714
Expression<String>? messageId,
10715
+
Expression<String>? ownerDid,
10189
10716
Expression<String>? convoId,
10190
10717
Expression<String>? senderDid,
10191
10718
Expression<String>? content,
···
10196
10723
}) {
10197
10724
return RawValuesInsertable({
10198
10725
if (messageId != null) 'message_id': messageId,
10726
+
if (ownerDid != null) 'owner_did': ownerDid,
10199
10727
if (convoId != null) 'convo_id': convoId,
10200
10728
if (senderDid != null) 'sender_did': senderDid,
10201
10729
if (content != null) 'content': content,
···
10208
10736
10209
10737
DmMessagesCompanion copyWith({
10210
10738
Value<String>? messageId,
10739
+
Value<String>? ownerDid,
10211
10740
Value<String>? convoId,
10212
10741
Value<String>? senderDid,
10213
10742
Value<String>? content,
···
10218
10747
}) {
10219
10748
return DmMessagesCompanion(
10220
10749
messageId: messageId ?? this.messageId,
10750
+
ownerDid: ownerDid ?? this.ownerDid,
10221
10751
convoId: convoId ?? this.convoId,
10222
10752
senderDid: senderDid ?? this.senderDid,
10223
10753
content: content ?? this.content,
···
10233
10763
final map = <String, Expression>{};
10234
10764
if (messageId.present) {
10235
10765
map['message_id'] = Variable<String>(messageId.value);
10766
+
}
10767
+
if (ownerDid.present) {
10768
+
map['owner_did'] = Variable<String>(ownerDid.value);
10236
10769
}
10237
10770
if (convoId.present) {
10238
10771
map['convo_id'] = Variable<String>(convoId.value);
···
10262
10795
String toString() {
10263
10796
return (StringBuffer('DmMessagesCompanion(')
10264
10797
..write('messageId: $messageId, ')
10798
+
..write('ownerDid: $ownerDid, ')
10265
10799
..write('convoId: $convoId, ')
10266
10800
..write('senderDid: $senderDid, ')
10267
10801
..write('content: $content, ')
···
10288
10822
type: DriftSqlType.string,
10289
10823
requiredDuringInsert: true,
10290
10824
);
10825
+
static const VerificationMeta _ownerDidMeta = const VerificationMeta('ownerDid');
10826
+
@override
10827
+
late final GeneratedColumn<String> ownerDid = GeneratedColumn<String>(
10828
+
'owner_did',
10829
+
aliasedName,
10830
+
false,
10831
+
type: DriftSqlType.string,
10832
+
requiredDuringInsert: true,
10833
+
);
10291
10834
static const VerificationMeta _convoIdMeta = const VerificationMeta('convoId');
10292
10835
@override
10293
10836
late final GeneratedColumn<String> convoId = GeneratedColumn<String>(
···
10355
10898
@override
10356
10899
List<GeneratedColumn> get $columns => [
10357
10900
outboxId,
10901
+
ownerDid,
10358
10902
convoId,
10359
10903
messageText,
10360
10904
status,
···
10383
10927
} else if (isInserting) {
10384
10928
context.missing(_outboxIdMeta);
10385
10929
}
10930
+
if (data.containsKey('owner_did')) {
10931
+
context.handle(
10932
+
_ownerDidMeta,
10933
+
ownerDid.isAcceptableOrUnknown(data['owner_did']!, _ownerDidMeta),
10934
+
);
10935
+
} else if (isInserting) {
10936
+
context.missing(_ownerDidMeta);
10937
+
}
10386
10938
if (data.containsKey('convo_id')) {
10387
10939
context.handle(_convoIdMeta, convoId.isAcceptableOrUnknown(data['convo_id']!, _convoIdMeta));
10388
10940
} else if (isInserting) {
···
10440
10992
DriftSqlType.string,
10441
10993
data['${effectivePrefix}outbox_id'],
10442
10994
)!,
10995
+
ownerDid: attachedDatabase.typeMapping.read(
10996
+
DriftSqlType.string,
10997
+
data['${effectivePrefix}owner_did'],
10998
+
)!,
10443
10999
convoId: attachedDatabase.typeMapping.read(
10444
11000
DriftSqlType.string,
10445
11001
data['${effectivePrefix}convo_id'],
···
10480
11036
class DmOutboxData extends DataClass implements Insertable<DmOutboxData> {
10481
11037
/// Local UUID for this outbox item.
10482
11038
final String outboxId;
11039
+
11040
+
/// The DID of the user who sent this message.
11041
+
final String ownerDid;
10483
11042
10484
11043
/// Conversation to send the message to.
10485
11044
final String convoId;
···
10503
11062
final String? errorMessage;
10504
11063
const DmOutboxData({
10505
11064
required this.outboxId,
11065
+
required this.ownerDid,
10506
11066
required this.convoId,
10507
11067
required this.messageText,
10508
11068
required this.status,
···
10515
11075
Map<String, Expression> toColumns(bool nullToAbsent) {
10516
11076
final map = <String, Expression>{};
10517
11077
map['outbox_id'] = Variable<String>(outboxId);
11078
+
map['owner_did'] = Variable<String>(ownerDid);
10518
11079
map['convo_id'] = Variable<String>(convoId);
10519
11080
map['message_text'] = Variable<String>(messageText);
10520
11081
map['status'] = Variable<String>(status);
···
10532
11093
DmOutboxCompanion toCompanion(bool nullToAbsent) {
10533
11094
return DmOutboxCompanion(
10534
11095
outboxId: Value(outboxId),
11096
+
ownerDid: Value(ownerDid),
10535
11097
convoId: Value(convoId),
10536
11098
messageText: Value(messageText),
10537
11099
status: Value(status),
···
10550
11112
serializer ??= driftRuntimeOptions.defaultSerializer;
10551
11113
return DmOutboxData(
10552
11114
outboxId: serializer.fromJson<String>(json['outboxId']),
11115
+
ownerDid: serializer.fromJson<String>(json['ownerDid']),
10553
11116
convoId: serializer.fromJson<String>(json['convoId']),
10554
11117
messageText: serializer.fromJson<String>(json['messageText']),
10555
11118
status: serializer.fromJson<String>(json['status']),
···
10564
11127
serializer ??= driftRuntimeOptions.defaultSerializer;
10565
11128
return <String, dynamic>{
10566
11129
'outboxId': serializer.toJson<String>(outboxId),
11130
+
'ownerDid': serializer.toJson<String>(ownerDid),
10567
11131
'convoId': serializer.toJson<String>(convoId),
10568
11132
'messageText': serializer.toJson<String>(messageText),
10569
11133
'status': serializer.toJson<String>(status),
···
10576
11140
10577
11141
DmOutboxData copyWith({
10578
11142
String? outboxId,
11143
+
String? ownerDid,
10579
11144
String? convoId,
10580
11145
String? messageText,
10581
11146
String? status,
···
10585
11150
Value<String?> errorMessage = const Value.absent(),
10586
11151
}) => DmOutboxData(
10587
11152
outboxId: outboxId ?? this.outboxId,
11153
+
ownerDid: ownerDid ?? this.ownerDid,
10588
11154
convoId: convoId ?? this.convoId,
10589
11155
messageText: messageText ?? this.messageText,
10590
11156
status: status ?? this.status,
···
10596
11162
DmOutboxData copyWithCompanion(DmOutboxCompanion data) {
10597
11163
return DmOutboxData(
10598
11164
outboxId: data.outboxId.present ? data.outboxId.value : this.outboxId,
11165
+
ownerDid: data.ownerDid.present ? data.ownerDid.value : this.ownerDid,
10599
11166
convoId: data.convoId.present ? data.convoId.value : this.convoId,
10600
11167
messageText: data.messageText.present ? data.messageText.value : this.messageText,
10601
11168
status: data.status.present ? data.status.value : this.status,
···
10610
11177
String toString() {
10611
11178
return (StringBuffer('DmOutboxData(')
10612
11179
..write('outboxId: $outboxId, ')
11180
+
..write('ownerDid: $ownerDid, ')
10613
11181
..write('convoId: $convoId, ')
10614
11182
..write('messageText: $messageText, ')
10615
11183
..write('status: $status, ')
···
10624
11192
@override
10625
11193
int get hashCode => Object.hash(
10626
11194
outboxId,
11195
+
ownerDid,
10627
11196
convoId,
10628
11197
messageText,
10629
11198
status,
···
10637
11206
identical(this, other) ||
10638
11207
(other is DmOutboxData &&
10639
11208
other.outboxId == this.outboxId &&
11209
+
other.ownerDid == this.ownerDid &&
10640
11210
other.convoId == this.convoId &&
10641
11211
other.messageText == this.messageText &&
10642
11212
other.status == this.status &&
···
10648
11218
10649
11219
class DmOutboxCompanion extends UpdateCompanion<DmOutboxData> {
10650
11220
final Value<String> outboxId;
11221
+
final Value<String> ownerDid;
10651
11222
final Value<String> convoId;
10652
11223
final Value<String> messageText;
10653
11224
final Value<String> status;
···
10658
11229
final Value<int> rowid;
10659
11230
const DmOutboxCompanion({
10660
11231
this.outboxId = const Value.absent(),
11232
+
this.ownerDid = const Value.absent(),
10661
11233
this.convoId = const Value.absent(),
10662
11234
this.messageText = const Value.absent(),
10663
11235
this.status = const Value.absent(),
···
10669
11241
});
10670
11242
DmOutboxCompanion.insert({
10671
11243
required String outboxId,
11244
+
required String ownerDid,
10672
11245
required String convoId,
10673
11246
required String messageText,
10674
11247
required String status,
···
10678
11251
this.errorMessage = const Value.absent(),
10679
11252
this.rowid = const Value.absent(),
10680
11253
}) : outboxId = Value(outboxId),
11254
+
ownerDid = Value(ownerDid),
10681
11255
convoId = Value(convoId),
10682
11256
messageText = Value(messageText),
10683
11257
status = Value(status),
10684
11258
createdAt = Value(createdAt);
10685
11259
static Insertable<DmOutboxData> custom({
10686
11260
Expression<String>? outboxId,
11261
+
Expression<String>? ownerDid,
10687
11262
Expression<String>? convoId,
10688
11263
Expression<String>? messageText,
10689
11264
Expression<String>? status,
···
10695
11270
}) {
10696
11271
return RawValuesInsertable({
10697
11272
if (outboxId != null) 'outbox_id': outboxId,
11273
+
if (ownerDid != null) 'owner_did': ownerDid,
10698
11274
if (convoId != null) 'convo_id': convoId,
10699
11275
if (messageText != null) 'message_text': messageText,
10700
11276
if (status != null) 'status': status,
···
10708
11284
10709
11285
DmOutboxCompanion copyWith({
10710
11286
Value<String>? outboxId,
11287
+
Value<String>? ownerDid,
10711
11288
Value<String>? convoId,
10712
11289
Value<String>? messageText,
10713
11290
Value<String>? status,
···
10719
11296
}) {
10720
11297
return DmOutboxCompanion(
10721
11298
outboxId: outboxId ?? this.outboxId,
11299
+
ownerDid: ownerDid ?? this.ownerDid,
10722
11300
convoId: convoId ?? this.convoId,
10723
11301
messageText: messageText ?? this.messageText,
10724
11302
status: status ?? this.status,
···
10736
11314
if (outboxId.present) {
10737
11315
map['outbox_id'] = Variable<String>(outboxId.value);
10738
11316
}
11317
+
if (ownerDid.present) {
11318
+
map['owner_did'] = Variable<String>(ownerDid.value);
11319
+
}
10739
11320
if (convoId.present) {
10740
11321
map['convo_id'] = Variable<String>(convoId.value);
10741
11322
}
···
10767
11348
String toString() {
10768
11349
return (StringBuffer('DmOutboxCompanion(')
10769
11350
..write('outboxId: $outboxId, ')
11351
+
..write('ownerDid: $ownerDid, ')
10770
11352
..write('convoId: $convoId, ')
10771
11353
..write('messageText: $messageText, ')
10772
11354
..write('status: $status, ')
···
12269
12851
FeedContentItemsCompanion Function({
12270
12852
required String feedKey,
12271
12853
required String postUri,
12854
+
required String ownerDid,
12272
12855
Value<String?> reason,
12273
12856
required String sortKey,
12274
12857
Value<int> rowid,
···
12277
12860
FeedContentItemsCompanion Function({
12278
12861
Value<String> feedKey,
12279
12862
Value<String> postUri,
12863
+
Value<String> ownerDid,
12280
12864
Value<String?> reason,
12281
12865
Value<String> sortKey,
12282
12866
Value<int> rowid,
···
12314
12898
ColumnFilters<String> get feedKey =>
12315
12899
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnFilters(column));
12316
12900
12901
+
ColumnFilters<String> get ownerDid =>
12902
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
12903
+
12317
12904
ColumnFilters<String> get reason =>
12318
12905
$composableBuilder(column: $table.reason, builder: (column) => ColumnFilters(column));
12319
12906
···
12352
12939
ColumnOrderings<String> get feedKey =>
12353
12940
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnOrderings(column));
12354
12941
12942
+
ColumnOrderings<String> get ownerDid =>
12943
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
12944
+
12355
12945
ColumnOrderings<String> get reason =>
12356
12946
$composableBuilder(column: $table.reason, builder: (column) => ColumnOrderings(column));
12357
12947
···
12389
12979
});
12390
12980
GeneratedColumn<String> get feedKey =>
12391
12981
$composableBuilder(column: $table.feedKey, builder: (column) => column);
12982
+
12983
+
GeneratedColumn<String> get ownerDid =>
12984
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
12392
12985
12393
12986
GeneratedColumn<String> get reason =>
12394
12987
$composableBuilder(column: $table.reason, builder: (column) => column);
···
12446
13039
({
12447
13040
Value<String> feedKey = const Value.absent(),
12448
13041
Value<String> postUri = const Value.absent(),
13042
+
Value<String> ownerDid = const Value.absent(),
12449
13043
Value<String?> reason = const Value.absent(),
12450
13044
Value<String> sortKey = const Value.absent(),
12451
13045
Value<int> rowid = const Value.absent(),
12452
13046
}) => FeedContentItemsCompanion(
12453
13047
feedKey: feedKey,
12454
13048
postUri: postUri,
13049
+
ownerDid: ownerDid,
12455
13050
reason: reason,
12456
13051
sortKey: sortKey,
12457
13052
rowid: rowid,
···
12460
13055
({
12461
13056
required String feedKey,
12462
13057
required String postUri,
13058
+
required String ownerDid,
12463
13059
Value<String?> reason = const Value.absent(),
12464
13060
required String sortKey,
12465
13061
Value<int> rowid = const Value.absent(),
12466
13062
}) => FeedContentItemsCompanion.insert(
12467
13063
feedKey: feedKey,
12468
13064
postUri: postUri,
13065
+
ownerDid: ownerDid,
12469
13066
reason: reason,
12470
13067
sortKey: sortKey,
12471
13068
rowid: rowid,
···
12665
13262
typedef $$FeedCursorsTableCreateCompanionBuilder =
12666
13263
FeedCursorsCompanion Function({
12667
13264
required String feedKey,
13265
+
required String ownerDid,
12668
13266
required String cursor,
12669
13267
Value<DateTime?> lastUpdated,
12670
13268
Value<int> rowid,
···
12672
13270
typedef $$FeedCursorsTableUpdateCompanionBuilder =
12673
13271
FeedCursorsCompanion Function({
12674
13272
Value<String> feedKey,
13273
+
Value<String> ownerDid,
12675
13274
Value<String> cursor,
12676
13275
Value<DateTime?> lastUpdated,
12677
13276
Value<int> rowid,
···
12687
13286
});
12688
13287
ColumnFilters<String> get feedKey =>
12689
13288
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnFilters(column));
13289
+
13290
+
ColumnFilters<String> get ownerDid =>
13291
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
12690
13292
12691
13293
ColumnFilters<String> get cursor =>
12692
13294
$composableBuilder(column: $table.cursor, builder: (column) => ColumnFilters(column));
···
12706
13308
ColumnOrderings<String> get feedKey =>
12707
13309
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnOrderings(column));
12708
13310
13311
+
ColumnOrderings<String> get ownerDid =>
13312
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
13313
+
12709
13314
ColumnOrderings<String> get cursor =>
12710
13315
$composableBuilder(column: $table.cursor, builder: (column) => ColumnOrderings(column));
12711
13316
···
12724
13329
GeneratedColumn<String> get feedKey =>
12725
13330
$composableBuilder(column: $table.feedKey, builder: (column) => column);
12726
13331
13332
+
GeneratedColumn<String> get ownerDid =>
13333
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
13334
+
12727
13335
GeneratedColumn<String> get cursor =>
12728
13336
$composableBuilder(column: $table.cursor, builder: (column) => column);
12729
13337
···
12758
13366
updateCompanionCallback:
12759
13367
({
12760
13368
Value<String> feedKey = const Value.absent(),
13369
+
Value<String> ownerDid = const Value.absent(),
12761
13370
Value<String> cursor = const Value.absent(),
12762
13371
Value<DateTime?> lastUpdated = const Value.absent(),
12763
13372
Value<int> rowid = const Value.absent(),
12764
13373
}) => FeedCursorsCompanion(
12765
13374
feedKey: feedKey,
13375
+
ownerDid: ownerDid,
12766
13376
cursor: cursor,
12767
13377
lastUpdated: lastUpdated,
12768
13378
rowid: rowid,
···
12770
13380
createCompanionCallback:
12771
13381
({
12772
13382
required String feedKey,
13383
+
required String ownerDid,
12773
13384
required String cursor,
12774
13385
Value<DateTime?> lastUpdated = const Value.absent(),
12775
13386
Value<int> rowid = const Value.absent(),
12776
13387
}) => FeedCursorsCompanion.insert(
12777
13388
feedKey: feedKey,
13389
+
ownerDid: ownerDid,
12778
13390
cursor: cursor,
12779
13391
lastUpdated: lastUpdated,
12780
13392
rowid: rowid,
···
13485
14097
typedef $$SavedFeedsTableCreateCompanionBuilder =
13486
14098
SavedFeedsCompanion Function({
13487
14099
required String uri,
14100
+
required String ownerDid,
13488
14101
required String displayName,
13489
14102
Value<String?> description,
13490
14103
Value<String?> avatar,
···
13499
14112
typedef $$SavedFeedsTableUpdateCompanionBuilder =
13500
14113
SavedFeedsCompanion Function({
13501
14114
Value<String> uri,
14115
+
Value<String> ownerDid,
13502
14116
Value<String> displayName,
13503
14117
Value<String?> description,
13504
14118
Value<String?> avatar,
···
13542
14156
ColumnFilters<String> get uri =>
13543
14157
$composableBuilder(column: $table.uri, builder: (column) => ColumnFilters(column));
13544
14158
14159
+
ColumnFilters<String> get ownerDid =>
14160
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
14161
+
13545
14162
ColumnFilters<String> get displayName =>
13546
14163
$composableBuilder(column: $table.displayName, builder: (column) => ColumnFilters(column));
13547
14164
···
13599
14216
ColumnOrderings<String> get uri =>
13600
14217
$composableBuilder(column: $table.uri, builder: (column) => ColumnOrderings(column));
13601
14218
14219
+
ColumnOrderings<String> get ownerDid =>
14220
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
14221
+
13602
14222
ColumnOrderings<String> get displayName =>
13603
14223
$composableBuilder(column: $table.displayName, builder: (column) => ColumnOrderings(column));
13604
14224
···
13655
14275
});
13656
14276
GeneratedColumn<String> get uri =>
13657
14277
$composableBuilder(column: $table.uri, builder: (column) => column);
14278
+
14279
+
GeneratedColumn<String> get ownerDid =>
14280
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
13658
14281
13659
14282
GeneratedColumn<String> get displayName =>
13660
14283
$composableBuilder(column: $table.displayName, builder: (column) => column);
···
13727
14350
updateCompanionCallback:
13728
14351
({
13729
14352
Value<String> uri = const Value.absent(),
14353
+
Value<String> ownerDid = const Value.absent(),
13730
14354
Value<String> displayName = const Value.absent(),
13731
14355
Value<String?> description = const Value.absent(),
13732
14356
Value<String?> avatar = const Value.absent(),
···
13739
14363
Value<int> rowid = const Value.absent(),
13740
14364
}) => SavedFeedsCompanion(
13741
14365
uri: uri,
14366
+
ownerDid: ownerDid,
13742
14367
displayName: displayName,
13743
14368
description: description,
13744
14369
avatar: avatar,
···
13753
14378
createCompanionCallback:
13754
14379
({
13755
14380
required String uri,
14381
+
required String ownerDid,
13756
14382
required String displayName,
13757
14383
Value<String?> description = const Value.absent(),
13758
14384
Value<String?> avatar = const Value.absent(),
···
13765
14391
Value<int> rowid = const Value.absent(),
13766
14392
}) => SavedFeedsCompanion.insert(
13767
14393
uri: uri,
14394
+
ownerDid: ownerDid,
13768
14395
displayName: displayName,
13769
14396
description: description,
13770
14397
avatar: avatar,
···
13840
14467
typedef $$PreferenceSyncQueueTableCreateCompanionBuilder =
13841
14468
PreferenceSyncQueueCompanion Function({
13842
14469
Value<int> id,
14470
+
required String ownerDid,
13843
14471
Value<String> category,
13844
14472
required String type,
13845
14473
required String payload,
···
13849
14477
typedef $$PreferenceSyncQueueTableUpdateCompanionBuilder =
13850
14478
PreferenceSyncQueueCompanion Function({
13851
14479
Value<int> id,
14480
+
Value<String> ownerDid,
13852
14481
Value<String> category,
13853
14482
Value<String> type,
13854
14483
Value<String> payload,
···
13867
14496
});
13868
14497
ColumnFilters<int> get id =>
13869
14498
$composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column));
14499
+
14500
+
ColumnFilters<String> get ownerDid =>
14501
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
13870
14502
13871
14503
ColumnFilters<String> get category =>
13872
14504
$composableBuilder(column: $table.category, builder: (column) => ColumnFilters(column));
···
13896
14528
ColumnOrderings<int> get id =>
13897
14529
$composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column));
13898
14530
14531
+
ColumnOrderings<String> get ownerDid =>
14532
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
14533
+
13899
14534
ColumnOrderings<String> get category =>
13900
14535
$composableBuilder(column: $table.category, builder: (column) => ColumnOrderings(column));
13901
14536
···
13923
14558
});
13924
14559
GeneratedColumn<int> get id =>
13925
14560
$composableBuilder(column: $table.id, builder: (column) => column);
14561
+
14562
+
GeneratedColumn<String> get ownerDid =>
14563
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
13926
14564
13927
14565
GeneratedColumn<String> get category =>
13928
14566
$composableBuilder(column: $table.category, builder: (column) => column);
···
13972
14610
updateCompanionCallback:
13973
14611
({
13974
14612
Value<int> id = const Value.absent(),
14613
+
Value<String> ownerDid = const Value.absent(),
13975
14614
Value<String> category = const Value.absent(),
13976
14615
Value<String> type = const Value.absent(),
13977
14616
Value<String> payload = const Value.absent(),
···
13979
14618
Value<int> retryCount = const Value.absent(),
13980
14619
}) => PreferenceSyncQueueCompanion(
13981
14620
id: id,
14621
+
ownerDid: ownerDid,
13982
14622
category: category,
13983
14623
type: type,
13984
14624
payload: payload,
···
13988
14628
createCompanionCallback:
13989
14629
({
13990
14630
Value<int> id = const Value.absent(),
14631
+
required String ownerDid,
13991
14632
Value<String> category = const Value.absent(),
13992
14633
required String type,
13993
14634
required String payload,
···
13995
14636
Value<int> retryCount = const Value.absent(),
13996
14637
}) => PreferenceSyncQueueCompanion.insert(
13997
14638
id: id,
14639
+
ownerDid: ownerDid,
13998
14640
category: category,
13999
14641
type: type,
14000
14642
payload: payload,
···
14823
15465
>;
14824
15466
typedef $$ProfileRelationshipsTableCreateCompanionBuilder =
14825
15467
ProfileRelationshipsCompanion Function({
15468
+
required String ownerDid,
14826
15469
required String profileDid,
14827
15470
Value<bool> following,
14828
15471
Value<String?> followingUri,
···
14838
15481
});
14839
15482
typedef $$ProfileRelationshipsTableUpdateCompanionBuilder =
14840
15483
ProfileRelationshipsCompanion Function({
15484
+
Value<String> ownerDid,
14841
15485
Value<String> profileDid,
14842
15486
Value<bool> following,
14843
15487
Value<String?> followingUri,
···
14882
15526
super.$addJoinBuilderToRootComposer,
14883
15527
super.$removeJoinBuilderFromRootComposer,
14884
15528
});
15529
+
ColumnFilters<String> get ownerDid =>
15530
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
15531
+
14885
15532
ColumnFilters<bool> get following =>
14886
15533
$composableBuilder(column: $table.following, builder: (column) => ColumnFilters(column));
14887
15534
···
14943
15590
super.$addJoinBuilderToRootComposer,
14944
15591
super.$removeJoinBuilderFromRootComposer,
14945
15592
});
15593
+
ColumnOrderings<String> get ownerDid =>
15594
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
15595
+
14946
15596
ColumnOrderings<bool> get following =>
14947
15597
$composableBuilder(column: $table.following, builder: (column) => ColumnOrderings(column));
14948
15598
···
15006
15656
super.$addJoinBuilderToRootComposer,
15007
15657
super.$removeJoinBuilderFromRootComposer,
15008
15658
});
15659
+
GeneratedColumn<String> get ownerDid =>
15660
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
15661
+
15009
15662
GeneratedColumn<bool> get following =>
15010
15663
$composableBuilder(column: $table.following, builder: (column) => column);
15011
15664
···
15084
15737
$$ProfileRelationshipsTableAnnotationComposer($db: db, $table: table),
15085
15738
updateCompanionCallback:
15086
15739
({
15740
+
Value<String> ownerDid = const Value.absent(),
15087
15741
Value<String> profileDid = const Value.absent(),
15088
15742
Value<bool> following = const Value.absent(),
15089
15743
Value<String?> followingUri = const Value.absent(),
···
15097
15751
Value<DateTime> updatedAt = const Value.absent(),
15098
15752
Value<int> rowid = const Value.absent(),
15099
15753
}) => ProfileRelationshipsCompanion(
15754
+
ownerDid: ownerDid,
15100
15755
profileDid: profileDid,
15101
15756
following: following,
15102
15757
followingUri: followingUri,
···
15112
15767
),
15113
15768
createCompanionCallback:
15114
15769
({
15770
+
required String ownerDid,
15115
15771
required String profileDid,
15116
15772
Value<bool> following = const Value.absent(),
15117
15773
Value<String?> followingUri = const Value.absent(),
···
15125
15781
required DateTime updatedAt,
15126
15782
Value<int> rowid = const Value.absent(),
15127
15783
}) => ProfileRelationshipsCompanion.insert(
15784
+
ownerDid: ownerDid,
15128
15785
profileDid: profileDid,
15129
15786
following: following,
15130
15787
followingUri: followingUri,
···
15643
16300
typedef $$BlueskyPreferencesTableCreateCompanionBuilder =
15644
16301
BlueskyPreferencesCompanion Function({
15645
16302
required String type,
16303
+
required String ownerDid,
15646
16304
required String data,
15647
16305
required DateTime lastSynced,
15648
16306
Value<int> rowid,
···
15650
16308
typedef $$BlueskyPreferencesTableUpdateCompanionBuilder =
15651
16309
BlueskyPreferencesCompanion Function({
15652
16310
Value<String> type,
16311
+
Value<String> ownerDid,
15653
16312
Value<String> data,
15654
16313
Value<DateTime> lastSynced,
15655
16314
Value<int> rowid,
···
15667
16326
ColumnFilters<String> get type =>
15668
16327
$composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column));
15669
16328
16329
+
ColumnFilters<String> get ownerDid =>
16330
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
16331
+
15670
16332
ColumnFilters<String> get data =>
15671
16333
$composableBuilder(column: $table.data, builder: (column) => ColumnFilters(column));
15672
16334
···
15686
16348
ColumnOrderings<String> get type =>
15687
16349
$composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column));
15688
16350
16351
+
ColumnOrderings<String> get ownerDid =>
16352
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
16353
+
15689
16354
ColumnOrderings<String> get data =>
15690
16355
$composableBuilder(column: $table.data, builder: (column) => ColumnOrderings(column));
15691
16356
···
15705
16370
GeneratedColumn<String> get type =>
15706
16371
$composableBuilder(column: $table.type, builder: (column) => column);
15707
16372
16373
+
GeneratedColumn<String> get ownerDid =>
16374
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
16375
+
15708
16376
GeneratedColumn<String> get data =>
15709
16377
$composableBuilder(column: $table.data, builder: (column) => column);
15710
16378
···
15744
16412
updateCompanionCallback:
15745
16413
({
15746
16414
Value<String> type = const Value.absent(),
16415
+
Value<String> ownerDid = const Value.absent(),
15747
16416
Value<String> data = const Value.absent(),
15748
16417
Value<DateTime> lastSynced = const Value.absent(),
15749
16418
Value<int> rowid = const Value.absent(),
15750
16419
}) => BlueskyPreferencesCompanion(
15751
16420
type: type,
16421
+
ownerDid: ownerDid,
15752
16422
data: data,
15753
16423
lastSynced: lastSynced,
15754
16424
rowid: rowid,
···
15756
16426
createCompanionCallback:
15757
16427
({
15758
16428
required String type,
16429
+
required String ownerDid,
15759
16430
required String data,
15760
16431
required DateTime lastSynced,
15761
16432
Value<int> rowid = const Value.absent(),
15762
16433
}) => BlueskyPreferencesCompanion.insert(
15763
16434
type: type,
16435
+
ownerDid: ownerDid,
15764
16436
data: data,
15765
16437
lastSynced: lastSynced,
15766
16438
rowid: rowid,
···
16156
16828
typedef $$NotificationsTableCreateCompanionBuilder =
16157
16829
NotificationsCompanion Function({
16158
16830
required String uri,
16831
+
required String ownerDid,
16159
16832
required String actorDid,
16160
16833
required String type,
16161
16834
Value<String?> reasonSubjectUri,
···
16169
16842
typedef $$NotificationsTableUpdateCompanionBuilder =
16170
16843
NotificationsCompanion Function({
16171
16844
Value<String> uri,
16845
+
Value<String> ownerDid,
16172
16846
Value<String> actorDid,
16173
16847
Value<String> type,
16174
16848
Value<String?> reasonSubjectUri,
···
16211
16885
ColumnFilters<String> get uri =>
16212
16886
$composableBuilder(column: $table.uri, builder: (column) => ColumnFilters(column));
16213
16887
16888
+
ColumnFilters<String> get ownerDid =>
16889
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
16890
+
16214
16891
ColumnFilters<String> get type =>
16215
16892
$composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column));
16216
16893
···
16265
16942
ColumnOrderings<String> get uri =>
16266
16943
$composableBuilder(column: $table.uri, builder: (column) => ColumnOrderings(column));
16267
16944
16945
+
ColumnOrderings<String> get ownerDid =>
16946
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
16947
+
16268
16948
ColumnOrderings<String> get type =>
16269
16949
$composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column));
16270
16950
···
16318
16998
});
16319
16999
GeneratedColumn<String> get uri =>
16320
17000
$composableBuilder(column: $table.uri, builder: (column) => column);
17001
+
17002
+
GeneratedColumn<String> get ownerDid =>
17003
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
16321
17004
16322
17005
GeneratedColumn<String> get type =>
16323
17006
$composableBuilder(column: $table.type, builder: (column) => column);
···
16389
17072
updateCompanionCallback:
16390
17073
({
16391
17074
Value<String> uri = const Value.absent(),
17075
+
Value<String> ownerDid = const Value.absent(),
16392
17076
Value<String> actorDid = const Value.absent(),
16393
17077
Value<String> type = const Value.absent(),
16394
17078
Value<String?> reasonSubjectUri = const Value.absent(),
···
16400
17084
Value<int> rowid = const Value.absent(),
16401
17085
}) => NotificationsCompanion(
16402
17086
uri: uri,
17087
+
ownerDid: ownerDid,
16403
17088
actorDid: actorDid,
16404
17089
type: type,
16405
17090
reasonSubjectUri: reasonSubjectUri,
···
16413
17098
createCompanionCallback:
16414
17099
({
16415
17100
required String uri,
17101
+
required String ownerDid,
16416
17102
required String actorDid,
16417
17103
required String type,
16418
17104
Value<String?> reasonSubjectUri = const Value.absent(),
···
16424
17110
Value<int> rowid = const Value.absent(),
16425
17111
}) => NotificationsCompanion.insert(
16426
17112
uri: uri,
17113
+
ownerDid: ownerDid,
16427
17114
actorDid: actorDid,
16428
17115
type: type,
16429
17116
reasonSubjectUri: reasonSubjectUri,
···
16498
17185
typedef $$NotificationCursorsTableCreateCompanionBuilder =
16499
17186
NotificationCursorsCompanion Function({
16500
17187
required String feedKey,
17188
+
required String ownerDid,
16501
17189
required String cursor,
16502
17190
Value<DateTime?> lastUpdated,
16503
17191
Value<int> rowid,
···
16505
17193
typedef $$NotificationCursorsTableUpdateCompanionBuilder =
16506
17194
NotificationCursorsCompanion Function({
16507
17195
Value<String> feedKey,
17196
+
Value<String> ownerDid,
16508
17197
Value<String> cursor,
16509
17198
Value<DateTime?> lastUpdated,
16510
17199
Value<int> rowid,
···
16521
17210
});
16522
17211
ColumnFilters<String> get feedKey =>
16523
17212
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnFilters(column));
17213
+
17214
+
ColumnFilters<String> get ownerDid =>
17215
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
16524
17216
16525
17217
ColumnFilters<String> get cursor =>
16526
17218
$composableBuilder(column: $table.cursor, builder: (column) => ColumnFilters(column));
···
16541
17233
ColumnOrderings<String> get feedKey =>
16542
17234
$composableBuilder(column: $table.feedKey, builder: (column) => ColumnOrderings(column));
16543
17235
17236
+
ColumnOrderings<String> get ownerDid =>
17237
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
17238
+
16544
17239
ColumnOrderings<String> get cursor =>
16545
17240
$composableBuilder(column: $table.cursor, builder: (column) => ColumnOrderings(column));
16546
17241
···
16559
17254
});
16560
17255
GeneratedColumn<String> get feedKey =>
16561
17256
$composableBuilder(column: $table.feedKey, builder: (column) => column);
17257
+
17258
+
GeneratedColumn<String> get ownerDid =>
17259
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
16562
17260
16563
17261
GeneratedColumn<String> get cursor =>
16564
17262
$composableBuilder(column: $table.cursor, builder: (column) => column);
···
16599
17297
updateCompanionCallback:
16600
17298
({
16601
17299
Value<String> feedKey = const Value.absent(),
17300
+
Value<String> ownerDid = const Value.absent(),
16602
17301
Value<String> cursor = const Value.absent(),
16603
17302
Value<DateTime?> lastUpdated = const Value.absent(),
16604
17303
Value<int> rowid = const Value.absent(),
16605
17304
}) => NotificationCursorsCompanion(
16606
17305
feedKey: feedKey,
17306
+
ownerDid: ownerDid,
16607
17307
cursor: cursor,
16608
17308
lastUpdated: lastUpdated,
16609
17309
rowid: rowid,
···
16611
17311
createCompanionCallback:
16612
17312
({
16613
17313
required String feedKey,
17314
+
required String ownerDid,
16614
17315
required String cursor,
16615
17316
Value<DateTime?> lastUpdated = const Value.absent(),
16616
17317
Value<int> rowid = const Value.absent(),
16617
17318
}) => NotificationCursorsCompanion.insert(
16618
17319
feedKey: feedKey,
17320
+
ownerDid: ownerDid,
16619
17321
cursor: cursor,
16620
17322
lastUpdated: lastUpdated,
16621
17323
rowid: rowid,
···
16647
17349
typedef $$NotificationsSyncQueueTableCreateCompanionBuilder =
16648
17350
NotificationsSyncQueueCompanion Function({
16649
17351
Value<int> id,
17352
+
required String ownerDid,
16650
17353
required String type,
16651
17354
required String seenAt,
16652
17355
required DateTime createdAt,
···
16655
17358
typedef $$NotificationsSyncQueueTableUpdateCompanionBuilder =
16656
17359
NotificationsSyncQueueCompanion Function({
16657
17360
Value<int> id,
17361
+
Value<String> ownerDid,
16658
17362
Value<String> type,
16659
17363
Value<String> seenAt,
16660
17364
Value<DateTime> createdAt,
···
16673
17377
ColumnFilters<int> get id =>
16674
17378
$composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column));
16675
17379
17380
+
ColumnFilters<String> get ownerDid =>
17381
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
17382
+
16676
17383
ColumnFilters<String> get type =>
16677
17384
$composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column));
16678
17385
···
16698
17405
ColumnOrderings<int> get id =>
16699
17406
$composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column));
16700
17407
17408
+
ColumnOrderings<String> get ownerDid =>
17409
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
17410
+
16701
17411
ColumnOrderings<String> get type =>
16702
17412
$composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column));
16703
17413
···
16723
17433
GeneratedColumn<int> get id =>
16724
17434
$composableBuilder(column: $table.id, builder: (column) => column);
16725
17435
17436
+
GeneratedColumn<String> get ownerDid =>
17437
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
17438
+
16726
17439
GeneratedColumn<String> get type =>
16727
17440
$composableBuilder(column: $table.type, builder: (column) => column);
16728
17441
···
16772
17485
updateCompanionCallback:
16773
17486
({
16774
17487
Value<int> id = const Value.absent(),
17488
+
Value<String> ownerDid = const Value.absent(),
16775
17489
Value<String> type = const Value.absent(),
16776
17490
Value<String> seenAt = const Value.absent(),
16777
17491
Value<DateTime> createdAt = const Value.absent(),
16778
17492
Value<int> retryCount = const Value.absent(),
16779
17493
}) => NotificationsSyncQueueCompanion(
16780
17494
id: id,
17495
+
ownerDid: ownerDid,
16781
17496
type: type,
16782
17497
seenAt: seenAt,
16783
17498
createdAt: createdAt,
···
16786
17501
createCompanionCallback:
16787
17502
({
16788
17503
Value<int> id = const Value.absent(),
17504
+
required String ownerDid,
16789
17505
required String type,
16790
17506
required String seenAt,
16791
17507
required DateTime createdAt,
16792
17508
Value<int> retryCount = const Value.absent(),
16793
17509
}) => NotificationsSyncQueueCompanion.insert(
16794
17510
id: id,
17511
+
ownerDid: ownerDid,
16795
17512
type: type,
16796
17513
seenAt: seenAt,
16797
17514
createdAt: createdAt,
···
16824
17541
typedef $$DmConvosTableCreateCompanionBuilder =
16825
17542
DmConvosCompanion Function({
16826
17543
required String convoId,
17544
+
required String ownerDid,
16827
17545
required String membersJson,
16828
17546
Value<String?> lastMessageText,
16829
17547
Value<DateTime?> lastMessageAt,
···
16837
17555
typedef $$DmConvosTableUpdateCompanionBuilder =
16838
17556
DmConvosCompanion Function({
16839
17557
Value<String> convoId,
17558
+
Value<String> ownerDid,
16840
17559
Value<String> membersJson,
16841
17560
Value<String?> lastMessageText,
16842
17561
Value<DateTime?> lastMessageAt,
···
16858
17577
});
16859
17578
ColumnFilters<String> get convoId =>
16860
17579
$composableBuilder(column: $table.convoId, builder: (column) => ColumnFilters(column));
17580
+
17581
+
ColumnFilters<String> get ownerDid =>
17582
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
16861
17583
16862
17584
ColumnFilters<String> get membersJson =>
16863
17585
$composableBuilder(column: $table.membersJson, builder: (column) => ColumnFilters(column));
···
16899
17621
ColumnOrderings<String> get convoId =>
16900
17622
$composableBuilder(column: $table.convoId, builder: (column) => ColumnOrderings(column));
16901
17623
17624
+
ColumnOrderings<String> get ownerDid =>
17625
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
17626
+
16902
17627
ColumnOrderings<String> get membersJson =>
16903
17628
$composableBuilder(column: $table.membersJson, builder: (column) => ColumnOrderings(column));
16904
17629
···
16940
17665
});
16941
17666
GeneratedColumn<String> get convoId =>
16942
17667
$composableBuilder(column: $table.convoId, builder: (column) => column);
17668
+
17669
+
GeneratedColumn<String> get ownerDid =>
17670
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
16943
17671
16944
17672
GeneratedColumn<String> get membersJson =>
16945
17673
$composableBuilder(column: $table.membersJson, builder: (column) => column);
···
16993
17721
updateCompanionCallback:
16994
17722
({
16995
17723
Value<String> convoId = const Value.absent(),
17724
+
Value<String> ownerDid = const Value.absent(),
16996
17725
Value<String> membersJson = const Value.absent(),
16997
17726
Value<String?> lastMessageText = const Value.absent(),
16998
17727
Value<DateTime?> lastMessageAt = const Value.absent(),
···
17004
17733
Value<int> rowid = const Value.absent(),
17005
17734
}) => DmConvosCompanion(
17006
17735
convoId: convoId,
17736
+
ownerDid: ownerDid,
17007
17737
membersJson: membersJson,
17008
17738
lastMessageText: lastMessageText,
17009
17739
lastMessageAt: lastMessageAt,
···
17017
17747
createCompanionCallback:
17018
17748
({
17019
17749
required String convoId,
17750
+
required String ownerDid,
17020
17751
required String membersJson,
17021
17752
Value<String?> lastMessageText = const Value.absent(),
17022
17753
Value<DateTime?> lastMessageAt = const Value.absent(),
···
17028
17759
Value<int> rowid = const Value.absent(),
17029
17760
}) => DmConvosCompanion.insert(
17030
17761
convoId: convoId,
17762
+
ownerDid: ownerDid,
17031
17763
membersJson: membersJson,
17032
17764
lastMessageText: lastMessageText,
17033
17765
lastMessageAt: lastMessageAt,
···
17062
17794
typedef $$DmMessagesTableCreateCompanionBuilder =
17063
17795
DmMessagesCompanion Function({
17064
17796
required String messageId,
17797
+
required String ownerDid,
17065
17798
required String convoId,
17066
17799
required String senderDid,
17067
17800
required String content,
···
17073
17806
typedef $$DmMessagesTableUpdateCompanionBuilder =
17074
17807
DmMessagesCompanion Function({
17075
17808
Value<String> messageId,
17809
+
Value<String> ownerDid,
17076
17810
Value<String> convoId,
17077
17811
Value<String> senderDid,
17078
17812
Value<String> content,
···
17112
17846
});
17113
17847
ColumnFilters<String> get messageId =>
17114
17848
$composableBuilder(column: $table.messageId, builder: (column) => ColumnFilters(column));
17849
+
17850
+
ColumnFilters<String> get ownerDid =>
17851
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
17115
17852
17116
17853
ColumnFilters<String> get convoId =>
17117
17854
$composableBuilder(column: $table.convoId, builder: (column) => ColumnFilters(column));
···
17159
17896
ColumnOrderings<String> get messageId =>
17160
17897
$composableBuilder(column: $table.messageId, builder: (column) => ColumnOrderings(column));
17161
17898
17899
+
ColumnOrderings<String> get ownerDid =>
17900
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
17901
+
17162
17902
ColumnOrderings<String> get convoId =>
17163
17903
$composableBuilder(column: $table.convoId, builder: (column) => ColumnOrderings(column));
17164
17904
···
17204
17944
});
17205
17945
GeneratedColumn<String> get messageId =>
17206
17946
$composableBuilder(column: $table.messageId, builder: (column) => column);
17947
+
17948
+
GeneratedColumn<String> get ownerDid =>
17949
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
17207
17950
17208
17951
GeneratedColumn<String> get convoId =>
17209
17952
$composableBuilder(column: $table.convoId, builder: (column) => column);
···
17267
18010
updateCompanionCallback:
17268
18011
({
17269
18012
Value<String> messageId = const Value.absent(),
18013
+
Value<String> ownerDid = const Value.absent(),
17270
18014
Value<String> convoId = const Value.absent(),
17271
18015
Value<String> senderDid = const Value.absent(),
17272
18016
Value<String> content = const Value.absent(),
···
17276
18020
Value<int> rowid = const Value.absent(),
17277
18021
}) => DmMessagesCompanion(
17278
18022
messageId: messageId,
18023
+
ownerDid: ownerDid,
17279
18024
convoId: convoId,
17280
18025
senderDid: senderDid,
17281
18026
content: content,
···
17287
18032
createCompanionCallback:
17288
18033
({
17289
18034
required String messageId,
18035
+
required String ownerDid,
17290
18036
required String convoId,
17291
18037
required String senderDid,
17292
18038
required String content,
···
17296
18042
Value<int> rowid = const Value.absent(),
17297
18043
}) => DmMessagesCompanion.insert(
17298
18044
messageId: messageId,
18045
+
ownerDid: ownerDid,
17299
18046
convoId: convoId,
17300
18047
senderDid: senderDid,
17301
18048
content: content,
···
17368
18115
typedef $$DmOutboxTableCreateCompanionBuilder =
17369
18116
DmOutboxCompanion Function({
17370
18117
required String outboxId,
18118
+
required String ownerDid,
17371
18119
required String convoId,
17372
18120
required String messageText,
17373
18121
required String status,
···
17380
18128
typedef $$DmOutboxTableUpdateCompanionBuilder =
17381
18129
DmOutboxCompanion Function({
17382
18130
Value<String> outboxId,
18131
+
Value<String> ownerDid,
17383
18132
Value<String> convoId,
17384
18133
Value<String> messageText,
17385
18134
Value<String> status,
···
17401
18150
ColumnFilters<String> get outboxId =>
17402
18151
$composableBuilder(column: $table.outboxId, builder: (column) => ColumnFilters(column));
17403
18152
18153
+
ColumnFilters<String> get ownerDid =>
18154
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnFilters(column));
18155
+
17404
18156
ColumnFilters<String> get convoId =>
17405
18157
$composableBuilder(column: $table.convoId, builder: (column) => ColumnFilters(column));
17406
18158
···
17433
18185
});
17434
18186
ColumnOrderings<String> get outboxId =>
17435
18187
$composableBuilder(column: $table.outboxId, builder: (column) => ColumnOrderings(column));
18188
+
18189
+
ColumnOrderings<String> get ownerDid =>
18190
+
$composableBuilder(column: $table.ownerDid, builder: (column) => ColumnOrderings(column));
17436
18191
17437
18192
ColumnOrderings<String> get convoId =>
17438
18193
$composableBuilder(column: $table.convoId, builder: (column) => ColumnOrderings(column));
···
17471
18226
GeneratedColumn<String> get outboxId =>
17472
18227
$composableBuilder(column: $table.outboxId, builder: (column) => column);
17473
18228
18229
+
GeneratedColumn<String> get ownerDid =>
18230
+
$composableBuilder(column: $table.ownerDid, builder: (column) => column);
18231
+
17474
18232
GeneratedColumn<String> get convoId =>
17475
18233
$composableBuilder(column: $table.convoId, builder: (column) => column);
17476
18234
···
17520
18278
updateCompanionCallback:
17521
18279
({
17522
18280
Value<String> outboxId = const Value.absent(),
18281
+
Value<String> ownerDid = const Value.absent(),
17523
18282
Value<String> convoId = const Value.absent(),
17524
18283
Value<String> messageText = const Value.absent(),
17525
18284
Value<String> status = const Value.absent(),
···
17530
18289
Value<int> rowid = const Value.absent(),
17531
18290
}) => DmOutboxCompanion(
17532
18291
outboxId: outboxId,
18292
+
ownerDid: ownerDid,
17533
18293
convoId: convoId,
17534
18294
messageText: messageText,
17535
18295
status: status,
···
17542
18302
createCompanionCallback:
17543
18303
({
17544
18304
required String outboxId,
18305
+
required String ownerDid,
17545
18306
required String convoId,
17546
18307
required String messageText,
17547
18308
required String status,
···
17552
18313
Value<int> rowid = const Value.absent(),
17553
18314
}) => DmOutboxCompanion.insert(
17554
18315
outboxId: outboxId,
18316
+
ownerDid: ownerDid,
17555
18317
convoId: convoId,
17556
18318
messageText: messageText,
17557
18319
status: status,
+26
-16
lib/src/infrastructure/db/daos/bluesky_preferences_dao.dart
+26
-16
lib/src/infrastructure/db/daos/bluesky_preferences_dao.dart
···
18
18
/// Gets a preference by its type identifier.
19
19
///
20
20
/// Returns null if no preference of that type exists.
21
-
Future<BlueskyPreference?> getPreferenceByType(String type) async {
22
-
final query = select(blueskyPreferences)..where((t) => t.type.equals(type));
21
+
Future<BlueskyPreference?> getPreferenceByType(String type, String ownerDid) async {
22
+
final query = select(blueskyPreferences)
23
+
..where((t) => t.type.equals(type) & t.ownerDid.equals(ownerDid));
23
24
return query.getSingleOrNull();
24
25
}
25
26
26
27
/// Watches a preference by its type identifier.
27
28
///
28
29
/// Emits null if no preference of that type exists.
29
-
Stream<BlueskyPreference?> watchPreferenceByType(String type) {
30
-
final query = select(blueskyPreferences)..where((t) => t.type.equals(type));
30
+
Stream<BlueskyPreference?> watchPreferenceByType(String type, String ownerDid) {
31
+
final query = select(blueskyPreferences)
32
+
..where((t) => t.type.equals(type) & t.ownerDid.equals(ownerDid));
31
33
return query.watchSingleOrNull();
32
34
}
33
35
34
-
/// Gets all cached preferences.
35
-
Future<List<BlueskyPreference>> getAllPreferences() async {
36
-
return select(blueskyPreferences).get();
36
+
/// Gets all cached preferences for a specific owner.
37
+
Future<List<BlueskyPreference>> getAllPreferences(String ownerDid) async {
38
+
return (select(blueskyPreferences)..where((t) => t.ownerDid.equals(ownerDid))).get();
37
39
}
38
40
39
-
/// Watches all cached preferences.
40
-
Stream<List<BlueskyPreference>> watchAllPreferences() {
41
-
return select(blueskyPreferences).watch();
41
+
/// Watches all cached preferences for a specific owner.
42
+
Stream<List<BlueskyPreference>> watchAllPreferences(String ownerDid) {
43
+
return (select(blueskyPreferences)..where((t) => t.ownerDid.equals(ownerDid))).watch();
42
44
}
43
45
44
46
/// Inserts or updates a preference.
···
48
50
required String type,
49
51
required String data,
50
52
required DateTime lastSynced,
53
+
required String ownerDid,
51
54
}) async {
52
55
await into(blueskyPreferences).insertOnConflictUpdate(
53
-
BlueskyPreferencesCompanion.insert(type: type, data: data, lastSynced: lastSynced),
56
+
BlueskyPreferencesCompanion.insert(
57
+
type: type,
58
+
ownerDid: ownerDid,
59
+
data: data,
60
+
lastSynced: lastSynced,
61
+
),
54
62
);
55
63
}
56
64
57
65
/// Deletes a preference by its type identifier.
58
66
///
59
67
/// Returns 1 if deleted, 0 if not found.
60
-
Future<int> deletePreference(String type) async {
61
-
return (delete(blueskyPreferences)..where((t) => t.type.equals(type))).go();
68
+
Future<int> deletePreference(String type, String ownerDid) async {
69
+
return (delete(
70
+
blueskyPreferences,
71
+
)..where((t) => t.type.equals(type) & t.ownerDid.equals(ownerDid))).go();
62
72
}
63
73
64
-
/// Clears all cached preferences.
74
+
/// Clears all cached preferences for a specific owner.
65
75
///
66
76
/// Useful when signing out or resetting the app.
67
-
Future<int> clearAll() async {
68
-
return delete(blueskyPreferences).go();
77
+
Future<int> clearAll(String ownerDid) async {
78
+
return (delete(blueskyPreferences)..where((t) => t.ownerDid.equals(ownerDid))).go();
69
79
}
70
80
}
+26
-18
lib/src/infrastructure/db/daos/dm_messages_dao.dart
+26
-18
lib/src/infrastructure/db/daos/dm_messages_dao.dart
···
26
26
});
27
27
}
28
28
29
-
/// Gets a stream of messages for a conversation sorted by sentAt ascending.
30
-
Stream<List<DmMessageWithSender>> watchMessagesByConvo(String convoId) {
29
+
/// Gets a stream of messages for a conversation sorted by sentAt ascending for a specific user.
30
+
Stream<List<DmMessageWithSender>> watchMessagesByConvo(String convoId, String ownerDid) {
31
31
final query = select(
32
32
dmMessages,
33
33
).join([innerJoin(profiles, profiles.did.equalsExp(dmMessages.senderDid))]);
34
-
query.where(dmMessages.convoId.equals(convoId));
34
+
query.where(dmMessages.convoId.equals(convoId) & dmMessages.ownerDid.equals(ownerDid));
35
35
query.orderBy([OrderingTerm.asc(dmMessages.sentAt)]);
36
36
37
37
return query.watch().map((rows) {
···
45
45
}
46
46
47
47
/// Gets messages for a conversation (non-streaming).
48
-
Future<List<DmMessageWithSender>> getMessagesByConvo(String convoId) async {
48
+
Future<List<DmMessageWithSender>> getMessagesByConvo(String convoId, String ownerDid) async {
49
49
final query = select(
50
50
dmMessages,
51
51
).join([innerJoin(profiles, profiles.did.equalsExp(dmMessages.senderDid))]);
52
-
query.where(dmMessages.convoId.equals(convoId));
52
+
query.where(dmMessages.convoId.equals(convoId) & dmMessages.ownerDid.equals(ownerDid));
53
53
query.orderBy([OrderingTerm.asc(dmMessages.sentAt)]);
54
54
55
55
final rows = await query.get();
···
67
67
}
68
68
69
69
/// Updates the status of a message.
70
-
Future<void> updateMessageStatus({required String messageId, required String status}) async {
71
-
await (update(dmMessages)..where((m) => m.messageId.equals(messageId))).write(
72
-
DmMessagesCompanion(status: Value(status)),
73
-
);
70
+
Future<void> updateMessageStatus({
71
+
required String messageId,
72
+
required String ownerDid,
73
+
required String status,
74
+
}) async {
75
+
await (update(dmMessages)
76
+
..where((m) => m.messageId.equals(messageId) & m.ownerDid.equals(ownerDid)))
77
+
.write(DmMessagesCompanion(status: Value(status)));
74
78
}
75
79
76
80
/// Deletes a single message by its ID.
77
-
Future<int> deleteMessage(String messageId) async {
78
-
return (delete(dmMessages)..where((m) => m.messageId.equals(messageId))).go();
81
+
Future<int> deleteMessage(String messageId, String ownerDid) async {
82
+
return (delete(
83
+
dmMessages,
84
+
)..where((m) => m.messageId.equals(messageId) & m.ownerDid.equals(ownerDid))).go();
79
85
}
80
86
81
87
/// Gets the most recent message in a conversation.
82
-
Future<DmMessage?> getLatestMessage(String convoId) async {
88
+
Future<DmMessage?> getLatestMessage(String convoId, String ownerDid) async {
83
89
final query = select(dmMessages)
84
-
..where((m) => m.convoId.equals(convoId))
90
+
..where((m) => m.convoId.equals(convoId) & m.ownerDid.equals(ownerDid))
85
91
..orderBy([(m) => OrderingTerm.desc(m.sentAt)])
86
92
..limit(1);
87
93
return query.getSingleOrNull();
88
94
}
89
95
90
96
/// Deletes all messages for a conversation.
91
-
Future<int> deleteMessagesByConvo(String convoId) async {
92
-
return (delete(dmMessages)..where((m) => m.convoId.equals(convoId))).go();
97
+
Future<int> deleteMessagesByConvo(String convoId, String ownerDid) async {
98
+
return (delete(
99
+
dmMessages,
100
+
)..where((m) => m.convoId.equals(convoId) & m.ownerDid.equals(ownerDid))).go();
93
101
}
94
102
95
-
/// Clears all cached messages.
96
-
Future<void> clearMessages() async {
97
-
await delete(dmMessages).go();
103
+
/// Clears all cached messages for a specific owner.
104
+
Future<int> clearMessages(String ownerDid) async {
105
+
return (delete(dmMessages)..where((m) => m.ownerDid.equals(ownerDid))).go();
98
106
}
99
107
}
100
108
+25
-18
lib/src/infrastructure/db/daos/dm_outbox_dao.dart
+25
-18
lib/src/infrastructure/db/daos/dm_outbox_dao.dart
···
18
18
await into(dmOutbox).insert(item);
19
19
}
20
20
21
-
/// Gets a stream of all pending outbox items.
22
-
Stream<List<DmOutboxData>> watchPending() {
21
+
/// Gets a stream of all pending outbox items for a specific owner.
22
+
Stream<List<DmOutboxData>> watchPending(String ownerDid) {
23
23
return (select(dmOutbox)
24
-
..where((o) => o.status.isIn(['pending', 'sending']))
24
+
..where((o) => o.ownerDid.equals(ownerDid) & o.status.isIn(['pending', 'sending']))
25
25
..orderBy([(o) => OrderingTerm.asc(o.createdAt)]))
26
26
.watch();
27
27
}
28
28
29
-
/// Gets all pending outbox items, oldest first.
30
-
Future<List<DmOutboxData>> getPending() async {
29
+
/// Gets all pending outbox items for a specific owner, oldest first.
30
+
Future<List<DmOutboxData>> getPending(String ownerDid) async {
31
31
return (select(dmOutbox)
32
-
..where((o) => o.status.equals('pending'))
32
+
..where((o) => o.ownerDid.equals(ownerDid) & o.status.equals('pending'))
33
33
..orderBy([(o) => OrderingTerm.asc(o.createdAt)]))
34
34
.get();
35
35
}
36
36
37
-
/// Gets all failed outbox items.
38
-
Future<List<DmOutboxData>> getFailed() async {
37
+
/// Gets all failed outbox items for a specific owner.
38
+
Future<List<DmOutboxData>> getFailed(String ownerDid) async {
39
39
return (select(dmOutbox)
40
-
..where((o) => o.status.equals('failed'))
40
+
..where((o) => o.ownerDid.equals(ownerDid) & o.status.equals('failed'))
41
41
..orderBy([(o) => OrderingTerm.desc(o.lastAttemptAt)]))
42
42
.get();
43
43
}
···
47
47
return (select(dmOutbox)..where((o) => o.outboxId.equals(outboxId))).getSingleOrNull();
48
48
}
49
49
50
-
/// Gets pending outbox items for a specific conversation.
51
-
Future<List<DmOutboxData>> getByConvo(String convoId) async {
50
+
/// Gets pending outbox items for a specific conversation and owner.
51
+
Future<List<DmOutboxData>> getByConvo(String convoId, String ownerDid) async {
52
52
return (select(dmOutbox)
53
-
..where((o) => o.convoId.equals(convoId) & o.status.isIn(['pending', 'sending']))
53
+
..where(
54
+
(o) =>
55
+
o.convoId.equals(convoId) &
56
+
o.ownerDid.equals(ownerDid) &
57
+
o.status.isIn(['pending', 'sending']),
58
+
)
54
59
..orderBy([(o) => OrderingTerm.asc(o.createdAt)]))
55
60
.get();
56
61
}
···
95
100
return (delete(dmOutbox)..where((o) => o.outboxId.equals(outboxId))).go();
96
101
}
97
102
98
-
/// Clears all outbox items.
99
-
Future<void> clearOutbox() async {
100
-
await delete(dmOutbox).go();
103
+
/// Clears all outbox items for a specific owner.
104
+
Future<void> clearOutbox(String ownerDid) async {
105
+
await (delete(dmOutbox)..where((o) => o.ownerDid.equals(ownerDid))).go();
101
106
}
102
107
103
-
/// Counts pending items.
104
-
Future<int> countPending() async {
108
+
/// Counts pending items for a specific owner.
109
+
Future<int> countPending(String ownerDid) async {
105
110
final count = dmOutbox.outboxId.count();
106
-
final query = selectOnly(dmOutbox)..addColumns([count]);
111
+
final query = selectOnly(dmOutbox)
112
+
..where(dmOutbox.ownerDid.equals(ownerDid))
113
+
..addColumns([count]);
107
114
query.where(dmOutbox.status.equals('pending'));
108
115
final result = await query.getSingle();
109
116
return result.read(count) ?? 0;
+32
-18
lib/src/infrastructure/db/daos/feed_content_dao.dart
+32
-18
lib/src/infrastructure/db/daos/feed_content_dao.dart
···
15
15
FeedContentDao(super.db);
16
16
17
17
/// Inserts or updates a batch of posts, profiles, and feed content items.
18
-
/// Also updates the cursor for the given [feedKey].
18
+
/// Also updates the cursor for the given [feedKey] and [ownerDid].
19
19
Future<void> insertFeedContentBatch({
20
20
required List<PostsCompanion> newPosts,
21
21
required List<ProfilesCompanion> newProfiles,
22
22
required List<ProfileRelationshipsCompanion> newRelationships,
23
23
required List<FeedContentItemsCompanion> newItems,
24
24
required String feedKey,
25
+
required String ownerDid,
25
26
String? newCursor,
26
27
}) {
27
28
return transaction(() async {
···
35
36
(item) => FeedContentItemsCompanion.insert(
36
37
feedKey: item.feedKey.value,
37
38
postUri: item.postUri.value,
39
+
ownerDid: ownerDid,
38
40
reason: item.reason,
39
41
sortKey: item.sortKey.value,
40
42
),
···
47
49
await into(feedCursors).insertOnConflictUpdate(
48
50
FeedCursorsCompanion.insert(
49
51
feedKey: feedKey,
52
+
ownerDid: ownerDid,
50
53
cursor: newCursor,
51
54
lastUpdated: Value(DateTime.now()),
52
55
),
···
55
58
});
56
59
}
57
60
58
-
/// Get stream of feed content items for a given feed.
59
-
Stream<List<FeedPost>> watchFeedContent(String feedKey) {
61
+
/// Get stream of feed content items for a given feed and owner.
62
+
Stream<List<FeedPost>> watchFeedContent(String feedKey, String ownerDid) {
60
63
final query = select(feedContentItems).join([
61
64
innerJoin(posts, posts.uri.equalsExp(feedContentItems.postUri)),
62
65
innerJoin(profiles, profiles.did.equalsExp(posts.authorDid)),
63
66
leftOuterJoin(profileRelationships, profileRelationships.profileDid.equalsExp(profiles.did)),
64
67
]);
65
68
66
-
query.where(feedContentItems.feedKey.equals(feedKey));
69
+
query.where(
70
+
feedContentItems.feedKey.equals(feedKey) & feedContentItems.ownerDid.equals(ownerDid),
71
+
);
67
72
query.orderBy([OrderingTerm.desc(feedContentItems.sortKey)]);
68
73
69
74
return query.watch().map((rows) {
···
79
84
});
80
85
}
81
86
82
-
/// Gets the cursor for a specific feed.
83
-
Future<String?> getCursor(String feedKey) async {
84
-
final query = select(feedCursors)..where((t) => t.feedKey.equals(feedKey));
87
+
/// Gets the cursor for a specific feed and owner.
88
+
Future<String?> getCursor(String feedKey, String ownerDid) async {
89
+
final query = select(feedCursors)
90
+
..where((t) => t.feedKey.equals(feedKey) & t.ownerDid.equals(ownerDid));
85
91
final result = await query.getSingleOrNull();
86
92
return result?.cursor;
87
93
}
88
94
89
-
/// Clears all cached items for a specific feed.
90
-
Future<void> clearFeedContent(String feedKey) async {
91
-
await (delete(feedContentItems)..where((t) => t.feedKey.equals(feedKey))).go();
92
-
await (delete(feedCursors)..where((t) => t.feedKey.equals(feedKey))).go();
95
+
/// Clears all cached items for a specific feed and owner.
96
+
Future<void> clearFeedContent(String feedKey, String ownerDid) async {
97
+
await (delete(
98
+
feedContentItems,
99
+
)..where((t) => t.feedKey.equals(feedKey) & t.ownerDid.equals(ownerDid))).go();
100
+
await (delete(
101
+
feedCursors,
102
+
)..where((t) => t.feedKey.equals(feedKey) & t.ownerDid.equals(ownerDid))).go();
93
103
}
94
104
95
-
/// Deletes feed content items and cursors for feeds not updated since [threshold].
96
-
Future<int> deleteStaleFeedContentItems(DateTime threshold) async {
105
+
/// Deletes feed content items and cursors for feeds not updated since [threshold] for a specific user.
106
+
Future<int> deleteStaleFeedContentItems(DateTime threshold, String ownerDid) async {
97
107
return transaction(() async {
98
-
final staleCursors = await (select(
99
-
feedCursors,
100
-
)..where((t) => t.lastUpdated.isSmallerThanValue(threshold))).get();
108
+
final staleCursors =
109
+
await (select(feedCursors)
110
+
..where((t) => t.ownerDid.equals(ownerDid))
111
+
..where((t) => t.lastUpdated.isSmallerThanValue(threshold)))
112
+
.get();
101
113
102
114
int deletedCount = 0;
103
115
104
116
for (final cursor in staleCursors) {
105
117
deletedCount += await (delete(
106
118
feedContentItems,
107
-
)..where((t) => t.feedKey.equals(cursor.feedKey))).go();
108
-
await (delete(feedCursors)..where((t) => t.feedKey.equals(cursor.feedKey))).go();
119
+
)..where((t) => t.feedKey.equals(cursor.feedKey) & t.ownerDid.equals(ownerDid))).go();
120
+
await (delete(
121
+
feedCursors,
122
+
)..where((t) => t.feedKey.equals(cursor.feedKey) & t.ownerDid.equals(ownerDid))).go();
109
123
}
110
124
111
125
return deletedCount;
+54
-47
lib/src/infrastructure/db/daos/notifications_dao.dart
+54
-47
lib/src/infrastructure/db/daos/notifications_dao.dart
···
14
14
NotificationsDao(super.db);
15
15
16
16
/// Inserts or updates a batch of notifications.
17
-
/// Also updates the cursor for pagination.
17
+
///
18
+
/// Also inserts associated profiles for authors.
18
19
Future<void> insertNotificationsBatch({
19
20
required List<NotificationsCompanion> newNotifications,
20
21
required List<ProfilesCompanion> newProfiles,
21
-
String? newCursor,
22
+
required String? newCursor,
23
+
required String ownerDid,
22
24
}) {
23
25
return transaction(() async {
24
26
await batch((batch) {
···
30
32
await into(notificationCursors).insertOnConflictUpdate(
31
33
NotificationCursorsCompanion.insert(
32
34
feedKey: 'notifications',
35
+
ownerDid: ownerDid,
33
36
cursor: newCursor,
34
37
lastUpdated: Value(DateTime.now()),
35
38
),
···
38
41
});
39
42
}
40
43
41
-
/// Gets a stream of notifications with their actors.
42
-
Stream<List<NotificationWithActor>> watchNotifications() {
43
-
final query = select(
44
-
notifications,
45
-
).join([innerJoin(profiles, profiles.did.equalsExp(notifications.actorDid))]);
46
-
47
-
query.orderBy([OrderingTerm.desc(notifications.indexedAt)]);
48
-
49
-
return query.watch().map((rows) {
50
-
return rows.map((row) {
51
-
return NotificationWithActor(
52
-
notification: row.readTable(notifications),
53
-
actor: row.readTable(profiles),
54
-
);
55
-
}).toList();
56
-
});
44
+
/// Gets a stream of notifications from the local cache for a specific user.
45
+
Stream<List<NotificationWithActor>> watchNotifications(String ownerDid) {
46
+
return (select(notifications)
47
+
..where((t) => t.ownerDid.equals(ownerDid))
48
+
..orderBy([(t) => OrderingTerm(expression: t.indexedAt, mode: OrderingMode.desc)]))
49
+
.join([innerJoin(profiles, profiles.did.equalsExp(notifications.actorDid))])
50
+
.watch()
51
+
.map((rows) {
52
+
return rows.map((row) {
53
+
return NotificationWithActor(
54
+
notification: row.readTable(notifications),
55
+
actor: row.readTable(profiles),
56
+
);
57
+
}).toList();
58
+
});
57
59
}
58
60
59
-
/// Gets the pagination cursor.
60
-
Future<String?> getCursor() async {
61
-
final query = select(notificationCursors)..where((t) => t.feedKey.equals('notifications'));
62
-
final result = await query.getSingleOrNull();
63
-
return result?.cursor;
61
+
/// Gets the pagination cursor for loading more notifications.
62
+
Future<String?> getCursor(String ownerDid) {
63
+
final query = select(notificationCursors)
64
+
..where((t) => t.feedKey.equals('notifications') & t.ownerDid.equals(ownerDid));
65
+
return query.map((t) => t.cursor).getSingleOrNull();
64
66
}
65
67
66
-
/// Clears all cached notifications and cursor.
67
-
Future<void> clearNotifications() async {
68
-
await delete(notifications).go();
69
-
await (delete(notificationCursors)..where((t) => t.feedKey.equals('notifications'))).go();
68
+
/// Clears all cached notifications for a specific user.
69
+
Future<void> clearNotifications(String ownerDid) async {
70
+
await (delete(notifications)..where((t) => t.ownerDid.equals(ownerDid))).go();
71
+
await (delete(
72
+
notificationCursors,
73
+
)..where((t) => t.feedKey.equals('notifications') & t.ownerDid.equals(ownerDid))).go();
70
74
}
71
75
72
-
/// Deletes notifications older than the given threshold.
73
-
Future<int> deleteStaleNotifications(DateTime threshold) async {
74
-
return (delete(notifications)..where((t) => t.cachedAt.isSmallerThanValue(threshold))).go();
76
+
/// Deletes notifications older than the specified threshold for a specific user.
77
+
Future<int> deleteStaleNotifications(DateTime threshold, String ownerDid) {
78
+
return (delete(notifications)
79
+
..where((t) => t.ownerDid.equals(ownerDid))
80
+
..where((t) => t.cachedAt.isSmallerThanValue(threshold)))
81
+
.go();
75
82
}
76
83
77
-
/// Marks all notifications as read.
78
-
Future<void> markAllAsRead() async {
79
-
await (update(notifications)..where((t) => t.isRead.equals(false))).write(
84
+
/// Marks all notifications as read locally for a specific user.
85
+
Future<void> markAllAsRead(String ownerDid) {
86
+
return (update(notifications)..where((t) => t.ownerDid.equals(ownerDid))).write(
80
87
const NotificationsCompanion(isRead: Value(true)),
81
88
);
82
89
}
83
90
84
-
/// Marks notifications as read if they were indexed before the given timestamp.
85
-
///
86
-
/// This is used for batching mark as seen operations - all notifications
87
-
/// before [seenAt] are marked as read locally.
88
-
Future<void> markAsSeenBefore(DateTime seenAt) async {
89
-
await (update(notifications)
90
-
..where((t) => t.indexedAt.isSmallerOrEqualValue(seenAt) & t.isRead.equals(false)))
91
+
/// Marks notifications indexed before the given timestamp as seen (read + seenAt set) for a user.
92
+
Future<void> markAsSeenBefore(DateTime seenAt, String ownerDid) {
93
+
return (update(notifications)
94
+
..where((t) => t.ownerDid.equals(ownerDid))
95
+
..where((t) => t.indexedAt.isSmallerThanValue(seenAt)))
91
96
.write(NotificationsCompanion(isRead: const Value(true), seenAt: Value(seenAt)));
92
97
}
93
98
94
-
/// Gets a stream of the unread notification count.
95
-
///
96
-
/// Emits updates whenever notifications are inserted, updated, or deleted.
97
-
Stream<int> watchUnreadCount() {
98
-
final query = selectOnly(notifications)..addColumns([notifications.uri.count()]);
99
-
query.where(notifications.isRead.equals(false));
100
-
return query.map((row) => row.read(notifications.uri.count()) ?? 0).watchSingle();
99
+
/// Returns a stream of the unread notification count for a specific user.
100
+
Stream<int> watchUnreadCount(String ownerDid) {
101
+
final count = notifications.uri.count();
102
+
final query = selectOnly(notifications)
103
+
..where(notifications.ownerDid.equals(ownerDid))
104
+
..where(notifications.isRead.not())
105
+
..addColumns([count]);
106
+
107
+
return query.map((row) => row.read(count) ?? 0).watchSingle();
101
108
}
102
109
}
103
110
+33
-35
lib/src/infrastructure/db/daos/notifications_sync_queue_dao.dart
+33
-35
lib/src/infrastructure/db/daos/notifications_sync_queue_dao.dart
···
19
19
with _$NotificationsSyncQueueDaoMixin {
20
20
NotificationsSyncQueueDao(super.db);
21
21
22
-
/// Enqueues a mark-as-seen operation.
22
+
/// Enqueues a mark-as-seen operation for a specific user.
23
23
///
24
24
/// The [seenAt] timestamp marks all notifications up to that point as seen.
25
-
Future<int> enqueueMarkSeen(DateTime seenAt) {
25
+
Future<int> enqueueMarkSeen(DateTime seenAt, String ownerDid) {
26
26
return into(notificationsSyncQueue).insert(
27
27
NotificationsSyncQueueCompanion.insert(
28
28
type: 'mark_seen',
29
29
seenAt: seenAt.toIso8601String(),
30
+
ownerDid: ownerDid,
30
31
createdAt: DateTime.now(),
31
32
),
32
33
);
33
34
}
34
35
35
-
/// Gets all retryable items (retryCount < [kMaxNotificationSyncRetries]).
36
-
///
37
-
/// Returns items ordered by creation time (oldest first).
38
-
Future<List<NotificationsSyncQueueData>> getRetryableItems() {
36
+
/// Get the latest 'seenAt' timestamp from the queue for a user.
37
+
Future<DateTime?> getLatestSeenAt(String ownerDid) async {
38
+
final query = select(notificationsSyncQueue)
39
+
..where((t) => t.ownerDid.equals(ownerDid))
40
+
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
41
+
..limit(1);
42
+
43
+
final item = await query.getSingleOrNull();
44
+
if (item == null) return null;
45
+
return DateTime.tryParse(item.seenAt);
46
+
}
47
+
48
+
/// Get items that can be retried for a user.
49
+
Future<List<NotificationsSyncQueueData>> getRetryableItems(String ownerDid) {
39
50
return (select(notificationsSyncQueue)
40
-
..where((t) => t.retryCount.isSmallerThanValue(kMaxNotificationSyncRetries))
51
+
..where(
52
+
(t) =>
53
+
t.ownerDid.equals(ownerDid) &
54
+
t.retryCount.isSmallerThanValue(kMaxNotificationSyncRetries),
55
+
)
41
56
..orderBy([(t) => OrderingTerm(expression: t.createdAt)]))
42
57
.get();
43
58
}
44
59
45
-
/// Gets the latest seenAt timestamp from the queue.
46
-
///
47
-
/// Returns null if queue is empty. This allows batching multiple failed
48
-
/// operations into a single retry with the most recent timestamp.
49
-
Future<DateTime?> getLatestSeenAt() async {
50
-
final items = await getRetryableItems();
51
-
if (items.isEmpty) {
52
-
return null;
53
-
}
54
-
55
-
DateTime? latest;
56
-
for (final item in items) {
57
-
final timestamp = DateTime.parse(item.seenAt);
58
-
if (latest == null || timestamp.isAfter(latest)) {
59
-
latest = timestamp;
60
-
}
61
-
}
62
-
return latest;
63
-
}
64
-
65
60
/// Increments the retry count for a specific item.
66
61
Future<int> incrementRetryCount(int id) {
67
62
return (update(notificationsSyncQueue)..where((t) => t.id.equals(id))).write(
···
76
71
return (delete(notificationsSyncQueue)..where((t) => t.id.equals(id))).go();
77
72
}
78
73
79
-
/// Deletes all items with seenAt <= the specified timestamp.
74
+
/// Deletes all items with seenAt <= the specified timestamp for a specific user.
80
75
///
81
76
/// Used after successfully syncing to remove obsolete queue items.
82
-
Future<int> deleteItemsUpTo(DateTime seenAt) {
83
-
return (delete(
84
-
notificationsSyncQueue,
85
-
)..where((t) => t.seenAt.isSmallerOrEqualValue(seenAt.toIso8601String()))).go();
77
+
Future<int> deleteItemsUpTo(DateTime seenAt, String ownerDid) {
78
+
return (delete(notificationsSyncQueue)..where(
79
+
(t) =>
80
+
t.seenAt.isSmallerOrEqualValue(seenAt.toIso8601String()) &
81
+
t.ownerDid.equals(ownerDid),
82
+
))
83
+
.go();
86
84
}
87
85
88
86
/// Cleans up old permanently failed items.
···
98
96
.go();
99
97
}
100
98
101
-
/// Clears the entire queue.
102
-
Future<int> clearQueue() {
103
-
return delete(notificationsSyncQueue).go();
99
+
/// Clears the entire queue for a specific user.
100
+
Future<int> clearQueue(String ownerDid) {
101
+
return (delete(notificationsSyncQueue)..where((t) => t.ownerDid.equals(ownerDid))).go();
104
102
}
105
103
}
+34
-22
lib/src/infrastructure/db/daos/preference_sync_queue_dao.dart
+34
-22
lib/src/infrastructure/db/daos/preference_sync_queue_dao.dart
···
22
22
return into(preferenceSyncQueue).insert(item);
23
23
}
24
24
25
-
/// Enqueues a feed preference update.
26
-
Future<int> enqueueFeedSync({required String type, required String feedUri}) {
25
+
/// Enqueues a feed preference update for a specific user.
26
+
Future<int> enqueueFeedSync({
27
+
required String type,
28
+
required String feedUri,
29
+
required String ownerDid,
30
+
}) {
27
31
return enqueue(
28
32
PreferenceSyncQueueCompanion.insert(
29
33
category: const Value('feed'),
30
34
type: type,
31
35
payload: feedUri,
36
+
ownerDid: ownerDid,
32
37
createdAt: DateTime.now(),
33
38
),
34
39
);
35
40
}
36
41
37
-
/// Enqueues a Bluesky preference update.
42
+
/// Enqueues a Bluesky preference update for a specific user.
38
43
///
39
44
/// The [preferenceType] should be one of: 'adultContent', 'contentLabels',
40
45
/// 'labelers', 'feedView', 'threadView', 'mutedWords'.
41
46
Future<int> enqueueBlueskyPrefSync({
42
47
required String preferenceType,
43
48
required String preferenceData,
49
+
required String ownerDid,
44
50
}) {
45
51
return enqueue(
46
52
PreferenceSyncQueueCompanion.insert(
47
53
category: const Value('bluesky_pref'),
48
54
type: preferenceType,
49
55
payload: preferenceData,
56
+
ownerDid: ownerDid,
50
57
createdAt: DateTime.now(),
51
58
),
52
59
);
53
60
}
54
61
55
-
/// Gets all pending items in the queue, ordered by creation time.
56
-
Future<List<PreferenceSyncQueueData>> getPendingItems() {
57
-
return (select(
58
-
preferenceSyncQueue,
59
-
)..orderBy([(t) => OrderingTerm(expression: t.createdAt)])).get();
62
+
/// Gets all pending items in the queue for a specific user, ordered by creation time.
63
+
Future<List<PreferenceSyncQueueData>> getPendingItems(String ownerDid) {
64
+
return (select(preferenceSyncQueue)
65
+
..where((t) => t.ownerDid.equals(ownerDid))
66
+
..orderBy([(t) => OrderingTerm(expression: t.createdAt)]))
67
+
.get();
60
68
}
61
69
62
-
/// Gets items that can still be retried (retryCount < [kMaxSyncRetries]).
63
-
Future<List<PreferenceSyncQueueData>> getRetryableItems() {
70
+
/// Gets items that can still be retried (retryCount < [kMaxSyncRetries]) for a user.
71
+
Future<List<PreferenceSyncQueueData>> getRetryableItems(String ownerDid) {
64
72
return (select(preferenceSyncQueue)
73
+
..where((t) => t.ownerDid.equals(ownerDid))
65
74
..where((t) => t.retryCount.isSmallerThanValue(kMaxSyncRetries))
66
75
..orderBy([(t) => OrderingTerm(expression: t.createdAt)]))
67
76
.get();
68
77
}
69
78
70
-
/// Gets retryable feed preference items.
71
-
Future<List<PreferenceSyncQueueData>> getRetryableFeedItems() {
79
+
/// Gets retryable feed preference items for a user.
80
+
Future<List<PreferenceSyncQueueData>> getRetryableFeedItems(String ownerDid) {
72
81
return (select(preferenceSyncQueue)
82
+
..where((t) => t.ownerDid.equals(ownerDid))
73
83
..where(
74
84
(t) => t.category.equals('feed') & t.retryCount.isSmallerThanValue(kMaxSyncRetries),
75
85
)
···
77
87
.get();
78
88
}
79
89
80
-
/// Gets retryable Bluesky preference items.
81
-
Future<List<PreferenceSyncQueueData>> getRetryableBlueskyPrefItems() {
90
+
/// Gets retryable Bluesky preference items for a user.
91
+
Future<List<PreferenceSyncQueueData>> getRetryableBlueskyPrefItems(String ownerDid) {
82
92
return (select(preferenceSyncQueue)
93
+
..where((t) => t.ownerDid.equals(ownerDid))
83
94
..where(
84
95
(t) =>
85
96
t.category.equals('bluesky_pref') &
···
111
122
.go();
112
123
}
113
124
114
-
/// Watches all pending items in the queue.
115
-
Stream<List<PreferenceSyncQueueData>> watchPendingItems() {
116
-
return (select(
117
-
preferenceSyncQueue,
118
-
)..orderBy([(t) => OrderingTerm(expression: t.createdAt)])).watch();
125
+
/// Watches all pending items in the queue for a user.
126
+
Stream<List<PreferenceSyncQueueData>> watchPendingItems(String ownerDid) {
127
+
return (select(preferenceSyncQueue)
128
+
..where((t) => t.ownerDid.equals(ownerDid))
129
+
..orderBy([(t) => OrderingTerm(expression: t.createdAt)]))
130
+
.watch();
119
131
}
120
132
121
133
/// Deletes a specific item from the queue.
···
123
135
return (delete(preferenceSyncQueue)..where((t) => t.id.equals(id))).go();
124
136
}
125
137
126
-
/// Clears the entire queue.
127
-
Future<int> clearQueue() {
128
-
return delete(preferenceSyncQueue).go();
138
+
/// Clears the entire queue for a user.
139
+
Future<int> clearQueue(String ownerDid) {
140
+
return (delete(preferenceSyncQueue)..where((t) => t.ownerDid.equals(ownerDid))).go();
129
141
}
130
142
}
+21
-12
lib/src/infrastructure/db/daos/profile_relationship_dao.dart
+21
-12
lib/src/infrastructure/db/daos/profile_relationship_dao.dart
···
16
16
}
17
17
18
18
/// Gets a viewer relationship by profile DID.
19
-
Future<ProfileRelationship?> getRelationship(String profileDid) {
20
-
return (select(
21
-
profileRelationships,
22
-
)..where((t) => t.profileDid.equals(profileDid))).getSingleOrNull();
19
+
Future<ProfileRelationship?> getRelationship(String profileDid, String ownerDid) {
20
+
return (select(profileRelationships)
21
+
..where((t) => t.profileDid.equals(profileDid) & t.ownerDid.equals(ownerDid)))
22
+
.getSingleOrNull();
23
23
}
24
24
25
25
/// Watches a viewer relationship by profile DID.
26
-
Stream<ProfileRelationship?> watchRelationship(String profileDid) {
27
-
return (select(
28
-
profileRelationships,
29
-
)..where((t) => t.profileDid.equals(profileDid))).watchSingleOrNull();
26
+
Stream<ProfileRelationship?> watchRelationship(String profileDid, String ownerDid) {
27
+
return (select(profileRelationships)
28
+
..where((t) => t.profileDid.equals(profileDid) & t.ownerDid.equals(ownerDid)))
29
+
.watchSingleOrNull();
30
30
}
31
31
32
32
/// Updates the mute status for a profile.
33
-
Future<void> updateMuteStatus(String profileDid, bool muted) {
34
-
return (update(profileRelationships)..where((t) => t.profileDid.equals(profileDid))).write(
33
+
Future<void> updateMuteStatus(String profileDid, bool muted, String ownerDid) {
34
+
return (update(
35
+
profileRelationships,
36
+
)..where((t) => t.profileDid.equals(profileDid) & t.ownerDid.equals(ownerDid))).write(
35
37
ProfileRelationshipsCompanion(muted: Value(muted), updatedAt: Value(DateTime.now())),
36
38
);
37
39
}
38
40
39
41
/// Updates the block status for a profile.
40
-
Future<void> updateBlockStatus(String profileDid, bool blocked, {String? blockingUri}) {
41
-
return (update(profileRelationships)..where((t) => t.profileDid.equals(profileDid))).write(
42
+
Future<void> updateBlockStatus(
43
+
String profileDid,
44
+
bool blocked,
45
+
String ownerDid, {
46
+
String? blockingUri,
47
+
}) {
48
+
return (update(
49
+
profileRelationships,
50
+
)..where((t) => t.profileDid.equals(profileDid) & t.ownerDid.equals(ownerDid))).write(
42
51
ProfileRelationshipsCompanion(
43
52
blocked: Value(blocked),
44
53
blockingUri: Value(blockingUri),
+73
-39
lib/src/infrastructure/db/daos/saved_feeds_dao.dart
+73
-39
lib/src/infrastructure/db/daos/saved_feeds_dao.dart
···
28
28
}
29
29
30
30
/// Deletes a saved feed by URI.
31
-
Future<int> deleteFeed(String uri) {
32
-
return (delete(savedFeeds)..where((t) => t.uri.equals(uri))).go();
31
+
Future<int> deleteFeed(String uri, String ownerDid) {
32
+
return (delete(savedFeeds)
33
+
..where((t) => t.uri.equals(uri))
34
+
..where((t) => t.ownerDid.equals(ownerDid)))
35
+
.go();
33
36
}
34
37
35
-
/// Deletes all saved feeds.
38
+
/// Deletes all saved feeds for a specific owner.
36
39
///
37
40
/// Used when clearing cache or during user logout.
38
-
Future<int> deleteAllFeeds() {
39
-
return delete(savedFeeds).go();
41
+
Future<int> deleteAllFeeds(String ownerDid) {
42
+
return (delete(savedFeeds)..where((t) => t.ownerDid.equals(ownerDid))).go();
40
43
}
41
44
42
45
/// Gets a saved feed by URI.
43
-
Future<SavedFeed?> getFeed(String uri) {
44
-
return (select(savedFeeds)..where((t) => t.uri.equals(uri))).getSingleOrNull();
46
+
Future<SavedFeed?> getFeed(String uri, String ownerDid) {
47
+
return (select(savedFeeds)
48
+
..where((t) => t.uri.equals(uri))
49
+
..where((t) => t.ownerDid.equals(ownerDid)))
50
+
.getSingleOrNull();
45
51
}
46
52
47
53
/// Watches a saved feed reactively.
48
-
Stream<SavedFeed?> watchFeed(String uri) {
49
-
return (select(savedFeeds)..where((t) => t.uri.equals(uri))).watchSingleOrNull();
54
+
Stream<SavedFeed?> watchFeed(String uri, String ownerDid) {
55
+
return (select(savedFeeds)
56
+
..where((t) => t.uri.equals(uri))
57
+
..where((t) => t.ownerDid.equals(ownerDid)))
58
+
.watchSingleOrNull();
50
59
}
51
60
52
61
/// Gets all saved feeds ordered by sortOrder.
53
-
Future<List<SavedFeed>> getAllFeeds() {
54
-
return (select(savedFeeds)..orderBy([(t) => OrderingTerm(expression: t.sortOrder)])).get();
62
+
Future<List<SavedFeed>> getAllFeeds(String ownerDid) {
63
+
return (select(savedFeeds)
64
+
..where((t) => t.ownerDid.equals(ownerDid))
65
+
..orderBy([(t) => OrderingTerm(expression: t.sortOrder)]))
66
+
.get();
55
67
}
56
68
57
69
/// Watches all saved feeds reactively, ordered by sortOrder.
58
-
Stream<List<SavedFeed>> watchAllFeeds() {
59
-
return (select(savedFeeds)..orderBy([(t) => OrderingTerm(expression: t.sortOrder)])).watch();
70
+
Stream<List<SavedFeed>> watchAllFeeds(String ownerDid) {
71
+
return (select(savedFeeds)
72
+
..where((t) => t.ownerDid.equals(ownerDid))
73
+
..orderBy([(t) => OrderingTerm(expression: t.sortOrder)]))
74
+
.watch();
60
75
}
61
76
62
77
/// Gets all pinned feeds ordered by sortOrder.
63
-
Future<List<SavedFeed>> getPinnedFeeds() {
78
+
Future<List<SavedFeed>> getPinnedFeeds(String ownerDid) {
64
79
return (select(savedFeeds)
80
+
..where((t) => t.ownerDid.equals(ownerDid))
65
81
..where((t) => t.isPinned.equals(true))
66
82
..orderBy([(t) => OrderingTerm(expression: t.sortOrder)]))
67
83
.get();
68
84
}
69
85
70
86
/// Watches pinned feeds reactively, ordered by sortOrder.
71
-
Stream<List<SavedFeed>> watchPinnedFeeds() {
87
+
Stream<List<SavedFeed>> watchPinnedFeeds(String ownerDid) {
72
88
return (select(savedFeeds)
89
+
..where((t) => t.ownerDid.equals(ownerDid))
73
90
..where((t) => t.isPinned.equals(true))
74
91
..orderBy([(t) => OrderingTerm(expression: t.sortOrder)]))
75
92
.watch();
···
78
95
/// Gets feeds that haven't been synced recently (stale metadata).
79
96
///
80
97
/// Returns feeds where lastSynced is older than the given threshold.
81
-
Future<List<SavedFeed>> getStaleFeeds(DateTime threshold) {
82
-
return (select(savedFeeds)..where((t) => t.lastSynced.isSmallerThanValue(threshold))).get();
98
+
Future<List<SavedFeed>> getStaleFeeds(DateTime threshold, String ownerDid) {
99
+
return (select(savedFeeds)
100
+
..where((t) => t.ownerDid.equals(ownerDid))
101
+
..where((t) => t.lastSynced.isSmallerThanValue(threshold)))
102
+
.get();
83
103
}
84
104
85
105
/// Updates the sortOrder for a feed and marks it as locally modified.
86
-
Future<int> updateSortOrder(String uri, int sortOrder) {
87
-
return (update(savedFeeds)..where((t) => t.uri.equals(uri))).write(
88
-
SavedFeedsCompanion(sortOrder: Value(sortOrder), localUpdatedAt: Value(DateTime.now())),
89
-
);
106
+
Future<int> updateSortOrder(String uri, int sortOrder, String ownerDid) {
107
+
return (update(savedFeeds)
108
+
..where((t) => t.uri.equals(uri))
109
+
..where((t) => t.ownerDid.equals(ownerDid)))
110
+
.write(
111
+
SavedFeedsCompanion(sortOrder: Value(sortOrder), localUpdatedAt: Value(DateTime.now())),
112
+
);
90
113
}
91
114
92
115
/// Updates the isPinned status for a feed.
93
-
Future<int> updatePinnedStatus(String uri, bool isPinned) {
94
-
return (update(savedFeeds)..where((t) => t.uri.equals(uri))).write(
95
-
SavedFeedsCompanion(isPinned: Value(isPinned), localUpdatedAt: Value(DateTime.now())),
96
-
);
116
+
Future<int> updatePinnedStatus(String uri, bool isPinned, String ownerDid) {
117
+
return (update(savedFeeds)
118
+
..where((t) => t.uri.equals(uri))
119
+
..where((t) => t.ownerDid.equals(ownerDid)))
120
+
.write(
121
+
SavedFeedsCompanion(isPinned: Value(isPinned), localUpdatedAt: Value(DateTime.now())),
122
+
);
97
123
}
98
124
99
125
/// Clears the localUpdatedAt timestamp after successful remote sync.
100
126
///
101
127
/// This marks the feed as "in sync" with the remote state.
102
-
Future<int> clearLocalModification(String uri) {
103
-
return (update(savedFeeds)..where((t) => t.uri.equals(uri))).write(
104
-
const SavedFeedsCompanion(localUpdatedAt: Value(null)),
105
-
);
128
+
Future<int> clearLocalModification(String uri, String ownerDid) {
129
+
return (update(savedFeeds)
130
+
..where((t) => t.uri.equals(uri))
131
+
..where((t) => t.ownerDid.equals(ownerDid)))
132
+
.write(const SavedFeedsCompanion(localUpdatedAt: Value(null)));
106
133
}
107
134
108
135
/// Updates sync-related fields for an existing feed.
···
115
142
required int sortOrder,
116
143
required bool isPinned,
117
144
required DateTime lastSynced,
145
+
required String ownerDid,
118
146
bool clearLocalModification = false,
119
147
}) {
120
-
return (update(savedFeeds)..where((t) => t.uri.equals(uri))).write(
121
-
SavedFeedsCompanion(
122
-
sortOrder: Value(sortOrder),
123
-
isPinned: Value(isPinned),
124
-
lastSynced: Value(lastSynced),
125
-
localUpdatedAt: clearLocalModification ? const Value(null) : const Value.absent(),
126
-
),
127
-
);
148
+
return (update(savedFeeds)
149
+
..where((t) => t.uri.equals(uri))
150
+
..where((t) => t.ownerDid.equals(ownerDid)))
151
+
.write(
152
+
SavedFeedsCompanion(
153
+
sortOrder: Value(sortOrder),
154
+
isPinned: Value(isPinned),
155
+
lastSynced: Value(lastSynced),
156
+
localUpdatedAt: clearLocalModification ? const Value(null) : const Value.absent(),
157
+
),
158
+
);
128
159
}
129
160
130
161
/// Gets feeds with pending local modifications (localUpdatedAt != null).
131
-
Future<List<SavedFeed>> getLocallyModifiedFeeds() {
132
-
return (select(savedFeeds)..where((t) => t.localUpdatedAt.isNotNull())).get();
162
+
Future<List<SavedFeed>> getLocallyModifiedFeeds(String ownerDid) {
163
+
return (select(savedFeeds)
164
+
..where((t) => t.ownerDid.equals(ownerDid))
165
+
..where((t) => t.localUpdatedAt.isNotNull()))
166
+
.get();
133
167
}
134
168
}
+41
-9
lib/src/infrastructure/db/tables.dart
+41
-9
lib/src/infrastructure/db/tables.dart
···
77
77
class FeedContentItems extends Table {
78
78
TextColumn get feedKey => text()();
79
79
TextColumn get postUri => text().references(Posts, #uri)();
80
+
TextColumn get ownerDid => text()();
80
81
TextColumn get reason => text().nullable()();
81
82
TextColumn get sortKey => text()();
82
83
83
84
@override
84
-
Set<Column> get primaryKey => {feedKey, postUri};
85
+
Set<Column> get primaryKey => {feedKey, postUri, ownerDid};
85
86
}
86
87
87
88
class Accounts extends Table {
···
95
96
96
97
class FeedCursors extends Table {
97
98
TextColumn get feedKey => text()();
99
+
TextColumn get ownerDid => text()();
98
100
TextColumn get cursor => text()();
99
101
DateTimeColumn get lastUpdated => dateTime().nullable()();
100
102
101
103
@override
102
-
Set<Column> get primaryKey => {feedKey};
104
+
Set<Column> get primaryKey => {feedKey, ownerDid};
103
105
}
104
106
105
107
class RecentSearches extends Table {
···
161
163
162
164
/// Stores normalized viewer relationships for profiles.
163
165
class ProfileRelationships extends Table {
166
+
/// The DID of the owner (the user who sees these relationships).
167
+
TextColumn get ownerDid => text()();
168
+
164
169
/// The DID of the profile this relationship applies to (subject).
165
170
TextColumn get profileDid => text().references(Profiles, #did)();
166
171
···
195
200
DateTimeColumn get updatedAt => dateTime()();
196
201
197
202
@override
198
-
Set<Column> get primaryKey => {profileDid};
203
+
Set<Column> get primaryKey => {ownerDid, profileDid};
199
204
}
200
205
201
206
/// Stores saved feed generators with metadata.
···
206
211
/// Feed generator AT URI (at://did:plc:xxx/app.bsky.feed.generator/yyy).
207
212
TextColumn get uri => text()();
208
213
214
+
/// The DID of the user who saved this feed.
215
+
TextColumn get ownerDid => text()();
216
+
209
217
/// Display name of the feed.
210
218
TextColumn get displayName => text()();
211
219
···
235
243
DateTimeColumn get localUpdatedAt => dateTime().nullable()();
236
244
237
245
@override
238
-
Set<Column> get primaryKey => {uri};
246
+
Set<Column> get primaryKey => {uri, ownerDid};
239
247
}
240
248
241
249
/// Stores queued preference updates for offline synchronization.
···
244
252
/// preferences (content labels, muted words, etc.).
245
253
class PreferenceSyncQueue extends Table {
246
254
IntColumn get id => integer().autoIncrement()();
255
+
256
+
/// The DID of the user who owns this action.
257
+
TextColumn get ownerDid => text()();
247
258
248
259
/// Category of preference being synced: 'feed' or 'bluesky_pref'.
249
260
TextColumn get category => text().withDefault(const Constant('feed'))();
···
274
285
class NotificationsSyncQueue extends Table {
275
286
IntColumn get id => integer().autoIncrement()();
276
287
288
+
/// The DID of the user who owns this action.
289
+
TextColumn get ownerDid => text()();
290
+
277
291
/// Type of operation: 'mark_seen'.
278
292
TextColumn get type => text()();
279
293
···
349
363
class BlueskyPreferences extends Table {
350
364
/// The preference type identifier (e.g., 'contentLabel', 'adultContent').
351
365
TextColumn get type => text()();
366
+
367
+
/// The DID of the owner of these preferences.
368
+
TextColumn get ownerDid => text()();
352
369
353
370
/// The preference data serialized as JSON.
354
371
TextColumn get data => text()();
···
357
374
DateTimeColumn get lastSynced => dateTime()();
358
375
359
376
@override
360
-
Set<Column> get primaryKey => {type};
377
+
Set<Column> get primaryKey => {type, ownerDid};
361
378
}
362
379
363
380
/// Stores user-customized themes.
···
417
434
/// Notification AT URI (primary key).
418
435
TextColumn get uri => text()();
419
436
437
+
/// The DID of the user receiving the notification.
438
+
TextColumn get ownerDid => text()();
439
+
420
440
/// DID of the user who triggered the notification.
421
441
TextColumn get actorDid => text().references(Profiles, #did)();
422
442
···
442
462
DateTimeColumn get cachedAt => dateTime()();
443
463
444
464
@override
445
-
Set<Column> get primaryKey => {uri};
465
+
Set<Column> get primaryKey => {uri, ownerDid};
446
466
}
447
467
448
468
/// Stores pagination cursor for notifications feed.
···
450
470
/// Feed key identifier (e.g., 'notifications').
451
471
TextColumn get feedKey => text()();
452
472
473
+
/// The DID of the user this cursor belongs to.
474
+
TextColumn get ownerDid => text()();
475
+
453
476
/// Pagination cursor from API.
454
477
TextColumn get cursor => text()();
455
478
···
457
480
DateTimeColumn get lastUpdated => dateTime().nullable()();
458
481
459
482
@override
460
-
Set<Column> get primaryKey => {feedKey};
483
+
Set<Column> get primaryKey => {feedKey, ownerDid};
461
484
}
462
485
463
486
/// Stores direct message conversations from chat.bsky.convo.listConvos.
···
468
491
/// Conversation ID (unique identifier from API).
469
492
TextColumn get convoId => text()();
470
493
494
+
/// The DID of the user who owns this conversation view.
495
+
TextColumn get ownerDid => text()();
496
+
471
497
/// JSON array of participant DIDs.
472
498
TextColumn get membersJson => text()();
473
499
···
493
519
DateTimeColumn get cachedAt => dateTime()();
494
520
495
521
@override
496
-
Set<Column> get primaryKey => {convoId};
522
+
Set<Column> get primaryKey => {convoId, ownerDid};
497
523
}
498
524
499
525
/// Stores direct messages from chat.bsky.convo.getMessages.
···
505
531
/// Message ID (unique identifier from API).
506
532
TextColumn get messageId => text()();
507
533
534
+
/// The DID of the user who owns this message view.
535
+
TextColumn get ownerDid => text()();
536
+
508
537
/// Conversation this message belongs to.
509
538
TextColumn get convoId => text()();
510
539
···
524
553
DateTimeColumn get cachedAt => dateTime()();
525
554
526
555
@override
527
-
Set<Column> get primaryKey => {messageId};
556
+
Set<Column> get primaryKey => {messageId, ownerDid};
528
557
}
529
558
530
559
/// Stores pending message sends for reliable offline delivery.
···
534
563
class DmOutbox extends Table {
535
564
/// Local UUID for this outbox item.
536
565
TextColumn get outboxId => text()();
566
+
567
+
/// The DID of the user who sent this message.
568
+
TextColumn get ownerDid => text()();
537
569
538
570
/// Conversation to send the message to.
539
571
TextColumn get convoId => text()();
+1
-1
lib/src/infrastructure/network/providers.dart
+1
-1
lib/src/infrastructure/network/providers.dart
+1
-1
lib/src/infrastructure/network/providers.g.dart
+1
-1
lib/src/infrastructure/network/providers.g.dart
+83
-48
lib/src/infrastructure/preferences/bluesky_preferences_repository.dart
+83
-48
lib/src/infrastructure/preferences/bluesky_preferences_repository.dart
···
20
20
final Logger _logger;
21
21
22
22
/// Syncs all preferences from the remote Bluesky server.
23
-
///
24
-
/// Fetches via app.bsky.actor.getPreferences, parses each preference
25
-
/// by its $type, and stores in the local database.
26
-
///
27
-
/// For unauthenticated users, this is a no-op.
28
-
Future<void> syncPreferencesFromRemote() async {
23
+
Future<void> syncPreferencesFromRemote(String ownerDid) async {
29
24
if (!_api.isAuthenticated) {
30
25
_logger.debug('Skipping preference sync for unauthenticated user');
31
26
return;
32
27
}
33
28
34
-
_logger.info('Syncing Bluesky preferences from remote');
29
+
_logger.info('Syncing Bluesky preferences from remote', {'ownerDid': ownerDid});
35
30
36
31
try {
37
32
final response = await _api.call('app.bsky.actor.getPreferences');
···
80
75
if (adultContent != null) {
81
76
await _dao.upsertPreference(
82
77
type: 'adultContent',
78
+
ownerDid: ownerDid,
83
79
data: adultContent.toStoredJson(),
84
80
lastSynced: now,
85
81
);
···
89
85
final prefs = ContentLabelPrefs.fromJsonList(contentLabels);
90
86
await _dao.upsertPreference(
91
87
type: 'contentLabels',
88
+
ownerDid: ownerDid,
92
89
data: prefs.toStoredJson(),
93
90
lastSynced: now,
94
91
);
···
97
94
if (labelers != null) {
98
95
await _dao.upsertPreference(
99
96
type: 'labelers',
97
+
ownerDid: ownerDid,
100
98
data: labelers.toStoredJson(),
101
99
lastSynced: now,
102
100
);
···
105
103
if (feedView != null) {
106
104
await _dao.upsertPreference(
107
105
type: 'feedView',
106
+
ownerDid: ownerDid,
108
107
data: feedView.toStoredJson(),
109
108
lastSynced: now,
110
109
);
···
113
112
if (threadView != null) {
114
113
await _dao.upsertPreference(
115
114
type: 'threadView',
115
+
ownerDid: ownerDid,
116
116
data: threadView.toStoredJson(),
117
117
lastSynced: now,
118
118
);
···
121
121
if (mutedWords != null) {
122
122
await _dao.upsertPreference(
123
123
type: 'mutedWords',
124
+
ownerDid: ownerDid,
124
125
data: mutedWords.toStoredJson(),
125
126
lastSynced: now,
126
127
);
···
137
138
}
138
139
139
140
/// Gets the adult content preference.
140
-
Future<AdultContentPref> getAdultContentPref() async {
141
-
final row = await _dao.getPreferenceByType('adultContent');
141
+
Future<AdultContentPref> getAdultContentPref(String ownerDid) async {
142
+
final row = await _dao.getPreferenceByType('adultContent', ownerDid);
142
143
if (row == null) return const AdultContentPref(enabled: false);
143
144
try {
144
145
return AdultContentPref.fromStoredJson(row.data);
···
149
150
}
150
151
151
152
/// Watches the adult content preference for changes.
152
-
Stream<AdultContentPref> watchAdultContentPref() {
153
-
return _dao.watchPreferenceByType('adultContent').map((row) {
153
+
Stream<AdultContentPref> watchAdultContentPref(String ownerDid) {
154
+
return _dao.watchPreferenceByType('adultContent', ownerDid).map((row) {
154
155
if (row == null) return const AdultContentPref(enabled: false);
155
156
try {
156
157
return AdultContentPref.fromStoredJson(row.data);
···
162
163
}
163
164
164
165
/// Gets all content label preferences.
165
-
Future<ContentLabelPrefs> getContentLabelPrefs() async {
166
-
final row = await _dao.getPreferenceByType('contentLabels');
166
+
Future<ContentLabelPrefs> getContentLabelPrefs(String ownerDid) async {
167
+
final row = await _dao.getPreferenceByType('contentLabels', ownerDid);
167
168
if (row == null) return ContentLabelPrefs.empty;
168
169
try {
169
170
return ContentLabelPrefs.fromStoredJson(row.data);
···
174
175
}
175
176
176
177
/// Watches content label preferences for changes.
177
-
Stream<ContentLabelPrefs> watchContentLabelPrefs() {
178
-
return _dao.watchPreferenceByType('contentLabels').map((row) {
178
+
Stream<ContentLabelPrefs> watchContentLabelPrefs(String ownerDid) {
179
+
return _dao.watchPreferenceByType('contentLabels', ownerDid).map((row) {
179
180
if (row == null) return ContentLabelPrefs.empty;
180
181
try {
181
182
return ContentLabelPrefs.fromStoredJson(row.data);
···
187
188
}
188
189
189
190
/// Gets the labelers preference.
190
-
Future<LabelersPref> getLabelersPref() async {
191
-
final row = await _dao.getPreferenceByType('labelers');
191
+
Future<LabelersPref> getLabelersPref(String ownerDid) async {
192
+
final row = await _dao.getPreferenceByType('labelers', ownerDid);
192
193
if (row == null) return LabelersPref.empty;
193
194
try {
194
195
return LabelersPref.fromStoredJson(row.data);
···
199
200
}
200
201
201
202
/// Watches the labelers preference for changes.
202
-
Stream<LabelersPref> watchLabelersPref() {
203
-
return _dao.watchPreferenceByType('labelers').map((row) {
203
+
Stream<LabelersPref> watchLabelersPref(String ownerDid) {
204
+
return _dao.watchPreferenceByType('labelers', ownerDid).map((row) {
204
205
if (row == null) return LabelersPref.empty;
205
206
try {
206
207
return LabelersPref.fromStoredJson(row.data);
···
212
213
}
213
214
214
215
/// Gets the feed view preference.
215
-
Future<FeedViewPref> getFeedViewPref() async {
216
-
final row = await _dao.getPreferenceByType('feedView');
216
+
Future<FeedViewPref> getFeedViewPref(String ownerDid) async {
217
+
final row = await _dao.getPreferenceByType('feedView', ownerDid);
217
218
if (row == null) return FeedViewPref.defaultPref;
218
219
try {
219
220
return FeedViewPref.fromStoredJson(row.data);
···
224
225
}
225
226
226
227
/// Watches the feed view preference for changes.
227
-
Stream<FeedViewPref> watchFeedViewPref() {
228
-
return _dao.watchPreferenceByType('feedView').map((row) {
228
+
Stream<FeedViewPref> watchFeedViewPref(String ownerDid) {
229
+
return _dao.watchPreferenceByType('feedView', ownerDid).map((row) {
229
230
if (row == null) return FeedViewPref.defaultPref;
230
231
try {
231
232
return FeedViewPref.fromStoredJson(row.data);
···
237
238
}
238
239
239
240
/// Gets the thread view preference.
240
-
Future<ThreadViewPref> getThreadViewPref() async {
241
-
final row = await _dao.getPreferenceByType('threadView');
241
+
Future<ThreadViewPref> getThreadViewPref(String ownerDid) async {
242
+
final row = await _dao.getPreferenceByType('threadView', ownerDid);
242
243
if (row == null) return ThreadViewPref.defaultPref;
243
244
try {
244
245
return ThreadViewPref.fromStoredJson(row.data);
···
249
250
}
250
251
251
252
/// Watches the thread view preference for changes.
252
-
Stream<ThreadViewPref> watchThreadViewPref() {
253
-
return _dao.watchPreferenceByType('threadView').map((row) {
253
+
Stream<ThreadViewPref> watchThreadViewPref(String ownerDid) {
254
+
return _dao.watchPreferenceByType('threadView', ownerDid).map((row) {
254
255
if (row == null) return ThreadViewPref.defaultPref;
255
256
try {
256
257
return ThreadViewPref.fromStoredJson(row.data);
···
264
265
/// Updates the feed view preference.
265
266
///
266
267
/// Persists locally and queues for sync to Bluesky.
267
-
Future<void> updateFeedViewPref(FeedViewPref pref) async {
268
+
Future<void> updateFeedViewPref(FeedViewPref pref, String ownerDid) async {
268
269
_logger.info('Updating feed view preference');
269
270
final now = DateTime.now();
270
271
final data = pref.toStoredJson();
271
272
272
-
await _dao.upsertPreference(type: 'feedView', data: data, lastSynced: now);
273
+
await _dao.upsertPreference(type: 'feedView', ownerDid: ownerDid, data: data, lastSynced: now);
273
274
274
-
await _syncQueueDao.enqueueBlueskyPrefSync(preferenceType: 'feedView', preferenceData: data);
275
+
await _syncQueueDao.enqueueBlueskyPrefSync(
276
+
preferenceType: 'feedView',
277
+
preferenceData: data,
278
+
ownerDid: ownerDid,
279
+
);
275
280
276
281
_logger.debug('Feed view preference updated and queued for sync');
277
282
}
···
279
284
/// Updates the thread view preference.
280
285
///
281
286
/// Persists locally and queues for sync to Bluesky.
282
-
Future<void> updateThreadViewPref(ThreadViewPref pref) async {
287
+
Future<void> updateThreadViewPref(ThreadViewPref pref, String ownerDid) async {
283
288
_logger.info('Updating thread view preference');
284
289
final now = DateTime.now();
285
290
final data = pref.toStoredJson();
286
291
287
-
await _dao.upsertPreference(type: 'threadView', data: data, lastSynced: now);
292
+
await _dao.upsertPreference(
293
+
type: 'threadView',
294
+
ownerDid: ownerDid,
295
+
data: data,
296
+
lastSynced: now,
297
+
);
288
298
289
-
await _syncQueueDao.enqueueBlueskyPrefSync(preferenceType: 'threadView', preferenceData: data);
299
+
await _syncQueueDao.enqueueBlueskyPrefSync(
300
+
preferenceType: 'threadView',
301
+
preferenceData: data,
302
+
ownerDid: ownerDid,
303
+
);
290
304
291
305
_logger.debug('Thread view preference updated and queued for sync');
292
306
}
···
294
308
/// Updates the adult content preference.
295
309
///
296
310
/// Persists locally and queues for sync to Bluesky.
297
-
Future<void> updateAdultContentPref(AdultContentPref pref) async {
311
+
Future<void> updateAdultContentPref(AdultContentPref pref, String ownerDid) async {
298
312
_logger.info('Updating adult content preference');
299
313
final now = DateTime.now();
300
314
final data = pref.toStoredJson();
301
315
302
-
await _dao.upsertPreference(type: 'adultContent', data: data, lastSynced: now);
316
+
await _dao.upsertPreference(
317
+
type: 'adultContent',
318
+
ownerDid: ownerDid,
319
+
data: data,
320
+
lastSynced: now,
321
+
);
303
322
304
323
await _syncQueueDao.enqueueBlueskyPrefSync(
305
324
preferenceType: 'adultContent',
306
325
preferenceData: data,
326
+
ownerDid: ownerDid,
307
327
);
308
328
309
329
_logger.debug('Adult content preference updated and queued for sync');
···
312
332
/// Updates content label preferences.
313
333
///
314
334
/// Persists locally and queues for sync to Bluesky.
315
-
Future<void> updateContentLabelPrefs(ContentLabelPrefs prefs) async {
335
+
Future<void> updateContentLabelPrefs(ContentLabelPrefs prefs, String ownerDid) async {
316
336
_logger.info('Updating content label preferences');
317
337
final now = DateTime.now();
318
338
final data = prefs.toStoredJson();
319
339
320
-
await _dao.upsertPreference(type: 'contentLabels', data: data, lastSynced: now);
340
+
await _dao.upsertPreference(
341
+
type: 'contentLabels',
342
+
ownerDid: ownerDid,
343
+
data: data,
344
+
lastSynced: now,
345
+
);
321
346
322
347
await _syncQueueDao.enqueueBlueskyPrefSync(
323
348
preferenceType: 'contentLabels',
324
349
preferenceData: data,
350
+
ownerDid: ownerDid,
325
351
);
326
352
327
353
_logger.debug('Content label preferences updated and queued for sync');
328
354
}
329
355
330
356
/// Gets the muted words preference.
331
-
Future<MutedWordsPref> getMutedWordsPref() async {
332
-
final row = await _dao.getPreferenceByType('mutedWords');
357
+
Future<MutedWordsPref> getMutedWordsPref(String ownerDid) async {
358
+
final row = await _dao.getPreferenceByType('mutedWords', ownerDid);
333
359
if (row == null) return MutedWordsPref.empty;
334
360
try {
335
361
return MutedWordsPref.fromStoredJson(row.data);
···
340
366
}
341
367
342
368
/// Watches the muted words preference for changes.
343
-
Stream<MutedWordsPref> watchMutedWordsPref() {
344
-
return _dao.watchPreferenceByType('mutedWords').map((row) {
369
+
Stream<MutedWordsPref> watchMutedWordsPref(String ownerDid) {
370
+
return _dao.watchPreferenceByType('mutedWords', ownerDid).map((row) {
345
371
if (row == null) return MutedWordsPref.empty;
346
372
try {
347
373
return MutedWordsPref.fromStoredJson(row.data);
···
355
381
/// Updates the muted words preference.
356
382
///
357
383
/// Persists locally and queues for sync to Bluesky.
358
-
Future<void> updateMutedWordsPref(MutedWordsPref pref) async {
384
+
Future<void> updateMutedWordsPref(MutedWordsPref pref, String ownerDid) async {
359
385
_logger.info('Updating muted words preference');
360
386
final now = DateTime.now();
361
387
final data = pref.toStoredJson();
362
388
363
-
await _dao.upsertPreference(type: 'mutedWords', data: data, lastSynced: now);
389
+
await _dao.upsertPreference(
390
+
type: 'mutedWords',
391
+
ownerDid: ownerDid,
392
+
data: data,
393
+
lastSynced: now,
394
+
);
364
395
365
-
await _syncQueueDao.enqueueBlueskyPrefSync(preferenceType: 'mutedWords', preferenceData: data);
396
+
await _syncQueueDao.enqueueBlueskyPrefSync(
397
+
preferenceType: 'mutedWords',
398
+
preferenceData: data,
399
+
ownerDid: ownerDid,
400
+
);
366
401
367
402
_logger.debug('Muted words preference updated and queued for sync');
368
403
}
···
370
405
/// Clears all cached preferences.
371
406
///
372
407
/// Useful when signing out.
373
-
Future<void> clearAll() async {
408
+
Future<void> clearAll(String ownerDid) async {
374
409
_logger.info('Clearing all Bluesky preferences');
375
-
await _dao.clearAll();
410
+
await _dao.clearAll(ownerDid);
376
411
}
377
412
378
413
/// Processes the preference sync queue, retrying failed updates.
···
384
419
/// 4. Deletes the queue item on success, or increments retry count on failure
385
420
///
386
421
/// Also cleans up old permanently failed items (> 30 days).
387
-
Future<void> processSyncQueue() async {
422
+
Future<void> processSyncQueue(String ownerDid) async {
388
423
if (!_api.isAuthenticated) return;
389
424
390
425
final threshold = DateTime.now().subtract(const Duration(days: 30));
391
426
await _syncQueueDao.cleanupOldFailedItems(threshold);
392
427
393
-
final retryable = await _syncQueueDao.getRetryableBlueskyPrefItems();
428
+
final retryable = await _syncQueueDao.getRetryableBlueskyPrefItems(ownerDid);
394
429
if (retryable.isEmpty) {
395
430
return;
396
431
}
+15
-4
test/src/app/app_test.dart
+15
-4
test/src/app/app_test.dart
···
48
48
);
49
49
when(() => mockSessionStorage.getSession()).thenAnswer((_) async => testSession);
50
50
when(() => mockSearchRepository.watchRecentSearches()).thenAnswer((_) => Stream.value([]));
51
-
when(() => mockProfileRepository.getProfile(any())).thenAnswer(
51
+
when(
52
+
() => mockProfileRepository.getProfile(any(named: 'actor'), any(named: 'ownerDid')),
53
+
).thenAnswer(
52
54
(_) async => ProfileData(
53
55
did: 'did:web:test',
54
56
handle: 'handle',
···
62
64
);
63
65
when(() => mockProfileRepository.watchProfile(any())).thenAnswer((_) => Stream.value(null));
64
66
when(
65
-
() => mockFeedContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
67
+
() => mockFeedContentRepository.watchFeedContent(
68
+
ownerDid: any(named: 'ownerDid'),
69
+
feedKey: any(named: 'feedKey'),
70
+
),
66
71
).thenAnswer((_) => Stream.value([]));
67
72
when(
68
-
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: any(named: 'feedUri')),
73
+
() => mockFeedContentRepository.fetchAndCacheFeed(
74
+
ownerDid: any(named: 'ownerDid'),
75
+
feedUri: any(named: 'feedUri'),
76
+
),
69
77
).thenAnswer((_) async {});
70
78
when(
71
79
() => mockFeedContentRepository.fetchAndCacheFeed(
80
+
ownerDid: any(named: 'ownerDid'),
72
81
cursor: any(named: 'cursor'),
73
82
feedUri: any(named: 'feedUri'),
74
83
),
75
84
).thenAnswer((_) async {});
76
-
when(() => mockFeedContentRepository.getCursor(any())).thenAnswer((_) async => null);
85
+
when(
86
+
() => mockFeedContentRepository.getCursor(any(named: 'feedKey'), any(named: 'ownerDid')),
87
+
).thenAnswer((_) async => null);
77
88
});
78
89
79
90
List<Override> getTestOverrides() {
+36
-11
test/src/app/router_test.dart
+36
-11
test/src/app/router_test.dart
···
53
53
mockNotificationsRepository = MockNotificationsRepository();
54
54
mockDmsRepository = MockDmsRepository();
55
55
when(
56
-
() => mockNotificationsRepository.watchNotifications(),
56
+
() => mockNotificationsRepository.watchNotifications(any(named: 'ownerDid')),
57
57
).thenAnswer((_) => Stream.value([]));
58
-
when(() => mockNotificationsRepository.getCursor()).thenAnswer((_) async => null);
59
58
when(
60
-
() => mockNotificationsRepository.fetchNotifications(cursor: any(named: 'cursor')),
59
+
() => mockNotificationsRepository.getCursor(any(named: 'ownerDid')),
60
+
).thenAnswer((_) async => null);
61
+
when(
62
+
() => mockNotificationsRepository.fetchNotifications(
63
+
cursor: any(named: 'cursor'),
64
+
ownerDid: any(named: 'ownerDid'),
65
+
),
61
66
).thenAnswer((_) async {});
62
-
when(() => mockNotificationsRepository.fetchNotifications()).thenAnswer((_) async {});
63
-
when(() => mockDmsRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
67
+
when(
68
+
() => mockNotificationsRepository.fetchNotifications(ownerDid: any(named: 'ownerDid')),
69
+
).thenAnswer((_) async {});
70
+
when(
71
+
() => mockDmsRepository.watchConversations(any(named: 'ownerDid')),
72
+
).thenAnswer((_) => Stream.value([]));
73
+
when(
74
+
() => mockDmsRepository.fetchConversations(
75
+
cursor: any(named: 'cursor'),
76
+
ownerDid: any(named: 'ownerDid'),
77
+
),
78
+
).thenAnswer((_) async => null);
64
79
when(
65
-
() => mockDmsRepository.fetchConversations(cursor: any(named: 'cursor')),
80
+
() => mockDmsRepository.fetchConversations(ownerDid: any(named: 'ownerDid')),
66
81
).thenAnswer((_) async => null);
67
-
when(() => mockDmsRepository.fetchConversations()).thenAnswer((_) async => null);
68
82
testSession = Session(
69
83
did: 'did:web:test',
70
84
handle: 'handle',
···
77
91
);
78
92
when(() => mockSessionStorage.getSession()).thenAnswer((_) async => testSession);
79
93
when(() => mockSearchRepository.watchRecentSearches()).thenAnswer((_) => Stream.value([]));
80
-
when(() => mockProfileRepository.getProfile(any())).thenAnswer(
94
+
when(
95
+
() => mockProfileRepository.getProfile(any(named: 'actor'), any(named: 'ownerDid')),
96
+
).thenAnswer(
81
97
(_) async => ProfileData(
82
98
did: 'did:web:test',
83
99
handle: 'handle',
···
91
107
);
92
108
93
109
when(
94
-
() => mockFeedContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
110
+
() => mockFeedContentRepository.watchFeedContent(
111
+
feedKey: any(named: 'feedKey'),
112
+
ownerDid: any(named: 'ownerDid'),
113
+
),
95
114
).thenAnswer((_) => Stream.value([]));
96
115
when(
97
-
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: any(named: 'feedUri')),
116
+
() => mockFeedContentRepository.fetchAndCacheFeed(
117
+
feedUri: any(named: 'feedUri'),
118
+
ownerDid: any(named: 'ownerDid'),
119
+
),
98
120
).thenAnswer((_) async {});
99
121
when(
100
122
() => mockFeedContentRepository.fetchAndCacheFeed(
101
123
cursor: any(named: 'cursor'),
102
124
feedUri: any(named: 'feedUri'),
125
+
ownerDid: any(named: 'ownerDid'),
103
126
),
104
127
).thenAnswer((_) async {});
105
-
when(() => mockFeedContentRepository.getCursor(any())).thenAnswer((_) async => null);
128
+
when(
129
+
() => mockFeedContentRepository.getCursor(any(named: 'feedKey'), any(named: 'ownerDid')),
130
+
).thenAnswer((_) async => null);
106
131
});
107
132
108
133
List<Override> getTestOverrides() {
+31
-22
test/src/features/dms/infrastructure/dms_repository_test.dart
+31
-22
test/src/features/dms/infrastructure/dms_repository_test.dart
···
16
16
late MockDmMessagesDao mockMessagesDao;
17
17
late MockLogger mockLogger;
18
18
late DmsRepository repository;
19
+
const ownerDid = 'did:web:tester';
19
20
20
21
setUp(() {
21
22
mockApi = MockXrpcClient();
···
42
43
),
43
44
).thenAnswer((_) async {});
44
45
45
-
await repository.fetchConversations();
46
+
await repository.fetchConversations(ownerDid: ownerDid);
46
47
47
48
verify(
48
49
() => mockApi.call(
···
63
64
),
64
65
).thenAnswer((_) async {});
65
66
66
-
await repository.fetchConversations(cursor: 'test_cursor');
67
+
await repository.fetchConversations(ownerDid: ownerDid, cursor: 'test_cursor');
67
68
68
69
verify(
69
70
() => mockApi.call(
···
107
108
capturedProfiles = invocation.namedArguments[#newProfiles] as List;
108
109
});
109
110
110
-
final cursor = await repository.fetchConversations();
111
+
final cursor = await repository.fetchConversations(ownerDid: ownerDid);
111
112
112
113
expect(cursor, 'next_cursor');
113
114
expect(capturedConvos, hasLength(1));
···
134
135
capturedConvos = invocation.namedArguments[#newConvos] as List;
135
136
});
136
137
137
-
await repository.fetchConversations();
138
+
await repository.fetchConversations(ownerDid: ownerDid);
138
139
139
140
expect(capturedConvos, isEmpty);
140
141
});
···
144
145
() => mockApi.call(any(), params: any(named: 'params')),
145
146
).thenThrow(Exception('Network error'));
146
147
147
-
expect(() => repository.fetchConversations(), throwsException);
148
+
expect(() => repository.fetchConversations(ownerDid: ownerDid), throwsException);
148
149
149
150
verify(() => mockLogger.error(any(), any(), any())).called(1);
150
151
});
···
162
163
),
163
164
).thenAnswer((_) async {});
164
165
165
-
await repository.fetchMessages('convo1');
166
+
await repository.fetchMessages('convo1', ownerDid: ownerDid);
166
167
167
168
verify(
168
169
() => mockApi.call(
···
197
198
capturedMessages = invocation.namedArguments[#newMessages] as List;
198
199
});
199
200
200
-
final cursor = await repository.fetchMessages('convo1');
201
+
final cursor = await repository.fetchMessages('convo1', ownerDid: ownerDid);
201
202
202
203
expect(cursor, 'next_cursor');
203
204
expect(capturedMessages, hasLength(1));
···
207
208
group('acceptConversation', () {
208
209
test('calls acceptConvo API and updates DAO', () async {
209
210
when(() => mockApi.call(any(), body: any(named: 'body'))).thenAnswer((_) async => {});
210
-
when(() => mockConvosDao.acceptConvo(any())).thenAnswer((_) async {});
211
+
when(() => mockConvosDao.acceptConvo(any(), any())).thenAnswer((_) async {});
211
212
212
-
await repository.acceptConversation('convo1');
213
+
await repository.acceptConversation('convo1', ownerDid);
213
214
214
215
verify(
215
216
() => mockApi.call('chat.bsky.convo.acceptConvo', body: {'convoId': 'convo1'}),
216
217
).called(1);
217
-
verify(() => mockConvosDao.acceptConvo('convo1')).called(1);
218
+
verify(() => mockConvosDao.acceptConvo('convo1', ownerDid)).called(1);
218
219
});
219
220
});
220
221
···
223
224
when(() => mockApi.call(any(), body: any(named: 'body'))).thenAnswer((_) async => {});
224
225
when(
225
226
() => mockConvosDao.updateReadState(
227
+
ownerDid: any(named: 'ownerDid'),
226
228
convoId: any(named: 'convoId'),
227
229
lastReadMessageId: any(named: 'lastReadMessageId'),
228
230
unreadCount: any(named: 'unreadCount'),
229
231
),
230
232
).thenAnswer((_) async {});
231
233
232
-
await repository.updateReadState('convo1', 'msg123');
234
+
await repository.updateReadState(
235
+
convoId: 'convo1',
236
+
messageId: 'msg123',
237
+
ownerDid: ownerDid,
238
+
);
233
239
234
240
verify(
235
241
() => mockApi.call(
···
239
245
).called(1);
240
246
verify(
241
247
() => mockConvosDao.updateReadState(
248
+
ownerDid: ownerDid,
242
249
convoId: 'convo1',
243
250
lastReadMessageId: 'msg123',
244
251
unreadCount: 0,
···
249
256
250
257
group('watchConversations', () {
251
258
test('returns stream from DAO mapped to domain models', () async {
252
-
when(() => mockConvosDao.watchConversations()).thenAnswer((_) => Stream.value([]));
259
+
when(() => mockConvosDao.watchConversations(any())).thenAnswer((_) => Stream.value([]));
253
260
254
-
final stream = repository.watchConversations();
261
+
final stream = repository.watchConversations(ownerDid);
255
262
final result = await stream.first;
256
263
257
264
expect(result, isEmpty);
258
-
verify(() => mockConvosDao.watchConversations()).called(1);
265
+
verify(() => mockConvosDao.watchConversations(ownerDid)).called(1);
259
266
});
260
267
});
261
268
262
269
group('watchMessages', () {
263
270
test('returns stream from DAO mapped to domain models', () async {
264
271
when(
265
-
() => mockMessagesDao.watchMessagesByConvo(any()),
272
+
() => mockMessagesDao.watchMessagesByConvo(any(), any()),
266
273
).thenAnswer((_) => Stream.value([]));
267
274
268
-
final stream = repository.watchMessages('convo1');
275
+
final stream = repository.watchMessages('convo1', ownerDid);
269
276
final result = await stream.first;
270
277
271
278
expect(result, isEmpty);
272
-
verify(() => mockMessagesDao.watchMessagesByConvo('convo1')).called(1);
279
+
verify(() => mockMessagesDao.watchMessagesByConvo('convo1', ownerDid)).called(1);
273
280
});
274
281
});
275
282
276
283
group('clearAll', () {
277
284
test('clears messages and conversations', () async {
278
-
when(() => mockMessagesDao.clearMessages()).thenAnswer((_) async {});
279
-
when(() => mockConvosDao.clearConversations()).thenAnswer((_) async {});
285
+
when(() => mockMessagesDao.clearMessages(any())).thenAnswer((_) async {
286
+
return 0;
287
+
});
288
+
when(() => mockConvosDao.clearConversations(any())).thenAnswer((_) async {});
280
289
281
-
await repository.clearAll();
290
+
await repository.clearAll(ownerDid);
282
291
283
-
verify(() => mockMessagesDao.clearMessages()).called(1);
284
-
verify(() => mockConvosDao.clearConversations()).called(1);
292
+
verify(() => mockMessagesDao.clearMessages(ownerDid)).called(1);
293
+
verify(() => mockConvosDao.clearConversations(ownerDid)).called(1);
285
294
});
286
295
});
287
296
});
+42
-18
test/src/features/dms/infrastructure/outbox_repository_test.dart
+42
-18
test/src/features/dms/infrastructure/outbox_repository_test.dart
···
46
46
when(() => mockOutboxDao.enqueue(any())).thenAnswer((_) async {});
47
47
when(() => mockMessagesDao.insertLocalMessage(any())).thenAnswer((_) async {});
48
48
49
-
final outboxId = await repository.enqueueSend('convo1', 'Hello!');
49
+
final outboxId = await repository.enqueueSend('convo1', 'Hello!', 'did:plc:owner');
50
50
51
51
expect(outboxId, isNotEmpty);
52
52
verify(() => mockOutboxDao.enqueue(any())).called(1);
···
56
56
57
57
group('processOutbox', () {
58
58
test('does nothing when queue is empty', () async {
59
-
when(() => mockOutboxDao.getPending()).thenAnswer((_) async => []);
59
+
when(() => mockOutboxDao.getPending(any(named: 'ownerDid'))).thenAnswer((_) async => []);
60
60
61
-
await repository.processOutbox();
61
+
await repository.processOutbox('did:plc:owner');
62
62
63
63
verifyNever(() => mockApi.call(any(), body: any(named: 'body')));
64
64
});
···
74
74
createdAt: now,
75
75
lastAttemptAt: null,
76
76
errorMessage: null,
77
+
ownerDid: 'did:plc:owner',
77
78
);
78
79
79
-
when(() => mockOutboxDao.getPending()).thenAnswer((_) async => [pendingItem]);
80
+
when(
81
+
() => mockOutboxDao.getPending(any(named: 'ownerDid')),
82
+
).thenAnswer((_) async => [pendingItem]);
80
83
when(
81
84
() => mockOutboxDao.updateStatus(
82
85
outboxId: any(named: 'outboxId'),
···
88
91
() => mockMessagesDao.updateMessageStatus(
89
92
messageId: any(named: 'messageId'),
90
93
status: any(named: 'status'),
94
+
ownerDid: any(named: 'ownerDid'),
91
95
),
92
96
).thenAnswer((_) async {});
93
97
when(
···
95
99
).thenAnswer((_) async => {'id': 'server_msg_id'});
96
100
when(() => mockOutboxDao.deleteItem(any())).thenAnswer((_) async => 1);
97
101
98
-
await repository.processOutbox();
102
+
await repository.processOutbox('did:plc:owner');
99
103
100
104
verify(
101
105
() => mockApi.call('chat.bsky.convo.sendMessage', body: any(named: 'body')),
···
114
118
createdAt: now,
115
119
lastAttemptAt: null,
116
120
errorMessage: null,
121
+
ownerDid: 'did:plc:owner',
117
122
);
118
123
119
-
when(() => mockOutboxDao.getPending()).thenAnswer((_) async => [pendingItem]);
124
+
when(
125
+
() => mockOutboxDao.getPending(any(named: 'ownerDid')),
126
+
).thenAnswer((_) async => [pendingItem]);
120
127
when(
121
128
() => mockOutboxDao.updateStatus(
122
129
outboxId: any(named: 'outboxId'),
···
128
135
() => mockMessagesDao.updateMessageStatus(
129
136
messageId: any(named: 'messageId'),
130
137
status: any(named: 'status'),
138
+
ownerDid: any(named: 'ownerDid'),
131
139
),
132
140
).thenAnswer((_) async {});
133
141
when(
···
144
152
createdAt: now,
145
153
lastAttemptAt: now,
146
154
errorMessage: 'Error',
155
+
ownerDid: 'did:plc:owner',
147
156
),
148
157
);
149
158
150
-
await repository.processOutbox();
159
+
await repository.processOutbox('did:plc:owner');
151
160
152
161
verify(() => mockOutboxDao.incrementRetryCount('outbox1')).called(1);
153
162
verifyNever(() => mockOutboxDao.deleteItem(any()));
···
164
173
createdAt: now,
165
174
lastAttemptAt: null,
166
175
errorMessage: null,
176
+
ownerDid: 'did:plc:owner',
167
177
);
168
178
169
-
when(() => mockOutboxDao.getPending()).thenAnswer((_) async => [pendingItem]);
179
+
when(
180
+
() => mockOutboxDao.getPending(any(named: 'ownerDid')),
181
+
).thenAnswer((_) async => [pendingItem]);
170
182
when(
171
183
() => mockOutboxDao.updateStatus(
172
184
outboxId: any(named: 'outboxId'),
···
178
190
() => mockMessagesDao.updateMessageStatus(
179
191
messageId: any(named: 'messageId'),
180
192
status: any(named: 'status'),
193
+
ownerDid: any(named: 'ownerDid'),
181
194
),
182
195
).thenAnswer((_) async {});
183
196
when(
···
194
207
createdAt: now,
195
208
lastAttemptAt: now,
196
209
errorMessage: 'Error',
210
+
ownerDid: 'did:plc:owner',
197
211
),
198
212
);
199
213
200
-
await repository.processOutbox();
214
+
await repository.processOutbox('did:plc:owner');
201
215
202
216
verify(
203
217
() => mockOutboxDao.updateStatus(
···
220
234
createdAt: now.subtract(const Duration(minutes: 2)),
221
235
lastAttemptAt: null,
222
236
errorMessage: null,
237
+
ownerDid: 'did:plc:owner',
223
238
),
224
239
DmOutboxData(
225
240
outboxId: 'outbox2',
···
230
245
createdAt: now.subtract(const Duration(minutes: 1)),
231
246
lastAttemptAt: null,
232
247
errorMessage: null,
248
+
ownerDid: 'did:plc:owner',
233
249
),
234
250
DmOutboxData(
235
251
outboxId: 'outbox3',
···
240
256
createdAt: now,
241
257
lastAttemptAt: null,
242
258
errorMessage: null,
259
+
ownerDid: 'did:plc:owner',
243
260
),
244
261
];
245
262
246
-
when(() => mockOutboxDao.getPending()).thenAnswer((_) async => items);
263
+
when(
264
+
() => mockOutboxDao.getPending(any(named: 'ownerDid')),
265
+
).thenAnswer((_) async => items);
247
266
when(
248
267
() => mockOutboxDao.updateStatus(
249
268
outboxId: any(named: 'outboxId'),
···
255
274
() => mockMessagesDao.updateMessageStatus(
256
275
messageId: any(named: 'messageId'),
257
276
status: any(named: 'status'),
277
+
ownerDid: any(named: 'ownerDid'),
258
278
),
259
279
).thenAnswer((_) async {});
260
280
when(
···
262
282
).thenAnswer((_) async => {'id': 'server_msg_id'});
263
283
when(() => mockOutboxDao.deleteItem(any())).thenAnswer((_) async => 1);
264
284
265
-
await repository.processOutbox();
285
+
await repository.processOutbox('did:plc:owner');
266
286
267
287
verify(
268
288
() => mockApi.call('chat.bsky.convo.sendMessage', body: any(named: 'body')),
···
285
305
createdAt: now,
286
306
lastAttemptAt: null,
287
307
errorMessage: null,
308
+
ownerDid: 'did:plc:owner',
288
309
);
289
310
290
311
when(() => mockOutboxDao.resetForRetry(any())).thenAnswer((_) async {});
···
292
313
() => mockMessagesDao.updateMessageStatus(
293
314
messageId: any(named: 'messageId'),
294
315
status: any(named: 'status'),
316
+
ownerDid: any(named: 'ownerDid'),
295
317
),
296
318
).thenAnswer((_) async {});
297
319
when(() => mockOutboxDao.getById(any())).thenAnswer((_) async => item);
···
307
329
).thenAnswer((_) async => {'id': 'server_msg_id'});
308
330
when(() => mockOutboxDao.deleteItem(any())).thenAnswer((_) async => 1);
309
331
310
-
await repository.retryMessage('outbox1');
332
+
await repository.retryMessage('outbox1', 'did:plc:owner');
311
333
312
334
verify(() => mockOutboxDao.resetForRetry('outbox1')).called(1);
313
335
verify(
···
319
341
group('deleteOutboxItem', () {
320
342
test('removes item from outbox and deletes local message', () async {
321
343
when(() => mockOutboxDao.deleteItem(any())).thenAnswer((_) async => 1);
322
-
when(() => mockMessagesDao.deleteMessage(any())).thenAnswer((_) async => 1);
344
+
when(
345
+
() => mockMessagesDao.deleteMessage(any(), any(named: 'ownerDid')),
346
+
).thenAnswer((_) async => 1);
323
347
324
-
await repository.deleteOutboxItem('outbox1');
348
+
await repository.deleteOutboxItem('outbox1', 'did:plc:owner');
325
349
326
350
verify(() => mockOutboxDao.deleteItem('outbox1')).called(1);
327
-
verify(() => mockMessagesDao.deleteMessage('pending:outbox1')).called(1);
351
+
verify(() => mockMessagesDao.deleteMessage('pending:outbox1', 'did:plc:owner')).called(1);
328
352
});
329
353
});
330
354
331
355
group('getPendingCount', () {
332
356
test('returns count from DAO', () async {
333
-
when(() => mockOutboxDao.countPending()).thenAnswer((_) async => 5);
357
+
when(() => mockOutboxDao.countPending(any(named: 'ownerDid'))).thenAnswer((_) async => 5);
334
358
335
-
final count = await repository.getPendingCount();
359
+
final count = await repository.getPendingCount('did:plc:owner');
336
360
337
361
expect(count, 5);
338
-
verify(() => mockOutboxDao.countPending()).called(1);
362
+
verify(() => mockOutboxDao.countPending('did:plc:owner')).called(1);
339
363
});
340
364
});
341
365
});
+34
-12
test/src/features/dms/presentation/conversation_list_notifier_test.dart
+34
-12
test/src/features/dms/presentation/conversation_list_notifier_test.dart
···
23
23
});
24
24
25
25
test('build watches conversations', () async {
26
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
26
+
when(
27
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
28
+
).thenAnswer((_) => Stream.value([]));
27
29
28
30
final subscription = container.listen(conversationListProvider, (previous, next) {});
29
31
···
35
37
await container.read(conversationListProvider.future);
36
38
37
39
expect(container.read(conversationListProvider).value, isEmpty);
38
-
verify(() => mockRepository.watchConversations()).called(1);
40
+
verify(() => mockRepository.watchConversations(any(named: 'ownerDid'))).called(1);
39
41
40
42
subscription.close();
41
43
});
42
44
43
45
test('refresh fetches conversations', () async {
44
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
45
-
when(() => mockRepository.fetchConversations()).thenAnswer((_) async => 'cursor-1');
46
+
when(
47
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
48
+
).thenAnswer((_) => Stream.value([]));
49
+
when(
50
+
() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid')),
51
+
).thenAnswer((_) async => 'cursor-1');
46
52
47
53
final notifier = container.read(conversationListProvider.notifier);
48
54
await notifier.refresh();
49
55
50
-
verify(() => mockRepository.fetchConversations()).called(1);
56
+
verify(() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid'))).called(1);
51
57
});
52
58
53
59
test('loadMore fetches conversations with cursor', () async {
54
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
55
-
when(() => mockRepository.fetchConversations()).thenAnswer((_) async => 'cursor-1');
60
+
when(
61
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
62
+
).thenAnswer((_) => Stream.value([]));
56
63
when(
57
-
() => mockRepository.fetchConversations(cursor: 'cursor-1'),
64
+
() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid')),
65
+
).thenAnswer((_) async => 'cursor-1');
66
+
when(
67
+
() => mockRepository.fetchConversations(
68
+
ownerDid: any(named: 'ownerDid'),
69
+
cursor: 'cursor-1',
70
+
),
58
71
).thenAnswer((_) async => 'cursor-2');
59
72
60
73
final notifier = container.read(conversationListProvider.notifier);
···
63
76
64
77
await notifier.loadMore();
65
78
66
-
verify(() => mockRepository.fetchConversations(cursor: 'cursor-1')).called(1);
79
+
verify(
80
+
() => mockRepository.fetchConversations(
81
+
cursor: 'cursor-1',
82
+
ownerDid: any(named: 'ownerDid'),
83
+
),
84
+
).called(1);
67
85
});
68
86
69
87
test('acceptConversation calls repository', () async {
70
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
71
-
when(() => mockRepository.acceptConversation(any())).thenAnswer((_) async {});
88
+
when(
89
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
90
+
).thenAnswer((_) => Stream.value([]));
91
+
when(
92
+
() => mockRepository.acceptConversation(any(named: 'convoId'), any(named: 'ownerDid')),
93
+
).thenAnswer((_) async {});
72
94
73
95
final notifier = container.read(conversationListProvider.notifier);
74
96
await notifier.acceptConversation('123');
75
97
76
-
verify(() => mockRepository.acceptConversation('123')).called(1);
98
+
verify(() => mockRepository.acceptConversation('123', any(named: 'ownerDid'))).called(1);
77
99
});
78
100
});
79
101
}
+59
-28
test/src/features/dms/presentation/conversation_list_screen_test.dart
+59
-28
test/src/features/dms/presentation/conversation_list_screen_test.dart
···
20
20
setUp(() {
21
21
mockRepository = MockDmsRepository();
22
22
when(
23
-
() => mockRepository.fetchConversations(cursor: any(named: 'cursor')),
23
+
() => mockRepository.fetchConversations(
24
+
cursor: any(named: 'cursor'),
25
+
ownerDid: any(named: 'ownerDid'),
26
+
),
24
27
).thenAnswer((_) async => null);
25
-
when(() => mockRepository.muteConversation(any())).thenAnswer((_) async {});
26
-
when(() => mockRepository.unmuteConversation(any())).thenAnswer((_) async {});
27
-
when(() => mockRepository.leaveConversation(any())).thenAnswer((_) async {});
28
28
when(
29
-
() => mockRepository.fetchMessages(any(), limit: any(named: 'limit')),
29
+
() => mockRepository.muteConversation(any(), any(named: 'ownerDid')),
30
+
).thenAnswer((_) async {});
31
+
when(
32
+
() => mockRepository.unmuteConversation(any(), any(named: 'ownerDid')),
33
+
).thenAnswer((_) async {});
34
+
when(
35
+
() => mockRepository.leaveConversation(any(), any(named: 'ownerDid')),
36
+
).thenAnswer((_) async {});
37
+
when(
38
+
() => mockRepository.fetchMessages(
39
+
any(),
40
+
limit: any(named: 'limit'),
41
+
ownerDid: any(named: 'ownerDid'),
42
+
),
30
43
).thenAnswer((_) async => null);
31
-
when(() => mockRepository.watchMessages(any())).thenAnswer((_) => Stream.value([]));
32
-
when(() => mockRepository.updateReadState(any(), any())).thenAnswer((_) async {});
44
+
when(
45
+
() => mockRepository.watchMessages(any(), any(named: 'ownerDid')),
46
+
).thenAnswer((_) => Stream.value([]));
47
+
when(
48
+
() => mockRepository.updateReadState(convoId: any(), messageId: any(), ownerDid: any()),
49
+
).thenAnswer((_) async {});
33
50
});
34
51
35
52
const profile = Profile(
···
51
68
final controller = StreamController<List<DmConversation>>();
52
69
addTearDown(controller.close);
53
70
54
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => controller.stream);
71
+
when(
72
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
73
+
).thenAnswer((_) => controller.stream);
55
74
56
75
await tester.pumpApp(
57
76
const ConversationListScreen(),
···
63
82
});
64
83
65
84
testWidgets('renders empty state when no conversations', (tester) async {
66
-
when(() => mockRepository.watchConversations()).thenAnswer((_) => Stream.value([]));
67
-
when(() => mockRepository.fetchConversations()).thenAnswer((_) async => null);
85
+
when(
86
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
87
+
).thenAnswer((_) => Stream.value([]));
88
+
when(
89
+
() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid')),
90
+
).thenAnswer((_) async => null);
68
91
69
92
await tester.pumpApp(
70
93
const ConversationListScreen(),
···
78
101
79
102
testWidgets('renders conversation list', (tester) async {
80
103
when(
81
-
() => mockRepository.watchConversations(),
104
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
82
105
).thenAnswer((_) => Stream.value([conversation]));
83
106
84
107
await tester.pumpApp(
···
94
117
testWidgets('renders message requests section', (tester) async {
95
118
final requestConvo = conversation.copyWith(isAccepted: false, convoId: '456');
96
119
when(
97
-
() => mockRepository.watchConversations(),
120
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
98
121
).thenAnswer((_) => Stream.value([requestConvo, conversation]));
99
122
100
123
await tester.pumpApp(
···
110
133
111
134
testWidgets('triggers refresh on pull to refresh', (tester) async {
112
135
when(
113
-
() => mockRepository.watchConversations(),
136
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
114
137
).thenAnswer((_) => Stream.value([conversation]));
115
-
when(() => mockRepository.fetchConversations()).thenAnswer((_) async => 'new-cursor');
138
+
when(
139
+
() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid')),
140
+
).thenAnswer((_) async => 'new-cursor');
116
141
117
142
await tester.pumpApp(
118
143
const ConversationListScreen(),
···
123
148
await tester.drag(find.byType(CustomScrollView), const Offset(0, 300));
124
149
await tester.pumpAndSettle();
125
150
126
-
verify(() => mockRepository.fetchConversations()).called(1);
151
+
verify(() => mockRepository.fetchConversations(ownerDid: any(named: 'ownerDid'))).called(1);
127
152
});
128
153
129
154
testWidgets('shows FAB', (tester) async {
130
155
when(
131
-
() => mockRepository.watchConversations(),
156
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
132
157
).thenAnswer((_) => Stream.value([conversation]));
133
158
134
159
await tester.pumpApp(
···
142
167
143
168
testWidgets('triggers mark as read on swipe right', (tester) async {
144
169
when(
145
-
() => mockRepository.watchConversations(),
170
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
146
171
).thenAnswer((_) => Stream.value([conversation]));
147
172
148
173
final message = dmm.AppDmMessage(
···
154
179
status: dmm.MessageStatus.sent,
155
180
);
156
181
157
-
when(() => mockRepository.watchMessages('123')).thenAnswer((_) => Stream.value([message]));
182
+
when(
183
+
() => mockRepository.watchMessages('123', any(named: 'ownerDid')),
184
+
).thenAnswer((_) => Stream.value([message]));
158
185
159
186
await tester.pumpApp(
160
187
const ConversationListScreen(),
···
162
189
);
163
190
await tester.pumpAndSettle();
164
191
165
-
// Swipe right (Start to End)
166
192
await tester.drag(find.byType(ConversationListItem), const Offset(500, 0));
167
193
await tester.pumpAndSettle();
168
194
169
-
verify(() => mockRepository.fetchMessages('123', limit: 1)).called(1);
170
-
verify(() => mockRepository.updateReadState('123', 'msg1')).called(1);
195
+
verify(
196
+
() => mockRepository.fetchMessages('123', ownerDid: any(named: 'ownerDid'), limit: 1),
197
+
).called(1);
198
+
verify(
199
+
() => mockRepository.updateReadState(
200
+
convoId: '123',
201
+
messageId: 'msg1',
202
+
ownerDid: any(named: 'ownerDid'),
203
+
),
204
+
).called(1);
171
205
});
172
206
173
207
testWidgets('triggers leave conversation on swipe left and confirm', (tester) async {
174
208
when(
175
-
() => mockRepository.watchConversations(),
209
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
176
210
).thenAnswer((_) => Stream.value([conversation]));
177
211
178
212
await tester.pumpApp(
···
181
215
);
182
216
await tester.pumpAndSettle();
183
217
184
-
// Swipe left (End to Start) - confirmDismiss logic makes this tricky with tester.drag
185
-
// unless we assume direction is correct.
186
-
// Offset -500 is left.
187
218
await tester.drag(find.byType(ConversationListItem), const Offset(-500, 0));
188
219
await tester.pumpAndSettle();
189
220
···
192
223
await tester.tap(find.text('Leave'));
193
224
await tester.pumpAndSettle();
194
225
195
-
verify(() => mockRepository.leaveConversation('123')).called(1);
226
+
verify(() => mockRepository.leaveConversation('123', any(named: 'ownerDid'))).called(1);
196
227
});
197
228
198
229
testWidgets('triggers mute/unmute on long press', (tester) async {
199
230
when(
200
-
() => mockRepository.watchConversations(),
231
+
() => mockRepository.watchConversations(any(named: 'ownerDid')),
201
232
).thenAnswer((_) => Stream.value([conversation]));
202
233
203
234
await tester.pumpApp(
···
214
245
await tester.tap(find.text('Mute'));
215
246
await tester.pumpAndSettle();
216
247
217
-
verify(() => mockRepository.muteConversation('123')).called(1);
248
+
verify(() => mockRepository.muteConversation('123', any(named: 'ownerDid'))).called(1);
218
249
});
219
250
});
220
251
}
+69
-20
test/src/features/feeds/application/feed_content_notifier_test.dart
+69
-20
test/src/features/feeds/application/feed_content_notifier_test.dart
···
39
39
40
40
registerFallbackValue('');
41
41
when(
42
-
() => mockRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
42
+
() => mockRepository.watchFeedContent(
43
+
feedKey: any(named: 'feedKey'),
44
+
ownerDid: any(named: 'ownerDid'),
45
+
),
43
46
).thenAnswer((_) => Stream.value([]));
44
47
when(
45
48
() => mockRepository.fetchAndCacheFeed(
46
49
cursor: any(named: 'cursor'),
47
50
feedUri: any(named: 'feedUri'),
51
+
ownerDid: any(named: 'ownerDid'),
48
52
),
49
53
).thenAnswer((_) async {});
50
-
when(() => mockRepository.getCursor(any())).thenAnswer((_) async => null);
51
-
when(() => mockRepository.clearFeedContent(any())).thenAnswer((_) async {});
54
+
when(() => mockRepository.getCursor(any(), any())).thenAnswer((_) async => null);
55
+
when(() => mockRepository.clearFeedContent(any(), any())).thenAnswer((_) async {});
52
56
});
53
57
54
58
group('FeedContentNotifier', () {
···
61
65
await Future.delayed(Duration.zero);
62
66
63
67
verify(
64
-
() => mockRepository.watchFeedContent(feedKey: FeedContentRepository.kInternalHomeFeedKey),
68
+
() => mockRepository.watchFeedContent(
69
+
feedKey: FeedContentRepository.kInternalHomeFeedKey,
70
+
ownerDid: any(named: 'ownerDid'),
71
+
),
65
72
).called(1);
66
73
});
67
74
···
71
78
const feedUri = 'at://did:example:123/app.bsky.feed.generator/custom';
72
79
73
80
when(
74
-
() => mockRepository.watchFeedContent(feedKey: feedUri),
81
+
() => mockRepository.watchFeedContent(
82
+
feedKey: feedUri,
83
+
ownerDid: any(named: 'ownerDid'),
84
+
),
75
85
).thenAnswer((_) => Stream.value([]));
76
86
77
87
container.read(feedContentProvider(feedUri));
78
88
79
89
await Future.delayed(Duration.zero);
80
90
81
-
verify(() => mockRepository.watchFeedContent(feedKey: feedUri)).called(1);
91
+
verify(
92
+
() => mockRepository.watchFeedContent(
93
+
feedKey: feedUri,
94
+
ownerDid: any(named: 'ownerDid'),
95
+
),
96
+
).called(1);
82
97
});
83
98
84
99
test('build maps following feed to internal home key', () async {
···
91
106
await Future.delayed(Duration.zero);
92
107
93
108
verify(
94
-
() => mockRepository.watchFeedContent(feedKey: FeedContentRepository.kInternalHomeFeedKey),
109
+
() => mockRepository.watchFeedContent(
110
+
feedKey: FeedContentRepository.kInternalHomeFeedKey,
111
+
ownerDid: any(named: 'ownerDid'),
112
+
),
95
113
).called(1);
96
114
});
97
115
···
99
117
final container = createContainer();
100
118
addTearDown(container.dispose);
101
119
const feedUri = FeedRepository.kHomeFeedUri;
102
-
when(() => mockRepository.fetchAndCacheFeed(feedUri: null)).thenAnswer((_) async {});
120
+
when(
121
+
() => mockRepository.fetchAndCacheFeed(feedUri: null, ownerDid: any(named: 'ownerDid')),
122
+
).thenAnswer((_) async {});
103
123
104
124
await container.read(feedContentProvider(feedUri).notifier).refresh();
105
125
106
-
verify(() => mockRepository.fetchAndCacheFeed(feedUri: null)).called(1);
126
+
verify(
127
+
() => mockRepository.fetchAndCacheFeed(feedUri: null, ownerDid: any(named: 'ownerDid')),
128
+
).called(1);
107
129
});
108
130
109
131
test('refresh calls fetchAndCacheFeed with correct key for custom feed', () async {
···
112
134
const feedUri = 'at://did:example:123/app.bsky.feed.generator/custom';
113
135
114
136
when(
115
-
() => mockRepository.watchFeedContent(feedKey: feedUri),
137
+
() => mockRepository.watchFeedContent(
138
+
feedKey: feedUri,
139
+
ownerDid: any(named: 'ownerDid'),
140
+
),
116
141
).thenAnswer((_) => Stream.value([]));
117
-
when(() => mockRepository.fetchAndCacheFeed(feedUri: feedUri)).thenAnswer((_) async {});
142
+
when(
143
+
() => mockRepository.fetchAndCacheFeed(
144
+
feedUri: feedUri,
145
+
ownerDid: any(named: 'ownerDid'),
146
+
),
147
+
).thenAnswer((_) async {});
118
148
119
149
await container.read(feedContentProvider(feedUri).notifier).refresh();
120
150
121
-
verify(() => mockRepository.fetchAndCacheFeed(feedUri: feedUri)).called(1);
151
+
verify(
152
+
() => mockRepository.fetchAndCacheFeed(
153
+
feedUri: feedUri,
154
+
ownerDid: any(named: 'ownerDid'),
155
+
),
156
+
).called(1);
122
157
});
123
158
124
159
test('loadMore fetches next page using cursor', () async {
···
127
162
const feedUri = FeedRepository.kHomeFeedUri;
128
163
129
164
when(
130
-
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey),
165
+
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey, any()),
131
166
).thenAnswer((_) async => 'next_cursor');
132
167
when(
133
-
() => mockRepository.fetchAndCacheFeed(cursor: 'next_cursor', feedUri: null),
168
+
() => mockRepository.fetchAndCacheFeed(
169
+
cursor: 'next_cursor',
170
+
feedUri: null,
171
+
ownerDid: any(named: 'ownerDid'),
172
+
),
134
173
).thenAnswer((_) async {});
135
174
136
175
await container.read(feedContentProvider(feedUri).notifier).loadMore();
137
176
138
-
verify(() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey)).called(1);
177
+
verify(
178
+
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey, any()),
179
+
).called(1);
139
180
verify(
140
-
() => mockRepository.fetchAndCacheFeed(cursor: 'next_cursor', feedUri: null),
181
+
() => mockRepository.fetchAndCacheFeed(
182
+
cursor: 'next_cursor',
183
+
feedUri: null,
184
+
ownerDid: any(named: 'ownerDid'),
185
+
),
141
186
).called(1);
142
187
});
143
188
···
147
192
const feedUri = FeedRepository.kHomeFeedUri;
148
193
149
194
when(
150
-
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey),
195
+
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey, any()),
151
196
).thenAnswer((_) async => null);
152
197
153
198
await container.read(feedContentProvider(feedUri).notifier).loadMore();
154
199
155
-
verify(() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey)).called(1);
200
+
verify(
201
+
() => mockRepository.getCursor(FeedContentRepository.kInternalHomeFeedKey, any()),
202
+
).called(1);
156
203
verifyNever(
157
204
() => mockRepository.fetchAndCacheFeed(
158
205
cursor: any(named: 'cursor'),
159
206
feedUri: any(named: 'feedUri'),
207
+
ownerDid: any(named: 'ownerDid'),
160
208
),
161
209
);
162
210
});
···
167
215
const feedUri = FeedRepository.kHomeFeedUri;
168
216
169
217
when(
170
-
() => mockRepository.clearFeedContent(FeedContentRepository.kInternalHomeFeedKey),
218
+
() => mockRepository.clearFeedContent(FeedContentRepository.kInternalHomeFeedKey, any()),
171
219
).thenAnswer((_) async {});
172
220
173
221
await container.read(feedContentProvider(feedUri).notifier).clearFeedContent();
174
222
175
223
verify(
176
-
() => mockRepository.clearFeedContent(FeedContentRepository.kInternalHomeFeedKey),
224
+
() => mockRepository.clearFeedContent(FeedContentRepository.kInternalHomeFeedKey, any()),
177
225
).called(1);
178
226
});
179
227
···
188
236
() => mockRepository.fetchAndCacheFeed(
189
237
cursor: any(named: 'cursor'),
190
238
feedUri: any(named: 'feedUri'),
239
+
ownerDid: any(named: 'ownerDid'),
191
240
),
192
241
);
193
242
});
+19
-8
test/src/features/feeds/application/feed_providers_test.dart
+19
-8
test/src/features/feeds/application/feed_providers_test.dart
···
39
39
sortOrder: 0,
40
40
isPinned: false,
41
41
lastSynced: DateTime.now(),
42
+
ownerDid: 'did:plc:test',
42
43
),
43
44
SavedFeed(
44
45
uri: 'at://did:plc:test/app.bsky.feed.generator/feed2',
···
50
51
sortOrder: 1,
51
52
isPinned: true,
52
53
lastSynced: DateTime.now(),
54
+
ownerDid: 'did:plc:test',
53
55
),
54
56
];
55
57
56
-
when(() => mockRepository.watchAllFeeds()).thenAnswer((_) => Stream.value(feeds));
58
+
when(() => mockRepository.watchAllFeeds(any())).thenAnswer((_) => Stream.value(feeds));
57
59
58
60
final container = createContainer();
59
61
···
70
72
});
71
73
72
74
test('handles empty feed list', () async {
73
-
when(() => mockRepository.watchAllFeeds()).thenAnswer((_) => Stream.value([]));
75
+
when(() => mockRepository.watchAllFeeds(any())).thenAnswer((_) => Stream.value([]));
74
76
75
77
final container = createContainer();
76
78
···
96
98
sortOrder: 0,
97
99
isPinned: true,
98
100
lastSynced: DateTime.now(),
101
+
ownerDid: 'did:plc:test',
99
102
),
100
103
];
101
104
102
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value(pinnedFeeds));
105
+
when(
106
+
() => mockRepository.watchPinnedFeeds(any()),
107
+
).thenAnswer((_) => Stream.value(pinnedFeeds));
103
108
104
109
final container = createContainer();
105
110
···
114
119
});
115
120
116
121
test('handles no pinned feeds', () async {
117
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value([]));
122
+
when(() => mockRepository.watchPinnedFeeds(any())).thenAnswer((_) => Stream.value([]));
118
123
119
124
final container = createContainer();
120
125
···
139
144
sortOrder: 0,
140
145
isPinned: true,
141
146
lastSynced: DateTime.now(),
147
+
ownerDid: 'did:plc:test',
142
148
);
143
149
144
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value([pinnedFeed]));
150
+
when(
151
+
() => mockRepository.watchPinnedFeeds(any()),
152
+
).thenAnswer((_) => Stream.value([pinnedFeed]));
145
153
146
154
final container = createContainer(authenticated: true);
147
155
final subscription = container.listen(pinnedFeedsProvider, (previous, next) {});
···
156
164
});
157
165
158
166
test('initial state falls back to discover when authenticated with no pinned feeds', () async {
159
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value([]));
167
+
when(() => mockRepository.watchPinnedFeeds(any())).thenAnswer((_) => Stream.value([]));
160
168
161
169
final container = createContainer(authenticated: true);
162
170
···
205
213
sortOrder: 0,
206
214
isPinned: true,
207
215
lastSynced: DateTime.now(),
216
+
ownerDid: 'did:plc:test',
208
217
);
209
218
210
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value([pinnedFeed]));
219
+
when(
220
+
() => mockRepository.watchPinnedFeeds(any()),
221
+
).thenAnswer((_) => Stream.value([pinnedFeed]));
211
222
212
223
final container = createContainer(authenticated: true);
213
224
final subscription = container.listen(pinnedFeedsProvider, (previous, next) {});
···
224
235
});
225
236
226
237
test('resetToDefault falls back to discover when unauthenticated', () async {
227
-
when(() => mockRepository.watchPinnedFeeds()).thenAnswer((_) => Stream.value([]));
238
+
when(() => mockRepository.watchPinnedFeeds(any())).thenAnswer((_) => Stream.value([]));
228
239
229
240
final container = createContainer(authenticated: false);
230
241
final subscription = container.listen(pinnedFeedsProvider, (_, _) {});
+17
-17
test/src/features/feeds/application/feed_sync_controller_test.dart
+17
-17
test/src/features/feeds/application/feed_sync_controller_test.dart
···
8
8
9
9
setUp(() {
10
10
mockRepository = MockFeedRepository();
11
-
when(() => mockRepository.seedDefaultFeeds()).thenAnswer((_) async {});
12
-
when(() => mockRepository.syncOnResume()).thenAnswer((_) async {});
13
-
when(() => mockRepository.syncPreferences()).thenAnswer((_) async {});
11
+
when(() => mockRepository.seedDefaultFeeds(any())).thenAnswer((_) async {});
12
+
when(() => mockRepository.syncOnResume(any())).thenAnswer((_) async {});
13
+
when(() => mockRepository.syncPreferences(any())).thenAnswer((_) async {});
14
14
});
15
15
16
16
group('FeedRepository sync behavior', () {
17
17
test('syncOnResume calls syncPreferences', () async {
18
-
await mockRepository.syncOnResume();
18
+
await mockRepository.syncOnResume(any());
19
19
20
-
verify(() => mockRepository.syncOnResume()).called(1);
20
+
verify(() => mockRepository.syncOnResume(any())).called(1);
21
21
});
22
22
23
23
test('seedDefaultFeeds is callable', () async {
24
-
await mockRepository.seedDefaultFeeds();
24
+
await mockRepository.seedDefaultFeeds(any());
25
25
26
-
verify(() => mockRepository.seedDefaultFeeds()).called(1);
26
+
verify(() => mockRepository.seedDefaultFeeds(any())).called(1);
27
27
});
28
28
29
29
test('syncPreferences is callable', () async {
30
-
await mockRepository.syncPreferences();
30
+
await mockRepository.syncPreferences(any());
31
31
32
-
verify(() => mockRepository.syncPreferences()).called(1);
32
+
verify(() => mockRepository.syncPreferences(any())).called(1);
33
33
});
34
34
});
35
35
36
36
group('sync flow integration', () {
37
37
test('simulates login sync flow', () async {
38
-
await mockRepository.seedDefaultFeeds();
39
-
await mockRepository.syncOnResume();
38
+
await mockRepository.seedDefaultFeeds(any());
39
+
await mockRepository.syncOnResume(any());
40
40
41
-
verify(() => mockRepository.seedDefaultFeeds()).called(1);
42
-
verify(() => mockRepository.syncOnResume()).called(1);
41
+
verify(() => mockRepository.seedDefaultFeeds(any())).called(1);
42
+
verify(() => mockRepository.syncOnResume(any())).called(1);
43
43
});
44
44
45
45
test('simulates app resume flow', () async {
46
-
await mockRepository.seedDefaultFeeds();
47
-
await mockRepository.syncOnResume();
46
+
await mockRepository.seedDefaultFeeds(any());
47
+
await mockRepository.syncOnResume(any());
48
48
49
-
verify(() => mockRepository.seedDefaultFeeds()).called(1);
50
-
verify(() => mockRepository.syncOnResume()).called(1);
49
+
verify(() => mockRepository.seedDefaultFeeds(any())).called(1);
50
+
verify(() => mockRepository.syncOnResume(any())).called(1);
51
51
});
52
52
});
53
53
}
+26
-12
test/src/features/feeds/infrastructure/feed_content_repository_test.dart
+26
-12
test/src/features/feeds/infrastructure/feed_content_repository_test.dart
···
8
8
class MockFeedContentDao extends Mock implements FeedContentDao {}
9
9
10
10
void main() {
11
+
const ownerDid = 'did:web:tester';
11
12
late MockXrpcClient mockApi;
12
13
late MockFeedContentDao mockDao;
13
14
late MockLogger mockLogger;
···
35
36
when(
36
37
() => mockDao.insertFeedContentBatch(
37
38
feedKey: any(named: 'feedKey'),
39
+
ownerDid: ownerDid,
38
40
newPosts: any(named: 'newPosts'),
39
41
newProfiles: any(named: 'newProfiles'),
40
42
newRelationships: any(named: 'newRelationships'),
···
43
45
),
44
46
).thenAnswer((_) async {});
45
47
46
-
await repository.fetchAndCacheFeed();
48
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
47
49
48
50
verify(
49
51
() => mockApi.call('app.bsky.feed.getTimeline', params: any(named: 'params')),
···
56
58
newRelationships: any(named: 'newRelationships'),
57
59
newItems: any(named: 'newItems'),
58
60
newCursor: 'next_cursor',
61
+
ownerDid: ownerDid,
59
62
),
60
63
).called(1);
61
64
},
···
71
74
when(
72
75
() => mockDao.insertFeedContentBatch(
73
76
feedKey: any(named: 'feedKey'),
77
+
ownerDid: ownerDid,
74
78
newPosts: any(named: 'newPosts'),
75
79
newProfiles: any(named: 'newProfiles'),
76
80
newRelationships: any(named: 'newRelationships'),
···
79
83
),
80
84
).thenAnswer((_) async {});
81
85
82
-
await repository.fetchAndCacheFeed();
86
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
83
87
84
88
verify(
85
89
() => mockApi.call(
···
98
102
when(
99
103
() => mockDao.insertFeedContentBatch(
100
104
feedKey: any(named: 'feedKey'),
105
+
ownerDid: ownerDid,
101
106
newPosts: any(named: 'newPosts'),
102
107
newProfiles: any(named: 'newProfiles'),
103
108
newRelationships: any(named: 'newRelationships'),
···
107
112
).thenAnswer((_) async {});
108
113
109
114
const feedUri = 'at://did:example:123/app.bsky.feed.generator/custom';
110
-
await repository.fetchAndCacheFeed(feedUri: feedUri);
115
+
await repository.fetchAndCacheFeed(feedUri: feedUri, ownerDid: ownerDid);
111
116
112
117
verify(
113
118
() => mockApi.call(
···
119
124
verify(
120
125
() => mockDao.insertFeedContentBatch(
121
126
feedKey: feedUri,
127
+
ownerDid: ownerDid,
122
128
newPosts: any(named: 'newPosts'),
123
129
newProfiles: any(named: 'newProfiles'),
124
130
newRelationships: any(named: 'newRelationships'),
···
134
140
() => mockApi.call(any(), params: any(named: 'params')),
135
141
).thenThrow(Exception('Bad Request'));
136
142
137
-
expect(() => repository.fetchAndCacheFeed(), throwsException);
143
+
expect(() => repository.fetchAndCacheFeed(ownerDid: ownerDid), throwsException);
138
144
139
145
verify(() => mockLogger.error(any(), any(), any())).called(1);
140
146
});
···
148
154
when(
149
155
() => mockDao.insertFeedContentBatch(
150
156
feedKey: any(named: 'feedKey'),
157
+
ownerDid: ownerDid,
151
158
newPosts: any(named: 'newPosts'),
152
159
newProfiles: any(named: 'newProfiles'),
153
160
newRelationships: any(named: 'newRelationships'),
···
156
163
),
157
164
).thenAnswer((_) async {});
158
165
159
-
await repository.fetchAndCacheFeed();
166
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
160
167
161
168
verify(
162
169
() => mockDao.insertFeedContentBatch(
···
166
173
newRelationships: any(named: 'newRelationships'),
167
174
newItems: any(named: 'newItems'),
168
175
newCursor: any(named: 'newCursor'),
176
+
ownerDid: ownerDid,
169
177
),
170
178
).called(1);
171
179
···
192
200
);
193
201
194
202
expect(
195
-
() => repository.fetchAndCacheFeed(feedUri: '__internal:malicious'),
203
+
() => repository.fetchAndCacheFeed(feedUri: '__internal:malicious', ownerDid: ownerDid),
196
204
throwsA(
197
205
isA<ArgumentError>().having(
198
206
(e) => e.message,
···
211
219
when(
212
220
() => mockDao.insertFeedContentBatch(
213
221
feedKey: any(named: 'feedKey'),
222
+
ownerDid: ownerDid,
214
223
newPosts: any(named: 'newPosts'),
215
224
newProfiles: any(named: 'newProfiles'),
216
225
newRelationships: any(named: 'newRelationships'),
···
220
229
).thenAnswer((_) async {});
221
230
222
231
const validUri = 'at://did:plc:abc123/app.bsky.feed.generator/test';
223
-
await repository.fetchAndCacheFeed(feedUri: validUri);
232
+
await repository.fetchAndCacheFeed(feedUri: validUri, ownerDid: ownerDid);
224
233
225
234
verify(
226
235
() => mockDao.insertFeedContentBatch(
227
236
feedKey: validUri,
237
+
ownerDid: ownerDid,
228
238
newPosts: any(named: 'newPosts'),
229
239
newProfiles: any(named: 'newProfiles'),
230
240
newRelationships: any(named: 'newRelationships'),
···
267
277
when(
268
278
() => mockDao.insertFeedContentBatch(
269
279
feedKey: any(named: 'feedKey'),
280
+
ownerDid: ownerDid,
270
281
newPosts: any(named: 'newPosts'),
271
282
newProfiles: any(named: 'newProfiles'),
272
283
newRelationships: any(named: 'newRelationships'),
···
277
288
capturedPosts = invocation.namedArguments[#newPosts] as List;
278
289
});
279
290
280
-
await repository.fetchAndCacheFeed();
291
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
281
292
282
293
expect(capturedPosts, isNotNull);
283
294
expect(capturedPosts, hasLength(1));
···
312
323
when(
313
324
() => mockDao.insertFeedContentBatch(
314
325
feedKey: any(named: 'feedKey'),
326
+
ownerDid: ownerDid,
315
327
newPosts: any(named: 'newPosts'),
316
328
newProfiles: any(named: 'newProfiles'),
317
329
newRelationships: any(named: 'newRelationships'),
···
322
334
capturedPosts = invocation.namedArguments[#newPosts] as List;
323
335
});
324
336
325
-
await repository.fetchAndCacheFeed();
337
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
326
338
327
339
expect(capturedPosts, isNotNull);
328
340
final postCompanion = capturedPosts!.first;
···
373
385
when(
374
386
() => mockDao.insertFeedContentBatch(
375
387
feedKey: any(named: 'feedKey'),
388
+
ownerDid: ownerDid,
376
389
newPosts: any(named: 'newPosts'),
377
390
newProfiles: any(named: 'newProfiles'),
378
391
newRelationships: any(named: 'newRelationships'),
···
383
396
capturedItems = invocation.namedArguments[#newItems] as List;
384
397
});
385
398
386
-
await repository.fetchAndCacheFeed();
399
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
387
400
388
401
expect(capturedItems, isNotNull);
389
402
final sortKeys = capturedItems!
···
439
452
when(
440
453
() => mockDao.insertFeedContentBatch(
441
454
feedKey: any(named: 'feedKey'),
455
+
ownerDid: ownerDid,
442
456
newPosts: any(named: 'newPosts'),
443
457
newProfiles: any(named: 'newProfiles'),
444
458
newRelationships: any(named: 'newRelationships'),
···
450
464
allCapturedItems.add(items);
451
465
});
452
466
453
-
await repository.fetchAndCacheFeed();
454
-
await repository.fetchAndCacheFeed();
467
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
468
+
await repository.fetchAndCacheFeed(ownerDid: ownerDid);
455
469
456
470
final allSortKeys = allCapturedItems
457
471
.expand((items) => items)
+19
-13
test/src/features/feeds/infrastructure/feed_repository_lists_test.dart
+19
-13
test/src/features/feeds/infrastructure/feed_repository_lists_test.dart
···
12
12
late AppDatabase db;
13
13
late MockLogger mockLogger;
14
14
late FeedRepository repository;
15
+
const ownerDid = 'did:web:tester';
15
16
16
17
setUp(() {
17
18
mockApi = MockXrpcClient();
···
151
152
),
152
153
).thenAnswer((_) async => feedMetadata);
153
154
154
-
await repository.syncPreferences();
155
+
await repository.syncPreferences(ownerDid);
155
156
156
-
final feeds = await db.savedFeedsDao.getAllFeeds();
157
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
157
158
expect(feeds, hasLength(2));
158
159
159
160
final list = feeds.firstWhere((f) => f.uri == 'at://did:plc:abc/app.bsky.graph.list/mylist');
···
204
205
),
205
206
).thenAnswer((_) async => feedMetadata);
206
207
207
-
await repository.syncPreferences();
208
+
await repository.syncPreferences(ownerDid);
208
209
209
-
final feeds = await db.savedFeedsDao.getAllFeeds();
210
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
210
211
expect(feeds, hasLength(2));
211
212
212
213
final followingFeed = feeds.firstWhere((f) => f.uri == 'following');
···
232
233
() => mockApi.call('app.bsky.actor.getPreferences'),
233
234
).thenAnswer((_) async => prefsResponse);
234
235
235
-
await repository.syncPreferences();
236
+
await repository.syncPreferences(ownerDid);
236
237
237
-
final feeds = await db.savedFeedsDao.getAllFeeds();
238
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
238
239
expect(feeds, hasLength(1));
239
240
240
241
final timelineFeed = feeds.first;
···
254
255
isPinned: const Value(false),
255
256
lastSynced: DateTime.now().subtract(const Duration(days: 1)),
256
257
localUpdatedAt: const Value(null),
258
+
ownerDid: ownerDid,
257
259
),
258
260
);
259
261
···
271
273
() => mockApi.call('app.bsky.actor.getPreferences'),
272
274
).thenAnswer((_) async => prefsResponse);
273
275
274
-
await repository.syncPreferences();
276
+
await repository.syncPreferences(ownerDid);
275
277
276
-
final feeds = await db.savedFeedsDao.getAllFeeds();
278
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
277
279
expect(feeds, hasLength(1));
278
280
279
281
final followingFeed = feeds.first;
···
293
295
creatorDid: 'did:plc:abc',
294
296
sortOrder: 0,
295
297
lastSynced: staleTimestamp,
298
+
ownerDid: ownerDid,
296
299
),
297
300
);
298
301
···
315
318
),
316
319
).thenAnswer((_) async => listMetadata);
317
320
318
-
await repository.refreshStaleMetadata();
321
+
await repository.refreshStaleMetadata(ownerDid);
319
322
320
-
final feeds = await db.savedFeedsDao.getAllFeeds();
323
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
321
324
expect(feeds, hasLength(1));
322
325
323
326
final list = feeds.first;
···
337
340
creatorDid: '',
338
341
sortOrder: 0,
339
342
lastSynced: staleTimestamp,
343
+
ownerDid: ownerDid,
340
344
),
341
345
);
342
346
343
-
await repository.refreshStaleMetadata();
347
+
await repository.refreshStaleMetadata(ownerDid);
344
348
345
349
verifyNever(() => mockApi.call('app.bsky.graph.getList', params: any(named: 'params')));
346
350
verifyNever(
···
359
363
creatorDid: 'did:plc:abc',
360
364
sortOrder: 0,
361
365
lastSynced: staleTimestamp,
366
+
ownerDid: ownerDid,
362
367
),
363
368
SavedFeedsCompanion.insert(
364
369
uri: 'at://did:plc:def/app.bsky.feed.generator/test',
···
366
371
creatorDid: 'did:plc:def',
367
372
sortOrder: 1,
368
373
lastSynced: staleTimestamp,
374
+
ownerDid: ownerDid,
369
375
),
370
376
]);
371
377
···
405
411
),
406
412
).thenAnswer((_) async => feedMetadata);
407
413
408
-
await repository.refreshStaleMetadata();
414
+
await repository.refreshStaleMetadata(ownerDid);
409
415
410
-
final feeds = await db.savedFeedsDao.getAllFeeds();
416
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
411
417
expect(feeds, hasLength(2));
412
418
413
419
final list = feeds.firstWhere((f) => f.uri == 'at://did:plc:abc/app.bsky.graph.list/mylist');
+41
-32
test/src/features/feeds/infrastructure/feed_repository_management_test.dart
+41
-32
test/src/features/feeds/infrastructure/feed_repository_management_test.dart
···
11
11
late AppDatabase db;
12
12
late MockLogger mockLogger;
13
13
late FeedRepository repository;
14
+
const ownerDid = 'did:web:tester';
14
15
15
16
setUp(() {
16
17
mockApi = MockXrpcClient();
···
66
67
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
67
68
).thenAnswer((_) async => feedMetadata);
68
69
69
-
await repository.saveFeed(feedUri);
70
+
await repository.saveFeed(feedUri, ownerDid);
70
71
71
72
verify(
72
73
() => mockApi.call(
···
83
84
),
84
85
).called(1);
85
86
86
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
87
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
87
88
expect(feed, isNotNull);
88
89
expect(feed!.displayName, 'New Feed');
89
-
final queue = await db.preferenceSyncQueueDao.getPendingItems();
90
+
final queue = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
90
91
expect(queue, isEmpty);
91
92
});
92
93
···
111
112
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
112
113
).thenThrow(Exception('Network error'));
113
114
114
-
await repository.saveFeed(feedUri);
115
+
await repository.saveFeed(feedUri, ownerDid);
115
116
116
117
verify(
117
118
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
118
119
).called(1);
119
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
120
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
120
121
expect(feed, isNotNull);
121
122
expect(feed!.displayName, 'Saved Feed');
122
123
});
···
131
132
() => mockApi.call('app.bsky.actor.getPreferences'),
132
133
).thenThrow(Exception('Network error'));
133
134
134
-
await repository.saveFeed(feedUri);
135
+
await repository.saveFeed(feedUri, ownerDid);
135
136
136
-
final queue = await db.preferenceSyncQueueDao.getPendingItems();
137
+
final queue = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
137
138
expect(queue, hasLength(1));
138
139
expect(queue.first.type, 'save');
139
140
expect(queue.first.payload, feedUri);
140
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
141
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
141
142
expect(feed, isNotNull);
142
143
});
143
144
···
175
176
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
176
177
).thenAnswer((_) async => feedMetadata);
177
178
178
-
await repository.saveFeed(feedUri, pin: true);
179
+
await repository.saveFeed(feedUri, ownerDid, pin: true);
179
180
180
181
verify(
181
182
() => mockApi.call(
···
192
193
),
193
194
).called(1);
194
195
195
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
196
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
196
197
expect(feed!.isPinned, true);
197
198
});
198
199
···
230
231
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
231
232
).thenAnswer((_) async => feedMetadata);
232
233
233
-
await repository.saveFeed(feedUri);
234
+
await repository.saveFeed(feedUri, ownerDid);
234
235
235
236
verify(
236
237
() => mockApi.call(
···
284
285
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
285
286
).thenAnswer((_) async => feedMetadata);
286
287
287
-
await repository.saveFeed(feedUri);
288
+
await repository.saveFeed(feedUri, ownerDid);
288
289
289
290
final captured =
290
291
verify(
···
303
304
when(() => mockApi.isAuthenticated).thenReturn(false);
304
305
305
306
expect(
306
-
() => repository.saveFeed('at://did:plc:abc/app.bsky.feed.generator/test'),
307
+
() => repository.saveFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid),
307
308
throwsA(isA<Exception>()),
308
309
);
309
310
});
···
320
321
creatorDid: 'did:plc:abc',
321
322
sortOrder: 0,
322
323
lastSynced: DateTime.now(),
324
+
ownerDid: ownerDid,
323
325
),
324
326
);
325
327
···
341
343
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
342
344
).thenAnswer((_) async => {});
343
345
344
-
await repository.removeFeed(feedUri);
346
+
await repository.removeFeed(feedUri, ownerDid);
345
347
346
348
verify(
347
349
() => mockApi.call(
···
354
356
),
355
357
).called(1);
356
358
357
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
359
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
358
360
expect(feed, isNull);
359
361
});
360
362
···
371
373
() => mockApi.call('app.bsky.actor.getPreferences'),
372
374
).thenAnswer((_) async => currentPrefs);
373
375
374
-
await repository.removeFeed(feedUri);
376
+
await repository.removeFeed(feedUri, ownerDid);
375
377
376
378
verifyNever(() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')));
377
379
});
···
380
382
when(() => mockApi.isAuthenticated).thenReturn(false);
381
383
382
384
expect(
383
-
() => repository.removeFeed('at://did:plc:abc/app.bsky.feed.generator/test'),
385
+
() => repository.removeFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid),
384
386
throwsA(isA<Exception>()),
385
387
);
386
388
});
···
399
401
creatorDid: 'did:plc:abc',
400
402
sortOrder: 0,
401
403
lastSynced: DateTime.now(),
404
+
ownerDid: ownerDid,
402
405
),
403
406
SavedFeedsCompanion.insert(
404
407
uri: feed2,
···
406
409
creatorDid: 'did:plc:def',
407
410
sortOrder: 1,
408
411
lastSynced: DateTime.now(),
412
+
ownerDid: ownerDid,
409
413
),
410
414
SavedFeedsCompanion.insert(
411
415
uri: feed3,
···
413
417
creatorDid: 'did:plc:ghi',
414
418
sortOrder: 2,
415
419
lastSynced: DateTime.now(),
420
+
ownerDid: ownerDid,
416
421
),
417
422
]);
418
423
···
434
439
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
435
440
).thenAnswer((_) async => {});
436
441
437
-
await repository.reorderFeeds([feed3, feed1, feed2]);
442
+
await repository.reorderFeeds([feed3, feed1, feed2], ownerDid);
438
443
439
-
final feeds = await db.savedFeedsDao.getAllFeeds();
444
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
440
445
expect(feeds[0].uri, feed3);
441
446
expect(feeds[0].sortOrder, 0);
442
447
expect(feeds[1].uri, feed1);
···
456
461
creatorDid: 'did:plc:abc',
457
462
sortOrder: 0,
458
463
lastSynced: DateTime.now(),
464
+
ownerDid: ownerDid,
459
465
),
460
466
SavedFeedsCompanion.insert(
461
467
uri: feed2,
···
463
469
creatorDid: 'did:plc:def',
464
470
sortOrder: 1,
465
471
lastSynced: DateTime.now(),
472
+
ownerDid: ownerDid,
466
473
),
467
474
]);
468
475
···
484
491
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
485
492
).thenAnswer((_) async => {});
486
493
487
-
await repository.reorderFeeds([feed2, feed1]);
494
+
await repository.reorderFeeds([feed2, feed1], ownerDid);
488
495
489
496
verify(
490
497
() => mockApi.call(
···
513
520
creatorDid: 'did:plc:abc',
514
521
sortOrder: 0,
515
522
lastSynced: DateTime.now(),
523
+
ownerDid: ownerDid,
516
524
),
517
525
SavedFeedsCompanion.insert(
518
526
uri: feed2,
···
520
528
creatorDid: 'did:plc:def',
521
529
sortOrder: 1,
522
530
lastSynced: DateTime.now(),
531
+
ownerDid: ownerDid,
523
532
),
524
533
]);
525
534
···
527
536
() => mockApi.call('app.bsky.actor.getPreferences'),
528
537
).thenThrow(Exception('Network error'));
529
538
530
-
await repository.reorderFeeds([feed2, feed1]);
539
+
await repository.reorderFeeds([feed2, feed1], ownerDid);
531
540
532
-
final feeds = await db.savedFeedsDao.getAllFeeds();
541
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
533
542
expect(feeds[0].uri, feed2);
534
543
expect(feeds[1].uri, feed1);
535
544
536
-
final queue = await db.preferenceSyncQueueDao.getPendingItems();
545
+
final queue = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
537
546
expect(queue, hasLength(1));
538
547
expect(queue.first.type, 'reorder');
539
548
expect(queue.first.payload, '$feed2,$feed1');
···
543
552
when(() => mockApi.isAuthenticated).thenReturn(false);
544
553
545
554
expect(
546
-
() => repository.reorderFeeds(['at://did:plc:abc/app.bsky.feed.generator/test']),
555
+
() => repository.reorderFeeds(['at://did:plc:abc/app.bsky.feed.generator/test'], ownerDid),
547
556
throwsA(isA<Exception>()),
548
557
);
549
558
});
···
552
561
group('Feed URI Validation', () {
553
562
test('rejects empty feed URI', () {
554
563
expect(
555
-
() => repository.saveFeed(''),
564
+
() => repository.saveFeed('', ownerDid),
556
565
throwsA(
557
566
isA<ArgumentError>().having((e) => e.message, 'message', contains('cannot be empty')),
558
567
),
···
561
570
562
571
test('rejects URI without at:// scheme', () {
563
572
expect(
564
-
() => repository.saveFeed('did:plc:abc/app.bsky.feed.generator/test'),
573
+
() => repository.saveFeed('did:plc:abc/app.bsky.feed.generator/test', ownerDid),
565
574
throwsA(
566
575
isA<ArgumentError>().having(
567
576
(e) => e.message,
···
574
583
575
584
test('rejects URI with insufficient components', () {
576
585
expect(
577
-
() => repository.saveFeed('at://did:plc:abc'),
586
+
() => repository.saveFeed('at://did:plc:abc', ownerDid),
578
587
throwsA(
579
588
isA<ArgumentError>().having((e) => e.message, 'message', contains('must have format')),
580
589
),
···
583
592
584
593
test('rejects URI without valid DID', () {
585
594
expect(
586
-
() => repository.saveFeed('at://invalid/app.bsky.feed.generator/test'),
595
+
() => repository.saveFeed('at://invalid/app.bsky.feed.generator/test', ownerDid),
587
596
throwsA(
588
597
isA<ArgumentError>().having(
589
598
(e) => e.message,
···
596
605
597
606
test('rejects URI with wrong collection', () {
598
607
expect(
599
-
() => repository.saveFeed('at://did:plc:abc/app.bsky.post/test'),
608
+
() => repository.saveFeed('at://did:plc:abc/app.bsky.post/test', ownerDid),
600
609
throwsA(
601
610
isA<ArgumentError>().having(
602
611
(e) => e.message,
···
609
618
610
619
test('rejects URI with empty rkey', () {
611
620
expect(
612
-
() => repository.saveFeed('at://did:plc:abc/app.bsky.feed.generator/'),
621
+
() => repository.saveFeed('at://did:plc:abc/app.bsky.feed.generator/', ownerDid),
613
622
throwsA(
614
623
isA<ArgumentError>().having(
615
624
(e) => e.message,
···
650
659
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': validUri}),
651
660
).thenAnswer((_) async => feedMetadata);
652
661
653
-
await repository.saveFeed(validUri);
662
+
await repository.saveFeed(validUri, ownerDid);
654
663
655
-
final feed = await db.savedFeedsDao.getFeed(validUri);
664
+
final feed = await db.savedFeedsDao.getFeed(validUri, ownerDid);
656
665
expect(feed, isNotNull);
657
666
expect(feed!.displayName, 'Valid Feed');
658
667
});
+42
-26
test/src/features/feeds/infrastructure/feed_repository_query_test.dart
+42
-26
test/src/features/feeds/infrastructure/feed_repository_query_test.dart
···
12
12
late AppDatabase db;
13
13
late MockLogger mockLogger;
14
14
late FeedRepository repository;
15
+
const ownerDid = 'did:web:tester';
15
16
16
17
setUp(() {
17
18
mockApi = MockXrpcClient();
···
133
134
creatorDid: 'did:plc:abc',
134
135
sortOrder: 0,
135
136
lastSynced: oldDate,
137
+
ownerDid: ownerDid,
136
138
),
137
139
);
138
140
···
153
155
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
154
156
).thenAnswer((_) async => updatedMetadata);
155
157
156
-
await repository.refreshStaleMetadata();
158
+
await repository.refreshStaleMetadata(ownerDid);
157
159
158
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
160
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
159
161
expect(feed!.displayName, 'Updated Name');
160
162
expect(feed.likeCount, 200);
161
163
});
···
170
172
creatorDid: 'did:plc:abc',
171
173
sortOrder: 0,
172
174
lastSynced: recentDate,
175
+
ownerDid: ownerDid,
173
176
),
174
177
);
175
178
176
-
await repository.refreshStaleMetadata();
179
+
await repository.refreshStaleMetadata(ownerDid);
177
180
178
181
verifyNever(
179
182
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: any(named: 'params')),
···
190
193
creatorDid: 'did:plc:abc',
191
194
sortOrder: 0,
192
195
lastSynced: DateTime.now(),
196
+
ownerDid: ownerDid,
193
197
),
194
198
);
195
199
196
-
final stream = repository.watchAllFeeds();
200
+
final stream = repository.watchAllFeeds(ownerDid);
197
201
expect(stream, emits(hasLength(1)));
198
202
});
199
203
});
···
208
212
sortOrder: 0,
209
213
isPinned: const Value(true),
210
214
lastSynced: DateTime.now(),
215
+
ownerDid: ownerDid,
211
216
),
212
217
SavedFeedsCompanion.insert(
213
218
uri: 'at://did:plc:def/app.bsky.feed.generator/unpinned',
···
216
221
sortOrder: 1,
217
222
isPinned: const Value(false),
218
223
lastSynced: DateTime.now(),
224
+
ownerDid: ownerDid,
219
225
),
220
226
]);
221
227
222
-
final stream = repository.watchPinnedFeeds();
228
+
final stream = repository.watchPinnedFeeds(ownerDid);
223
229
final feeds = await stream.first;
224
230
expect(feeds, hasLength(1));
225
231
expect(feeds[0].displayName, 'Pinned Feed');
···
237
243
creatorDid: 'did:plc:abc',
238
244
sortOrder: 0,
239
245
lastSynced: DateTime.now(),
246
+
ownerDid: ownerDid,
240
247
),
241
248
);
242
249
243
-
final feed = await repository.getFeed(feedUri);
250
+
final feed = await repository.getFeed(feedUri, ownerDid);
244
251
expect(feed, isNotNull);
245
252
expect(feed!.displayName, 'Test Feed');
246
253
});
···
258
265
sortOrder: 0,
259
266
isPinned: const Value(true),
260
267
lastSynced: DateTime.now(),
268
+
ownerDid: ownerDid,
261
269
),
262
270
SavedFeedsCompanion.insert(
263
271
uri: FeedRepository.kForYouFeedUri,
···
266
274
sortOrder: 1,
267
275
isPinned: const Value(true),
268
276
lastSynced: DateTime.now(),
277
+
ownerDid: ownerDid,
269
278
),
270
279
SavedFeedsCompanion.insert(
271
280
uri: FeedRepository.kDiscoverFeedUri,
···
274
283
sortOrder: 2,
275
284
isPinned: const Value(true),
276
285
lastSynced: DateTime.now(),
286
+
ownerDid: ownerDid,
277
287
),
278
288
]);
279
289
280
-
await repository.seedDefaultFeeds();
290
+
await repository.seedDefaultFeeds(ownerDid);
281
291
282
-
final feeds = await db.savedFeedsDao.getAllFeeds();
292
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
283
293
expect(feeds, isEmpty);
284
294
});
285
295
···
306
316
),
307
317
).thenAnswer((_) async => mockMetadata);
308
318
309
-
await repository.seedDefaultFeeds();
319
+
await repository.seedDefaultFeeds(ownerDid);
310
320
311
-
final feeds = await db.savedFeedsDao.getAllFeeds();
321
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
312
322
expect(feeds, hasLength(1));
313
323
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
314
324
expect(feeds[0].displayName, 'What\'s Hot');
···
325
335
),
326
336
).thenThrow(Exception('Network error'));
327
337
328
-
await repository.seedDefaultFeeds();
338
+
await repository.seedDefaultFeeds(ownerDid);
329
339
330
-
final feeds = await db.savedFeedsDao.getAllFeeds();
340
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
331
341
expect(feeds, hasLength(1));
332
342
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
333
343
expect(feeds[0].displayName, 'Discover');
···
346
356
sortOrder: 0,
347
357
isPinned: const Value(true),
348
358
lastSynced: DateTime.now(),
359
+
ownerDid: ownerDid,
349
360
),
350
361
);
351
362
352
-
await repository.seedDefaultFeeds();
363
+
await repository.seedDefaultFeeds(ownerDid);
353
364
354
-
final feeds = await db.savedFeedsDao.getAllFeeds();
365
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
355
366
expect(feeds, hasLength(1));
356
367
expect(feeds[0].displayName, 'Existing Discover');
357
368
verifyNever(
···
372
383
creatorDid: 'did:plc:test',
373
384
sortOrder: 0,
374
385
lastSynced: DateTime.now(),
386
+
ownerDid: ownerDid,
375
387
),
376
388
);
377
389
···
395
407
),
396
408
).thenAnswer((_) async => mockMetadata);
397
409
398
-
await repository.seedDefaultFeeds();
410
+
await repository.seedDefaultFeeds(ownerDid);
399
411
400
-
final deprecatedFeed = await db.savedFeedsDao.getFeed(deprecatedUri);
412
+
final deprecatedFeed = await db.savedFeedsDao.getFeed(deprecatedUri, ownerDid);
401
413
expect(deprecatedFeed, isNull);
402
414
403
-
final feeds = await db.savedFeedsDao.getAllFeeds();
415
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
404
416
expect(feeds, hasLength(1));
405
417
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
406
418
});
···
419
431
sortOrder: 3,
420
432
isPinned: const Value(true),
421
433
lastSynced: DateTime.now(),
434
+
ownerDid: ownerDid,
422
435
),
423
436
);
424
437
···
442
455
),
443
456
).thenAnswer((_) async => mockMetadata);
444
457
445
-
await repository.seedDefaultFeeds();
458
+
await repository.seedDefaultFeeds(ownerDid);
446
459
447
-
final feeds = await db.savedFeedsDao.getAllFeeds();
460
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
448
461
expect(feeds, hasLength(1));
449
462
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
450
463
expect(feeds[0].isPinned, true, reason: 'Pin status should be preserved');
···
465
478
sortOrder: 5,
466
479
isPinned: const Value(false),
467
480
lastSynced: DateTime.now(),
481
+
ownerDid: ownerDid,
468
482
),
469
483
);
470
484
···
488
502
),
489
503
).thenAnswer((_) async => mockMetadata);
490
504
491
-
await repository.seedDefaultFeeds();
505
+
await repository.seedDefaultFeeds(ownerDid);
492
506
493
-
final feeds = await db.savedFeedsDao.getAllFeeds();
507
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
494
508
expect(feeds, hasLength(1));
495
509
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
496
510
expect(feeds[0].isPinned, false, reason: 'Unpinned status should be preserved');
···
511
525
sortOrder: 5,
512
526
isPinned: const Value(true),
513
527
lastSynced: DateTime.now(),
528
+
ownerDid: ownerDid,
514
529
),
515
530
);
516
531
···
522
537
sortOrder: 0,
523
538
isPinned: const Value(false),
524
539
lastSynced: DateTime.now(),
540
+
ownerDid: ownerDid,
525
541
),
526
542
);
527
543
528
-
await repository.seedDefaultFeeds();
544
+
await repository.seedDefaultFeeds(ownerDid);
529
545
530
-
final deprecatedFeed = await db.savedFeedsDao.getFeed(deprecatedUri);
546
+
final deprecatedFeed = await db.savedFeedsDao.getFeed(deprecatedUri, ownerDid);
531
547
expect(deprecatedFeed, isNull);
532
548
533
-
final feeds = await db.savedFeedsDao.getAllFeeds();
549
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
534
550
expect(feeds, hasLength(1));
535
551
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
536
552
expect(feeds[0].displayName, 'Existing Discover');
···
561
577
),
562
578
).thenAnswer((_) async => mockMetadata);
563
579
564
-
await repository.seedDefaultFeeds();
580
+
await repository.seedDefaultFeeds(ownerDid);
565
581
566
-
final feeds = await db.savedFeedsDao.getAllFeeds();
582
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
567
583
expect(feeds, hasLength(1));
568
584
expect(feeds[0].uri, FeedRepository.kDiscoverFeedUri);
569
585
expect(feeds[0].isPinned, true);
+62
-47
test/src/features/feeds/infrastructure/feed_repository_sync_test.dart
+62
-47
test/src/features/feeds/infrastructure/feed_repository_sync_test.dart
···
22
22
late AppDatabase db;
23
23
late MockLogger mockLogger;
24
24
late FeedRepository repository;
25
+
const ownerDid = 'did:web:tester';
25
26
26
27
setUp(() {
27
28
mockApi = MockXrpcClient();
···
101
102
),
102
103
).thenAnswer((_) async => feed2Metadata);
103
104
104
-
await repository.syncPreferences();
105
+
await repository.syncPreferences(ownerDid);
105
106
106
-
final feeds = await db.savedFeedsDao.getAllFeeds();
107
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
107
108
expect(feeds, hasLength(2));
108
109
expect(feeds[0].uri, 'at://did:plc:abc/app.bsky.feed.generator/test1');
109
110
expect(feeds[0].displayName, 'Test Feed 1');
···
126
127
() => mockApi.call('app.bsky.actor.getPreferences'),
127
128
).thenAnswer((_) async => prefsResponse);
128
129
129
-
await repository.syncPreferences();
130
+
await repository.syncPreferences(ownerDid);
130
131
131
-
final feeds = await db.savedFeedsDao.getAllFeeds();
132
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
132
133
expect(feeds, isEmpty);
133
134
});
134
135
···
143
144
() => mockApi.call('app.bsky.actor.getPreferences'),
144
145
).thenAnswer((_) async => prefsResponse);
145
146
146
-
await repository.syncPreferences();
147
+
await repository.syncPreferences(ownerDid);
147
148
148
-
final feeds = await db.savedFeedsDao.getAllFeeds();
149
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
149
150
expect(feeds, isEmpty);
150
151
});
151
152
152
153
test('skips sync for unauthenticated user', () async {
153
154
when(() => mockApi.isAuthenticated).thenReturn(false);
154
155
155
-
await repository.syncPreferences();
156
+
await repository.syncPreferences(ownerDid);
156
157
157
158
verifyNever(() => mockApi.call('app.bsky.actor.getPreferences'));
158
159
});
···
202
203
),
203
204
).thenAnswer((_) async => feed2Metadata);
204
205
205
-
await repository.syncPreferences();
206
+
await repository.syncPreferences(ownerDid);
206
207
207
-
final feeds = await db.savedFeedsDao.getAllFeeds();
208
+
final feeds = await db.savedFeedsDao.getAllFeeds(ownerDid);
208
209
expect(feeds, hasLength(1));
209
210
expect(feeds[0].displayName, 'Test Feed 2');
210
211
});
···
235
236
() => mockApi.call('app.bsky.actor.getPreferences'),
236
237
).thenThrow(Exception('Network error'));
237
238
238
-
await repository.saveFeed(feedUri);
239
+
await repository.saveFeed(feedUri, ownerDid);
239
240
240
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
241
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
241
242
expect(feed, isNotNull, reason: 'Local feed should be saved');
242
243
243
-
final queueItems = await db.preferenceSyncQueueDao.getPendingItems();
244
+
final queueItems = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
244
245
expect(queueItems, hasLength(1), reason: 'Should have queued sync operation');
245
246
expect(queueItems[0].payload, feedUri);
246
247
expect(queueItems[0].type, 'save');
···
276
277
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
277
278
).thenAnswer((_) async => {});
278
279
279
-
await repository.saveFeed(feedUri);
280
+
await repository.saveFeed(feedUri, ownerDid);
280
281
281
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
282
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
282
283
expect(feed, isNotNull, reason: 'Local feed should be saved');
283
284
284
-
final queueItems = await db.preferenceSyncQueueDao.getPendingItems();
285
+
final queueItems = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
285
286
expect(queueItems, isEmpty, reason: 'Queue should be empty after successful sync');
286
287
});
287
288
···
295
296
creatorDid: 'did:plc:remove',
296
297
sortOrder: 0,
297
298
lastSynced: DateTime.now(),
299
+
ownerDid: ownerDid,
298
300
),
299
301
);
300
302
···
302
304
() => mockApi.call('app.bsky.actor.getPreferences'),
303
305
).thenThrow(Exception('Network error'));
304
306
305
-
await repository.removeFeed(feedUri);
307
+
await repository.removeFeed(feedUri, ownerDid);
306
308
307
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
309
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
308
310
expect(feed, isNull, reason: 'Local feed should be deleted');
309
311
310
-
final queueItems = await db.preferenceSyncQueueDao.getPendingItems();
312
+
final queueItems = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
311
313
expect(queueItems, hasLength(1), reason: 'Should have queued sync operation');
312
314
expect(queueItems[0].payload, feedUri);
313
315
expect(queueItems[0].type, 'remove');
···
323
325
creatorDid: 'did:plc:remove',
324
326
sortOrder: 0,
325
327
lastSynced: DateTime.now(),
328
+
ownerDid: ownerDid,
326
329
),
327
330
);
328
331
···
344
347
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
345
348
).thenAnswer((_) async => {});
346
349
347
-
await repository.removeFeed(feedUri);
350
+
await repository.removeFeed(feedUri, ownerDid);
348
351
349
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
352
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
350
353
expect(feed, isNull, reason: 'Local feed should be deleted');
351
354
352
-
final queueItems = await db.preferenceSyncQueueDao.getPendingItems();
355
+
final queueItems = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
353
356
expect(queueItems, isEmpty, reason: 'Queue should be empty after successful sync');
354
357
});
355
358
···
376
379
'likeCount': 1,
377
380
},
378
381
};
379
-
380
382
when(
381
383
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': feedUri}),
382
384
).thenAnswer((_) async => feedMetadata);
383
385
384
-
expect(() => failingRepo.saveFeed(feedUri), throwsA(isA<Exception>()));
386
+
expect(() => failingRepo.saveFeed(feedUri, ownerDid), throwsA(isA<Exception>()));
385
387
386
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
388
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
387
389
expect(feed, isNull, reason: 'Local insert should be rolled back');
388
390
389
-
final queueItems = await db.preferenceSyncQueueDao.getPendingItems();
391
+
final queueItems = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
390
392
expect(queueItems, isEmpty, reason: 'Queue should remain empty on failure');
391
393
});
392
394
});
···
402
404
type: 'save',
403
405
payload: feedUri,
404
406
createdAt: now,
407
+
ownerDid: ownerDid,
405
408
),
406
409
);
407
410
···
412
415
creatorDid: 'did:plc:test',
413
416
sortOrder: 0,
414
417
lastSynced: now,
418
+
ownerDid: ownerDid,
415
419
),
416
420
);
417
421
···
419
423
() => mockApi.call('app.bsky.actor.getPreferences'),
420
424
).thenThrow(Exception('Network error'));
421
425
422
-
await repository.processSyncQueue();
426
+
await repository.processSyncQueue(ownerDid);
423
427
424
-
final items = await db.preferenceSyncQueueDao.getPendingItems();
428
+
final items = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
425
429
expect(items.length, 1);
426
430
expect(items.first.retryCount, 1, reason: 'Retry count should be incremented');
427
431
});
···
439
443
payload: feedUri,
440
444
createdAt: now,
441
445
retryCount: const Value(5),
446
+
ownerDid: ownerDid,
442
447
),
443
448
);
444
449
445
-
await repository.processSyncQueue();
450
+
await repository.processSyncQueue(ownerDid);
446
451
447
452
verifyNever(() => mockApi.call('app.bsky.actor.getPreferences'));
448
453
449
-
final items = await db.preferenceSyncQueueDao.getPendingItems();
454
+
final items = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
450
455
expect(items.length, 1);
451
456
expect(items.first.retryCount, 5, reason: 'Retry count should not change');
452
457
});
···
463
468
payload: 'at://did:plc:test/app.bsky.feed.generator/old-failed',
464
469
createdAt: now.subtract(const Duration(days: 45)),
465
470
retryCount: const Value(5),
471
+
ownerDid: ownerDid,
466
472
),
467
473
);
468
474
···
472
478
type: 'save',
473
479
payload: 'at://did:plc:test/app.bsky.feed.generator/recent',
474
480
createdAt: now,
481
+
ownerDid: ownerDid,
475
482
),
476
483
);
477
484
···
479
486
() => mockApi.call('app.bsky.actor.getPreferences'),
480
487
).thenThrow(Exception('Network error'));
481
488
482
-
await repository.syncOnResume();
489
+
await repository.syncOnResume(ownerDid);
483
490
484
-
final items = await db.preferenceSyncQueueDao.getPendingItems();
491
+
final items = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
485
492
expect(items.length, 1);
486
493
expect(items.first.payload, 'at://did:plc:test/app.bsky.feed.generator/recent');
487
494
});
···
495
502
type: 'save',
496
503
payload: 'at://did:plc:ok/app.bsky.feed.generator/retryable',
497
504
createdAt: now,
505
+
ownerDid: ownerDid,
498
506
),
499
507
);
500
508
···
505
513
creatorDid: 'did:plc:ok',
506
514
sortOrder: 0,
507
515
lastSynced: now,
516
+
ownerDid: ownerDid,
508
517
),
509
518
);
510
519
···
517
526
payload: 'at://did:plc:fail/app.bsky.feed.generator/maxed',
518
527
createdAt: now,
519
528
retryCount: const Value(5),
529
+
ownerDid: ownerDid,
520
530
),
521
531
);
522
532
···
528
538
() => mockApi.call('app.bsky.actor.putPreferences', body: any(named: 'body')),
529
539
).thenAnswer((_) async => {});
530
540
531
-
await repository.processSyncQueue();
541
+
await repository.processSyncQueue(ownerDid);
532
542
533
-
final items = await db.preferenceSyncQueueDao.getPendingItems();
543
+
final items = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
534
544
expect(items.length, 1, reason: 'Only the maxed-out item should remain');
535
545
expect(items.first.payload, 'at://did:plc:fail/app.bsky.feed.generator/maxed');
536
546
});
···
550
560
isPinned: const Value(true),
551
561
lastSynced: baseTime,
552
562
localUpdatedAt: Value(baseTime.add(const Duration(hours: 1))),
563
+
ownerDid: ownerDid,
553
564
),
554
565
);
555
566
···
567
578
() => mockApi.call('app.bsky.actor.getPreferences'),
568
579
).thenAnswer((_) async => remotePrefs);
569
580
570
-
await repository.syncPreferences();
581
+
await repository.syncPreferences(ownerDid);
571
582
572
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
583
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
573
584
expect(feed, isNotNull);
574
585
expect(feed!.isPinned, true, reason: 'Local pin status should win');
575
586
expect(feed.displayName, 'Local Name', reason: 'Local display name should be preserved');
576
-
final queue = await db.preferenceSyncQueueDao.getPendingItems();
587
+
final queue = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
577
588
expect(queue.any((q) => q.payload == feedUri && q.type == 'save'), true);
578
589
});
579
590
···
589
600
sortOrder: 5,
590
601
isPinned: const Value(false),
591
602
lastSynced: baseTime,
603
+
ownerDid: ownerDid,
592
604
),
593
605
);
594
606
···
606
618
() => mockApi.call('app.bsky.actor.getPreferences'),
607
619
).thenAnswer((_) async => remotePrefs);
608
620
609
-
await repository.syncPreferences();
621
+
await repository.syncPreferences(ownerDid);
610
622
611
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
623
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
612
624
expect(feed, isNotNull);
613
625
expect(feed!.isPinned, true, reason: 'Remote pin status should win');
614
626
expect(feed.sortOrder, 0, reason: 'Should use remote sort order');
···
648
660
() => mockApi.call('app.bsky.feed.getFeedGenerator', params: {'feed': newFeedUri}),
649
661
).thenAnswer((_) async => feedMetadata);
650
662
651
-
await repository.syncPreferences();
663
+
await repository.syncPreferences(ownerDid);
652
664
653
-
final feed = await db.savedFeedsDao.getFeed(newFeedUri);
665
+
final feed = await db.savedFeedsDao.getFeed(newFeedUri, ownerDid);
654
666
expect(feed, isNotNull);
655
667
expect(feed!.displayName, 'New Remote Feed');
656
668
expect(feed.isPinned, true);
···
672
684
creatorDid: 'did:plc:test',
673
685
sortOrder: 0,
674
686
lastSynced: baseTime,
687
+
ownerDid: ownerDid,
675
688
),
676
689
);
677
690
···
685
698
() => mockApi.call('app.bsky.actor.getPreferences'),
686
699
).thenAnswer((_) async => remotePrefs);
687
700
688
-
await repository.syncPreferences();
701
+
await repository.syncPreferences(ownerDid);
689
702
690
-
final feed = await db.savedFeedsDao.getFeed(deletedFeedUri);
703
+
final feed = await db.savedFeedsDao.getFeed(deletedFeedUri, ownerDid);
691
704
expect(feed, isNull, reason: 'Remotely deleted feed should be removed locally');
692
705
});
693
706
···
704
717
isPinned: const Value(true),
705
718
lastSynced: baseTime,
706
719
localUpdatedAt: Value(baseTime.add(const Duration(hours: 1))),
720
+
ownerDid: ownerDid,
707
721
),
708
722
);
709
723
···
717
731
() => mockApi.call('app.bsky.actor.getPreferences'),
718
732
).thenAnswer((_) async => remotePrefs);
719
733
720
-
await repository.syncPreferences();
734
+
await repository.syncPreferences(ownerDid);
721
735
722
-
final feed = await db.savedFeedsDao.getFeed(localOnlyFeedUri);
736
+
final feed = await db.savedFeedsDao.getFeed(localOnlyFeedUri, ownerDid);
723
737
expect(feed, isNotNull, reason: 'Local-only feed with modifications should be kept');
724
738
725
-
final queue = await db.preferenceSyncQueueDao.getPendingItems();
739
+
final queue = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
726
740
expect(
727
741
queue.any((q) => q.payload == localOnlyFeedUri && q.type == 'save'),
728
742
true,
···
743
757
isPinned: const Value(true),
744
758
lastSynced: baseTime,
745
759
localUpdatedAt: Value(baseTime.add(const Duration(hours: 1))),
760
+
ownerDid: ownerDid,
746
761
),
747
762
);
748
763
···
760
775
() => mockApi.call('app.bsky.actor.getPreferences'),
761
776
).thenAnswer((_) async => remotePrefs);
762
777
763
-
await repository.syncPreferences();
778
+
await repository.syncPreferences(ownerDid);
764
779
765
-
final feed = await db.savedFeedsDao.getFeed(feedUri);
780
+
final feed = await db.savedFeedsDao.getFeed(feedUri, ownerDid);
766
781
expect(feed, isNotNull);
767
782
expect(feed!.isPinned, true, reason: 'Local pin status should be preserved (newer)');
768
783
});
+13
-5
test/src/features/feeds/presentation/screens/feed_discovery_screen_test.dart
+13
-5
test/src/features/feeds/presentation/screens/feed_discovery_screen_test.dart
···
43
43
),
44
44
).thenAnswer((_) async => kTrendingFeeds);
45
45
46
-
when(() => mockRepository.saveFeed(any(), pin: any(named: 'pin'))).thenAnswer((_) async => {});
47
-
when(() => mockRepository.watchAllFeeds()).thenAnswer((_) async* {
46
+
when(
47
+
() => mockRepository.saveFeed(any(), any(), pin: any(named: 'pin')),
48
+
).thenAnswer((_) async => {});
49
+
when(() => mockRepository.watchAllFeeds(any())).thenAnswer((_) async* {
48
50
yield [];
49
51
});
50
52
51
53
final mockContentRepository = MockFeedContentRepository();
52
54
when(
53
-
() => mockContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
55
+
() => mockContentRepository.watchFeedContent(
56
+
feedKey: any(named: 'feedKey'),
57
+
ownerDid: any(named: 'ownerDid'),
58
+
),
54
59
).thenAnswer((_) => Stream.value([]));
55
60
when(
56
-
() => mockContentRepository.fetchAndCacheFeed(feedUri: any(named: 'feedUri')),
61
+
() => mockContentRepository.fetchAndCacheFeed(
62
+
feedUri: any(named: 'feedUri'),
63
+
ownerDid: any(named: 'ownerDid'),
64
+
),
57
65
).thenAnswer((_) async {});
58
-
when(() => mockRepository.watchAllFeeds()).thenAnswer((_) => Stream.value([]));
66
+
when(() => mockRepository.watchAllFeeds(any())).thenAnswer((_) => Stream.value([]));
59
67
60
68
await tester.pumpWidget(
61
69
ProviderScope(
+11
-8
test/src/features/feeds/presentation/screens/feed_management_screen_test.dart
+11
-8
test/src/features/feeds/presentation/screens/feed_management_screen_test.dart
···
48
48
),
49
49
];
50
50
51
-
when(() => mockRepository.saveFeed(any(), pin: any(named: 'pin'))).thenAnswer((_) async => {});
52
-
53
-
when(() => mockRepository.removeFeed(any())).thenAnswer((_) async => {});
51
+
when(
52
+
() => mockRepository.saveFeed(any(), any(), pin: any(named: 'pin')),
53
+
).thenAnswer((_) async => {});
54
+
when(() => mockRepository.removeFeed(any(), any())).thenAnswer((_) async => {});
54
55
55
56
await tester.pumpWidget(
56
57
ProviderScope(
···
75
76
expect(find.text('Saved'), findsOneWidget);
76
77
77
78
await tester.tap(find.widgetWithIcon(IconButton, Icons.push_pin).first);
78
-
verify(() => mockRepository.saveFeed('at://did:1/feed/saved1', pin: false)).called(1);
79
+
verify(() => mockRepository.saveFeed('at://did:1/feed/saved1', any(), pin: false)).called(1);
79
80
80
81
final deleteButtons = find.widgetWithIcon(IconButton, Icons.delete_outline);
81
82
await tester.tap(deleteButtons.last);
82
83
83
-
verify(() => mockRepository.removeFeed('at://did:1/feed/saved2')).called(1);
84
+
verify(() => mockRepository.removeFeed('at://did:1/feed/saved2', any())).called(1);
84
85
});
85
86
86
87
testWidgets('FeedManagementScreen renders drag handles for reordering', (tester) async {
···
108
109
),
109
110
];
110
111
111
-
when(() => mockRepository.saveFeed(any(), pin: any(named: 'pin'))).thenAnswer((_) async => {});
112
-
when(() => mockRepository.removeFeed(any())).thenAnswer((_) async => {});
113
-
when(() => mockRepository.reorderFeeds(any())).thenAnswer((_) async => {});
112
+
when(
113
+
() => mockRepository.saveFeed(any(), any(), pin: any(named: 'pin')),
114
+
).thenAnswer((_) async => {});
115
+
when(() => mockRepository.removeFeed(any(), any())).thenAnswer((_) async => {});
116
+
when(() => mockRepository.reorderFeeds(any(), any())).thenAnswer((_) async => {});
114
117
115
118
await tester.pumpWidget(
116
119
ProviderScope(
+17
-6
test/src/features/feeds/presentation/screens/feed_screen_test.dart
+17
-6
test/src/features/feeds/presentation/screens/feed_screen_test.dart
···
12
12
import '../../../../../helpers/mocks.dart';
13
13
14
14
void main() {
15
+
const ownerDid = 'did:web:tester';
15
16
late MockFeedContentRepository mockContentRepository;
16
17
17
18
setUp(() {
18
19
mockContentRepository = MockFeedContentRepository();
19
20
20
-
when(() => mockContentRepository.cleanupCache()).thenAnswer((_) async {});
21
-
when(() => mockContentRepository.getCursor(any())).thenAnswer((_) async => null);
21
+
when(() => mockContentRepository.cleanupCache(ownerDid)).thenAnswer((_) async {});
22
+
when(() => mockContentRepository.getCursor(any(), ownerDid)).thenAnswer((_) async => null);
22
23
when(
23
-
() => mockContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
24
+
() => mockContentRepository.watchFeedContent(
25
+
feedKey: any(named: 'feedKey'),
26
+
ownerDid: ownerDid,
27
+
),
24
28
).thenAnswer((_) => Stream.value([]));
25
29
when(
26
-
() => mockContentRepository.fetchAndCacheFeed(feedUri: any(named: 'feedUri')),
30
+
() => mockContentRepository.fetchAndCacheFeed(
31
+
feedUri: any(named: 'feedUri'),
32
+
ownerDid: ownerDid,
33
+
),
27
34
).thenAnswer((_) async {});
28
35
});
29
36
···
45
52
46
53
await tester.pump();
47
54
await tester.pump(Duration.zero);
48
-
verify(() => mockContentRepository.fetchAndCacheFeed(feedUri: activeFeed)).called(1);
55
+
verify(
56
+
() => mockContentRepository.fetchAndCacheFeed(feedUri: activeFeed, ownerDid: ownerDid),
57
+
).called(1);
49
58
});
50
59
51
60
testWidgets('FeedScreen refresh triggers fetch', (tester) async {
···
69
78
70
79
await tester.drag(find.byType(CustomScrollView), const Offset(0, 300));
71
80
await tester.pumpAndSettle();
72
-
verify(() => mockContentRepository.fetchAndCacheFeed(feedUri: activeFeed)).called(2);
81
+
verify(
82
+
() => mockContentRepository.fetchAndCacheFeed(feedUri: activeFeed, ownerDid: ownerDid),
83
+
).called(2);
73
84
});
74
85
}
+15
-8
test/src/features/feeds/presentation/widgets/feed_preview_modal_test.dart
+15
-8
test/src/features/feeds/presentation/widgets/feed_preview_modal_test.dart
···
15
15
import '../../../../../helpers/mocks.dart';
16
16
17
17
void main() {
18
+
const ownerDid = 'did:web:tester';
18
19
late MockFeedRepository mockFeedRepository;
19
20
late MockFeedContentRepository mockFeedContentRepository;
20
21
···
51
52
];
52
53
53
54
when(
54
-
() => mockFeedContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
55
+
() => mockFeedContentRepository.watchFeedContent(
56
+
feedKey: any(named: 'feedKey'),
57
+
ownerDid: ownerDid,
58
+
),
55
59
).thenAnswer((_) => Stream.value(posts));
56
60
when(
57
-
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: feedUri),
61
+
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: feedUri, ownerDid: ownerDid),
58
62
).thenAnswer((_) async {});
59
-
when(() => mockFeedRepository.watchAllFeeds()).thenAnswer((_) => Stream.value([]));
63
+
when(() => mockFeedRepository.watchAllFeeds(ownerDid)).thenAnswer((_) => Stream.value([]));
60
64
61
65
await tester.pumpWidget(
62
66
ProviderScope(
···
93
97
const feedUri = 'at://did:1/feed/test';
94
98
95
99
when(
96
-
() => mockFeedContentRepository.watchFeedContent(feedKey: any(named: 'feedKey')),
100
+
() => mockFeedContentRepository.watchFeedContent(
101
+
feedKey: any(named: 'feedKey'),
102
+
ownerDid: ownerDid,
103
+
),
97
104
).thenAnswer((_) => Stream.value([]));
98
105
when(
99
-
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: feedUri),
106
+
() => mockFeedContentRepository.fetchAndCacheFeed(feedUri: feedUri, ownerDid: ownerDid),
100
107
).thenAnswer((_) async {});
101
-
when(() => mockFeedRepository.watchAllFeeds()).thenAnswer((_) => Stream.value([]));
108
+
when(() => mockFeedRepository.watchAllFeeds(ownerDid)).thenAnswer((_) => Stream.value([]));
102
109
when(
103
-
() => mockFeedRepository.saveFeed(any(), pin: any(named: 'pin')),
110
+
() => mockFeedRepository.saveFeed(any(), ownerDid, pin: any(named: 'pin')),
104
111
).thenAnswer((_) async {});
105
112
106
113
await tester.pumpWidget(
···
123
130
await tester.tap(find.text('Save'));
124
131
await tester.pump();
125
132
126
-
verify(() => mockFeedRepository.saveFeed(feedUri, pin: false)).called(1);
133
+
verify(() => mockFeedRepository.saveFeed(feedUri, ownerDid, pin: false)).called(1);
127
134
});
128
135
}
+48
-46
test/src/features/notifications/application/mark_as_seen_service_test.dart
+48
-46
test/src/features/notifications/application/mark_as_seen_service_test.dart
···
11
11
class MockNotificationsSyncQueueDao extends Mock implements NotificationsSyncQueueDao {}
12
12
13
13
void main() {
14
+
const ownerDid = 'did:web:tester';
14
15
late MockNotificationsRepository mockRepository;
15
16
late MockNotificationsSyncQueueDao mockSyncQueue;
16
17
late MockLogger mockLogger;
···
40
41
final timestamp2 = DateTime.parse('2026-01-07T12:01:00.000Z');
41
42
final timestamp3 = DateTime.parse('2026-01-07T12:02:00.000Z');
42
43
43
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
44
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
44
45
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
45
46
46
-
service.markAsSeen(timestamp1);
47
-
service.markAsSeen(timestamp2);
48
-
service.markAsSeen(timestamp3);
47
+
service.markAsSeen(timestamp1, ownerDid);
48
+
service.markAsSeen(timestamp2, ownerDid);
49
+
service.markAsSeen(timestamp3, ownerDid);
49
50
50
51
await Future.delayed(const Duration(seconds: 3));
51
52
52
-
verify(() => mockRepository.markAsSeenLocally(timestamp3)).called(1);
53
+
verify(() => mockRepository.markAsSeenLocally(timestamp3, ownerDid)).called(1);
53
54
verify(() => mockRepository.updateSeen(timestamp3)).called(1);
54
55
});
55
56
···
58
59
final later = DateTime.parse('2026-01-07T12:05:00.000Z');
59
60
final middle = DateTime.parse('2026-01-07T12:02:00.000Z');
60
61
61
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
62
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
62
63
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
63
64
64
-
service.markAsSeen(middle);
65
-
service.markAsSeen(earlier);
66
-
service.markAsSeen(later);
65
+
service.markAsSeen(middle, ownerDid);
66
+
service.markAsSeen(earlier, ownerDid);
67
+
service.markAsSeen(later, ownerDid);
67
68
68
69
await Future.delayed(const Duration(seconds: 3));
69
70
70
-
verify(() => mockRepository.markAsSeenLocally(later)).called(1);
71
+
verify(() => mockRepository.markAsSeenLocally(later, ownerDid)).called(1);
71
72
verify(() => mockRepository.updateSeen(later)).called(1);
72
73
});
73
74
···
75
76
final timestamp1 = DateTime.parse('2026-01-07T12:00:00.000Z');
76
77
final timestamp2 = DateTime.parse('2026-01-07T12:01:00.000Z');
77
78
78
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
79
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
79
80
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
80
81
81
-
service.markAsSeen(timestamp1);
82
+
service.markAsSeen(timestamp1, ownerDid);
82
83
83
84
await Future.delayed(const Duration(seconds: 1));
84
85
85
-
service.markAsSeen(timestamp2);
86
+
service.markAsSeen(timestamp2, ownerDid);
86
87
87
88
await Future.delayed(const Duration(milliseconds: 2500));
88
89
89
-
verify(() => mockRepository.markAsSeenLocally(timestamp2)).called(1);
90
+
verify(() => mockRepository.markAsSeenLocally(timestamp2, ownerDid)).called(1);
90
91
});
91
92
});
92
93
···
94
95
test('immediately flushes pending operations', () async {
95
96
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
96
97
97
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
98
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
98
99
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
99
100
100
-
service.markAsSeen(timestamp);
101
+
service.markAsSeen(timestamp, ownerDid);
101
102
102
103
await service.flush();
103
104
104
-
verify(() => mockRepository.markAsSeenLocally(timestamp)).called(1);
105
+
verify(() => mockRepository.markAsSeenLocally(timestamp, ownerDid)).called(1);
105
106
verify(() => mockRepository.updateSeen(timestamp)).called(1);
106
107
});
107
108
108
109
test('does nothing when no pending operations', () async {
109
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
110
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
110
111
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
111
112
112
113
await service.flush();
113
114
114
-
verifyNever(() => mockRepository.markAsSeenLocally(any()));
115
+
verifyNever(() => mockRepository.markAsSeenLocally(any(), any()));
115
116
verifyNever(() => mockRepository.updateSeen(any()));
116
117
});
117
118
118
119
test('cancels pending timer', () async {
119
120
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
120
121
121
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
122
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
122
123
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
123
124
124
-
service.markAsSeen(timestamp);
125
+
service.markAsSeen(timestamp, ownerDid);
125
126
await service.flush();
126
127
127
128
await Future.delayed(const Duration(seconds: 3));
128
129
129
-
verify(() => mockRepository.markAsSeenLocally(timestamp)).called(1);
130
+
verify(() => mockRepository.markAsSeenLocally(timestamp, ownerDid)).called(1);
130
131
verify(() => mockRepository.updateSeen(timestamp)).called(1);
131
132
});
132
133
});
···
135
136
test('updates local cache even when API fails', () async {
136
137
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
137
138
138
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
139
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
139
140
when(() => mockRepository.updateSeen(any())).thenThrow(Exception('Network error'));
140
-
when(() => mockSyncQueue.enqueueMarkSeen(any())).thenAnswer((_) async => 1);
141
+
when(() => mockSyncQueue.enqueueMarkSeen(any(), any())).thenAnswer((_) async => 1);
141
142
142
-
service.markAsSeen(timestamp);
143
+
service.markAsSeen(timestamp, ownerDid);
143
144
await Future.delayed(const Duration(seconds: 3));
144
145
145
-
verify(() => mockRepository.markAsSeenLocally(timestamp)).called(1);
146
+
verify(() => mockRepository.markAsSeenLocally(timestamp, ownerDid)).called(1);
146
147
verify(() => mockRepository.updateSeen(timestamp)).called(1);
147
148
verify(() => mockLogger.error(any(), any(), any())).called(1);
148
149
});
···
150
151
test('does not retry failed operations automatically', () async {
151
152
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
152
153
153
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
154
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
154
155
when(() => mockRepository.updateSeen(any())).thenThrow(Exception('Network error'));
155
-
when(() => mockSyncQueue.enqueueMarkSeen(any())).thenAnswer((_) async => 1);
156
+
when(() => mockSyncQueue.enqueueMarkSeen(any(), any())).thenAnswer((_) async => 1);
156
157
157
-
service.markAsSeen(timestamp);
158
+
service.markAsSeen(timestamp, ownerDid);
158
159
await Future.delayed(const Duration(seconds: 3));
159
160
160
161
verify(() => mockRepository.updateSeen(timestamp)).called(1);
···
163
164
test('enqueues failed operation to sync queue', () async {
164
165
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
165
166
166
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
167
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
167
168
when(() => mockRepository.updateSeen(any())).thenThrow(Exception('Network error'));
168
-
when(() => mockSyncQueue.enqueueMarkSeen(any())).thenAnswer((_) async => 1);
169
+
when(() => mockSyncQueue.enqueueMarkSeen(any(), any())).thenAnswer((_) async => 1);
169
170
170
-
service.markAsSeen(timestamp);
171
+
service.markAsSeen(timestamp, ownerDid);
171
172
await Future.delayed(const Duration(seconds: 3));
172
173
173
-
verify(() => mockSyncQueue.enqueueMarkSeen(timestamp)).called(1);
174
+
verify(() => mockSyncQueue.enqueueMarkSeen(timestamp, ownerDid)).called(1);
174
175
});
175
176
176
177
test('logs error if queueing fails', () async {
177
178
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
178
179
179
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
180
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
180
181
when(() => mockRepository.updateSeen(any())).thenThrow(Exception('Network error'));
181
-
when(() => mockSyncQueue.enqueueMarkSeen(any())).thenThrow(Exception('Queue error'));
182
-
183
-
service.markAsSeen(timestamp);
182
+
when(
183
+
() => mockSyncQueue.enqueueMarkSeen(any(), any()),
184
+
).thenThrow(Exception('Queue error'));
185
+
service.markAsSeen(timestamp, ownerDid);
184
186
await Future.delayed(const Duration(seconds: 3));
185
187
186
188
verify(() => mockLogger.error(any(), any(), any())).called(2);
···
189
191
test('does not enqueue when flush succeeds', () async {
190
192
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
191
193
192
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
194
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
193
195
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
194
196
195
-
service.markAsSeen(timestamp);
197
+
service.markAsSeen(timestamp, ownerDid);
196
198
await Future.delayed(const Duration(seconds: 3));
197
199
198
-
verifyNever(() => mockSyncQueue.enqueueMarkSeen(any()));
200
+
verifyNever(() => mockSyncQueue.enqueueMarkSeen(any(), any()));
199
201
});
200
202
});
201
203
···
203
205
test('cancels pending timer', () async {
204
206
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
205
207
206
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
208
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
207
209
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
208
210
209
-
service.markAsSeen(timestamp);
211
+
service.markAsSeen(timestamp, ownerDid);
210
212
service.dispose();
211
213
212
214
await Future.delayed(const Duration(seconds: 3));
213
215
214
-
verifyNever(() => mockRepository.markAsSeenLocally(any()));
216
+
verifyNever(() => mockRepository.markAsSeenLocally(any(), any()));
215
217
verifyNever(() => mockRepository.updateSeen(any()));
216
218
});
217
219
});
···
220
222
test('prevents concurrent flush operations', () async {
221
223
final timestamp = DateTime.parse('2026-01-07T12:00:00.000Z');
222
224
223
-
when(() => mockRepository.markAsSeenLocally(any())).thenAnswer((_) async {});
225
+
when(() => mockRepository.markAsSeenLocally(any(), any())).thenAnswer((_) async {});
224
226
when(
225
227
() => mockRepository.updateSeen(any()),
226
228
).thenAnswer((_) async => Future.delayed(const Duration(milliseconds: 100)));
227
229
228
-
service.markAsSeen(timestamp);
230
+
service.markAsSeen(timestamp, ownerDid);
229
231
230
232
final flush1 = service.flush();
231
233
final flush2 = service.flush();
232
234
233
235
await Future.wait([flush1, flush2]);
234
236
235
-
verify(() => mockRepository.markAsSeenLocally(timestamp)).called(1);
237
+
verify(() => mockRepository.markAsSeenLocally(timestamp, ownerDid)).called(1);
236
238
verify(() => mockRepository.updateSeen(timestamp)).called(1);
237
239
});
238
240
});
+44
-20
test/src/features/notifications/application/notifications_notifier_test.dart
+44
-20
test/src/features/notifications/application/notifications_notifier_test.dart
···
41
41
when(() => mockLogger.info(any(), any())).thenReturn(null);
42
42
when(() => mockLogger.error(any(), any(), any())).thenReturn(null);
43
43
44
-
when(() => mockRepository.watchNotifications()).thenAnswer((_) => Stream.value([]));
44
+
when(
45
+
() => mockRepository.watchNotifications(any(named: 'ownerDid')),
46
+
).thenAnswer((_) => Stream.value([]));
45
47
when(
46
48
() => mockRepository.fetchNotifications(
47
49
cursor: any(named: 'cursor'),
48
50
limit: any(named: 'limit'),
51
+
ownerDid: any(named: 'ownerDid'),
49
52
),
50
53
).thenAnswer((_) async {});
51
-
when(() => mockRepository.fetchNotifications()).thenAnswer((_) async {});
52
-
when(() => mockRepository.getCursor()).thenAnswer((_) async => null);
53
-
when(() => mockRepository.markAllAsRead()).thenAnswer((_) async {});
54
+
when(
55
+
() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid')),
56
+
).thenAnswer((_) async {});
57
+
when(() => mockRepository.getCursor(any(named: 'ownerDid'))).thenAnswer((_) async => null);
58
+
when(() => mockRepository.markAllAsRead(any(named: 'ownerDid'))).thenAnswer((_) async {});
54
59
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
55
60
when(() => mockMarkAsSeenService.flush()).thenAnswer((_) async {});
56
61
···
66
71
67
72
await Future.delayed(Duration.zero);
68
73
69
-
verify(() => mockRepository.watchNotifications()).called(1);
74
+
verify(() => mockRepository.watchNotifications(any(named: 'ownerDid'))).called(1);
70
75
});
71
76
72
77
test('refresh calls repository fetchNotifications when authenticated', () async {
···
75
80
76
81
await container.read(notificationsProvider.notifier).refresh();
77
82
78
-
verify(() => mockRepository.fetchNotifications()).called(1);
83
+
verify(() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid'))).called(1);
79
84
});
80
85
81
86
test('refresh skips fetch when not authenticated', () async {
···
84
89
85
90
await container.read(notificationsProvider.notifier).refresh();
86
91
87
-
verifyNever(() => mockRepository.fetchNotifications());
92
+
verifyNever(() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid')));
88
93
});
89
94
90
95
test('loadMore fetches next page using cursor', () async {
91
-
when(() => mockRepository.getCursor()).thenAnswer((_) async => 'next_cursor');
92
96
when(
93
-
() => mockRepository.fetchNotifications(cursor: 'next_cursor'),
97
+
() => mockRepository.getCursor(any(named: 'ownerDid')),
98
+
).thenAnswer((_) async => 'next_cursor');
99
+
when(
100
+
() => mockRepository.fetchNotifications(
101
+
cursor: 'next_cursor',
102
+
ownerDid: any(named: 'ownerDid'),
103
+
),
94
104
).thenAnswer((_) async {});
95
105
96
106
final container = createContainer();
···
98
108
99
109
await container.read(notificationsProvider.notifier).loadMore();
100
110
101
-
verify(() => mockRepository.getCursor()).called(1);
102
-
verify(() => mockRepository.fetchNotifications(cursor: 'next_cursor')).called(1);
111
+
verify(() => mockRepository.getCursor(any(named: 'ownerDid'))).called(1);
112
+
verify(
113
+
() => mockRepository.fetchNotifications(
114
+
cursor: 'next_cursor',
115
+
ownerDid: any(named: 'ownerDid'),
116
+
),
117
+
).called(1);
103
118
});
104
119
105
120
test('loadMore does nothing without cursor', () async {
106
-
when(() => mockRepository.getCursor()).thenAnswer((_) async => null);
121
+
when(() => mockRepository.getCursor(any(named: 'ownerDid'))).thenAnswer((_) async => null);
107
122
108
123
final container = createContainer();
109
124
addTearDown(container.dispose);
110
125
111
126
await container.read(notificationsProvider.notifier).loadMore();
112
127
113
-
verify(() => mockRepository.getCursor()).called(1);
114
-
verifyNever(() => mockRepository.fetchNotifications(cursor: any(named: 'cursor')));
128
+
verify(() => mockRepository.getCursor(any(named: 'ownerDid'))).called(1);
129
+
verifyNever(
130
+
() => mockRepository.fetchNotifications(
131
+
cursor: any(named: 'cursor'),
132
+
ownerDid: any(named: 'ownerDid'),
133
+
),
134
+
);
115
135
});
116
136
117
137
test('loadMore skips when not authenticated', () async {
···
120
140
121
141
await container.read(notificationsProvider.notifier).loadMore();
122
142
123
-
verifyNever(() => mockRepository.getCursor());
143
+
verifyNever(() => mockRepository.getCursor(any(named: 'ownerDid')));
124
144
});
125
145
126
146
test('markAllAsRead flushes service and syncs with server', () async {
···
131
151
132
152
verifyInOrder([
133
153
() => mockMarkAsSeenService.flush(),
134
-
() => mockRepository.markAllAsRead(),
154
+
() => mockRepository.markAllAsRead(any(named: 'ownerDid')),
135
155
() => mockRepository.updateSeen(any()),
136
156
]);
137
157
});
138
158
139
159
test('markAllAsRead rethrows errors', () async {
140
160
when(() => mockMarkAsSeenService.flush()).thenAnswer((_) async {});
141
-
when(() => mockRepository.markAllAsRead()).thenThrow(Exception('DB error'));
161
+
when(
162
+
() => mockRepository.markAllAsRead(any(named: 'ownerDid')),
163
+
).thenThrow(Exception('DB error'));
142
164
143
165
final container = createContainer();
144
166
addTearDown(container.dispose);
···
150
172
});
151
173
152
174
test('refresh rethrows errors', () async {
153
-
when(() => mockRepository.fetchNotifications()).thenThrow(Exception('Network error'));
175
+
when(
176
+
() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid')),
177
+
).thenThrow(Exception('Network error'));
154
178
155
179
final container = createContainer();
156
180
addTearDown(container.dispose);
···
170
194
);
171
195
172
196
when(
173
-
() => mockRepository.watchNotifications(),
197
+
() => mockRepository.watchNotifications(any(named: 'ownerDid')),
174
198
).thenAnswer((_) => Stream.value([testNotification]));
175
199
176
200
final container = createContainer();
···
180
204
181
205
await Future.delayed(Duration.zero);
182
206
183
-
verify(() => mockRepository.watchNotifications()).called(1);
207
+
verify(() => mockRepository.watchNotifications(any(named: 'ownerDid'))).called(1);
184
208
});
185
209
});
186
210
}
+19
-9
test/src/features/notifications/application/unread_count_notifier_test.dart
+19
-9
test/src/features/notifications/application/unread_count_notifier_test.dart
···
38
38
when(() => mockLogger.info(any(), any())).thenReturn(null);
39
39
when(() => mockLogger.error(any(), any(), any())).thenReturn(null);
40
40
41
-
when(() => mockRepository.watchUnreadCount()).thenAnswer((_) => Stream.value(0));
41
+
when(
42
+
() => mockRepository.watchUnreadCount(any(named: 'ownerDid')),
43
+
).thenAnswer((_) => Stream.value(0));
42
44
});
43
45
44
46
group('UnreadCountNotifier', () {
···
50
52
51
53
await pumpEventQueue();
52
54
53
-
verify(() => mockRepository.watchUnreadCount()).called(1);
55
+
verify(() => mockRepository.watchUnreadCount(any(named: 'ownerDid'))).called(1);
54
56
});
55
57
56
58
test('build returns 0 when not authenticated', () async {
···
65
67
await pumpEventQueue();
66
68
67
69
expect(emittedValue, 0);
68
-
verifyNever(() => mockRepository.watchUnreadCount());
70
+
verifyNever(() => mockRepository.watchUnreadCount(any(named: 'ownerDid')));
69
71
});
70
72
71
73
test('stream emits unread count from repository', () async {
72
-
when(() => mockRepository.watchUnreadCount()).thenAnswer((_) => Stream.value(5));
74
+
when(
75
+
() => mockRepository.watchUnreadCount(any(named: 'ownerDid')),
76
+
).thenAnswer((_) => Stream.value(5));
73
77
74
78
final container = createContainer();
75
79
addTearDown(container.dispose);
···
82
86
await pumpEventQueue();
83
87
84
88
expect(emittedValue, 5);
85
-
verify(() => mockRepository.watchUnreadCount()).called(1);
89
+
verify(() => mockRepository.watchUnreadCount(any(named: 'ownerDid'))).called(1);
86
90
});
87
91
88
92
test('stream emits multiple updates as count changes', () async {
89
93
final controller = StreamController<int>();
90
-
when(() => mockRepository.watchUnreadCount()).thenAnswer((_) => controller.stream);
94
+
when(
95
+
() => mockRepository.watchUnreadCount(any(named: 'ownerDid')),
96
+
).thenAnswer((_) => controller.stream);
91
97
92
98
final container = createContainer();
93
99
addTearDown(() {
···
113
119
});
114
120
115
121
test('stream handles zero unread count', () async {
116
-
when(() => mockRepository.watchUnreadCount()).thenAnswer((_) => Stream.value(0));
122
+
when(
123
+
() => mockRepository.watchUnreadCount(any(named: 'ownerDid')),
124
+
).thenAnswer((_) => Stream.value(0));
117
125
118
126
final container = createContainer();
119
127
addTearDown(container.dispose);
···
126
134
await pumpEventQueue();
127
135
128
136
expect(emittedValue, 0);
129
-
verify(() => mockRepository.watchUnreadCount()).called(1);
137
+
verify(() => mockRepository.watchUnreadCount(any(named: 'ownerDid'))).called(1);
130
138
});
131
139
132
140
test('stream handles large unread counts', () async {
133
-
when(() => mockRepository.watchUnreadCount()).thenAnswer((_) => Stream.value(9999));
141
+
when(
142
+
() => mockRepository.watchUnreadCount(any(named: 'ownerDid')),
143
+
).thenAnswer((_) => Stream.value(9999));
134
144
135
145
final container = createContainer();
136
146
addTearDown(container.dispose);
+53
-43
test/src/features/notifications/infrastructure/notifications_repository_test.dart
+53
-43
test/src/features/notifications/infrastructure/notifications_repository_test.dart
···
17
17
late MockNotificationsSyncQueueDao mockSyncQueue;
18
18
late MockLogger mockLogger;
19
19
late NotificationsRepository repository;
20
+
const ownerDid = 'did:web:tester';
20
21
21
22
setUp(() {
22
23
mockApi = MockXrpcClient();
···
41
42
newNotifications: any(named: 'newNotifications'),
42
43
newProfiles: any(named: 'newProfiles'),
43
44
newCursor: any(named: 'newCursor'),
45
+
ownerDid: any(named: 'ownerDid'),
44
46
),
45
47
).thenAnswer((_) async {});
46
48
47
-
await repository.fetchNotifications();
49
+
await repository.fetchNotifications(ownerDid: ownerDid);
48
50
49
51
verify(
50
52
() => mockApi.call(
···
63
65
newNotifications: any(named: 'newNotifications'),
64
66
newProfiles: any(named: 'newProfiles'),
65
67
newCursor: any(named: 'newCursor'),
68
+
ownerDid: any(named: 'ownerDid'),
66
69
),
67
70
).thenAnswer((_) async {});
68
71
69
-
await repository.fetchNotifications(cursor: 'test_cursor');
72
+
await repository.fetchNotifications(cursor: 'test_cursor', ownerDid: ownerDid);
70
73
71
74
verify(
72
75
() => mockApi.call(
···
114
117
newNotifications: any(named: 'newNotifications'),
115
118
newProfiles: any(named: 'newProfiles'),
116
119
newCursor: any(named: 'newCursor'),
120
+
ownerDid: any(named: 'ownerDid'),
117
121
),
118
122
).thenAnswer((invocation) async {
119
123
capturedNotifications = invocation.namedArguments[#newNotifications] as List;
120
124
capturedProfiles = invocation.namedArguments[#newProfiles] as List;
121
125
});
122
126
123
-
await repository.fetchNotifications();
127
+
await repository.fetchNotifications(ownerDid: ownerDid);
124
128
125
129
expect(capturedNotifications, hasLength(2));
126
130
expect(capturedProfiles, hasLength(2));
···
130
134
newNotifications: any(named: 'newNotifications'),
131
135
newProfiles: any(named: 'newProfiles'),
132
136
newCursor: 'next_cursor',
137
+
ownerDid: ownerDid,
133
138
),
134
139
).called(1);
135
140
});
···
156
161
newNotifications: any(named: 'newNotifications'),
157
162
newProfiles: any(named: 'newProfiles'),
158
163
newCursor: any(named: 'newCursor'),
164
+
ownerDid: any(named: 'ownerDid'),
159
165
),
160
166
).thenAnswer((invocation) async {
161
167
capturedNotifications = invocation.namedArguments[#newNotifications] as List;
162
168
});
163
169
164
-
await repository.fetchNotifications();
170
+
await repository.fetchNotifications(ownerDid: ownerDid);
165
171
166
172
expect(capturedNotifications, isEmpty);
167
173
});
···
189
195
newNotifications: any(named: 'newNotifications'),
190
196
newProfiles: any(named: 'newProfiles'),
191
197
newCursor: any(named: 'newCursor'),
198
+
ownerDid: any(named: 'ownerDid'),
192
199
),
193
200
).thenAnswer((invocation) async {
194
201
capturedNotifications = invocation.namedArguments[#newNotifications] as List;
195
202
});
196
203
197
-
await repository.fetchNotifications();
204
+
await repository.fetchNotifications(ownerDid: ownerDid);
198
205
199
206
expect(capturedNotifications, isEmpty);
200
207
});
···
222
229
newNotifications: any(named: 'newNotifications'),
223
230
newProfiles: any(named: 'newProfiles'),
224
231
newCursor: any(named: 'newCursor'),
232
+
ownerDid: any(named: 'ownerDid'),
225
233
),
226
234
).thenAnswer((invocation) async {
227
235
capturedNotifications = invocation.namedArguments[#newNotifications] as List;
228
236
});
229
237
230
-
await repository.fetchNotifications();
238
+
await repository.fetchNotifications(ownerDid: ownerDid);
231
239
232
240
expect(capturedNotifications, hasLength(1));
233
241
});
···
237
245
() => mockApi.call(any(), params: any(named: 'params')),
238
246
).thenThrow(Exception('Network error'));
239
247
240
-
expect(() => repository.fetchNotifications(), throwsException);
248
+
expect(() => repository.fetchNotifications(ownerDid: ownerDid), throwsException);
241
249
242
250
verify(() => mockLogger.error(any(), any(), any())).called(1);
243
251
});
···
245
253
246
254
group('watchNotifications', () {
247
255
test('returns stream from DAO mapped to domain models', () async {
248
-
when(() => mockDao.watchNotifications()).thenAnswer((_) => Stream.value([]));
256
+
when(() => mockDao.watchNotifications(any())).thenAnswer((_) => Stream.value([]));
249
257
250
-
final stream = repository.watchNotifications();
258
+
final stream = repository.watchNotifications(ownerDid);
251
259
final result = await stream.first;
252
260
253
261
expect(result, isEmpty);
254
-
verify(() => mockDao.watchNotifications()).called(1);
262
+
verify(() => mockDao.watchNotifications(ownerDid)).called(1);
255
263
});
256
264
});
257
265
258
266
group('getCursor', () {
259
267
test('returns cursor from DAO', () async {
260
-
when(() => mockDao.getCursor()).thenAnswer((_) async => 'test_cursor');
268
+
when(() => mockDao.getCursor(any())).thenAnswer((_) async => 'test_cursor');
261
269
262
-
final cursor = await repository.getCursor();
270
+
final cursor = await repository.getCursor(ownerDid);
263
271
264
272
expect(cursor, 'test_cursor');
265
-
verify(() => mockDao.getCursor()).called(1);
273
+
verify(() => mockDao.getCursor(ownerDid)).called(1);
266
274
});
267
275
});
268
276
269
277
group('clearNotifications', () {
270
278
test('calls DAO clearNotifications', () async {
271
-
when(() => mockDao.clearNotifications()).thenAnswer((_) async {});
279
+
when(() => mockDao.clearNotifications(any())).thenAnswer((_) async {});
272
280
273
-
await repository.clearNotifications();
281
+
await repository.clearNotifications(ownerDid);
274
282
275
-
verify(() => mockDao.clearNotifications()).called(1);
283
+
verify(() => mockDao.clearNotifications(ownerDid)).called(1);
276
284
});
277
285
});
278
286
279
287
group('markAllAsRead', () {
280
288
test('calls DAO markAllAsRead', () async {
281
-
when(() => mockDao.markAllAsRead()).thenAnswer((_) async {});
289
+
when(() => mockDao.markAllAsRead(any())).thenAnswer((_) async {});
282
290
283
-
await repository.markAllAsRead();
291
+
await repository.markAllAsRead(ownerDid);
284
292
285
-
verify(() => mockDao.markAllAsRead()).called(1);
293
+
verify(() => mockDao.markAllAsRead(ownerDid)).called(1);
286
294
});
287
295
});
288
296
···
347
355
group('markAsSeenLocally', () {
348
356
test('calls DAO markAsSeenBefore with timestamp', () async {
349
357
final seenAt = DateTime.parse('2026-01-07T12:30:00.000Z');
350
-
when(() => mockDao.markAsSeenBefore(any())).thenAnswer((_) async {});
358
+
when(() => mockDao.markAsSeenBefore(any(), any())).thenAnswer((_) async {});
351
359
352
-
await repository.markAsSeenLocally(seenAt);
360
+
await repository.markAsSeenLocally(seenAt, ownerDid);
353
361
354
-
verify(() => mockDao.markAsSeenBefore(seenAt)).called(1);
362
+
verify(() => mockDao.markAsSeenBefore(seenAt, ownerDid)).called(1);
355
363
});
356
364
});
357
365
358
366
group('watchUnreadCount', () {
359
367
test('returns stream from DAO', () async {
360
-
when(() => mockDao.watchUnreadCount()).thenAnswer((_) => Stream.value(5));
368
+
when(() => mockDao.watchUnreadCount(any())).thenAnswer((_) => Stream.value(5));
361
369
362
-
final stream = repository.watchUnreadCount();
370
+
final stream = repository.watchUnreadCount(ownerDid);
363
371
final result = await stream.first;
364
372
365
373
expect(result, 5);
366
-
verify(() => mockDao.watchUnreadCount()).called(1);
374
+
verify(() => mockDao.watchUnreadCount(ownerDid)).called(1);
367
375
});
368
376
});
369
377
370
378
group('processSyncQueue', () {
371
379
test('does nothing when queue is empty', () async {
372
380
when(() => mockSyncQueue.cleanupOldFailedItems(any())).thenAnswer((_) async => 0);
373
-
when(() => mockSyncQueue.getLatestSeenAt()).thenAnswer((_) async => null);
381
+
when(() => mockSyncQueue.getLatestSeenAt(any())).thenAnswer((_) async => null);
374
382
375
-
await repository.processSyncQueue();
383
+
await repository.processSyncQueue(ownerDid);
376
384
377
385
verify(() => mockSyncQueue.cleanupOldFailedItems(any())).called(1);
378
-
verify(() => mockSyncQueue.getLatestSeenAt()).called(1);
379
-
verifyNever(() => mockDao.markAsSeenBefore(any()));
386
+
verify(() => mockSyncQueue.getLatestSeenAt(ownerDid)).called(1);
387
+
verifyNever(() => mockDao.markAsSeenBefore(any(), any()));
380
388
verifyNever(() => mockApi.call(any(), body: any(named: 'body')));
381
389
});
382
390
···
384
392
final latestSeenAt = DateTime.parse('2026-01-07T12:30:00.000Z');
385
393
386
394
when(() => mockSyncQueue.cleanupOldFailedItems(any())).thenAnswer((_) async => 0);
387
-
when(() => mockSyncQueue.getLatestSeenAt()).thenAnswer((_) async => latestSeenAt);
388
-
when(() => mockSyncQueue.getRetryableItems()).thenAnswer((_) async => []);
389
-
when(() => mockDao.markAsSeenBefore(any())).thenAnswer((_) async {});
395
+
when(() => mockSyncQueue.getLatestSeenAt(any())).thenAnswer((_) async => latestSeenAt);
396
+
when(() => mockSyncQueue.getRetryableItems(any())).thenAnswer((_) async => []);
397
+
when(() => mockDao.markAsSeenBefore(any(), any())).thenAnswer((_) async {});
390
398
when(() => mockApi.call(any(), body: any(named: 'body'))).thenAnswer((_) async => {});
391
-
when(() => mockSyncQueue.deleteItemsUpTo(any())).thenAnswer((_) async => 2);
399
+
when(() => mockSyncQueue.deleteItemsUpTo(any(), any())).thenAnswer((_) async => 2);
392
400
393
-
await repository.processSyncQueue();
401
+
await repository.processSyncQueue(ownerDid);
394
402
395
-
verify(() => mockDao.markAsSeenBefore(latestSeenAt)).called(1);
403
+
verify(() => mockDao.markAsSeenBefore(latestSeenAt, ownerDid)).called(1);
396
404
verify(
397
405
() => mockApi.call(
398
406
'app.bsky.notification.updateSeen',
399
407
body: {'seenAt': latestSeenAt.toIso8601String()},
400
408
),
401
409
).called(1);
402
-
verify(() => mockSyncQueue.deleteItemsUpTo(latestSeenAt)).called(1);
410
+
verify(() => mockSyncQueue.deleteItemsUpTo(latestSeenAt, ownerDid)).called(1);
403
411
});
404
412
405
413
test('increments retry count on failure', () async {
···
412
420
seenAt: latestSeenAt.toIso8601String(),
413
421
createdAt: now,
414
422
retryCount: 0,
423
+
ownerDid: ownerDid,
415
424
);
416
425
final mockItem2 = NotificationsSyncQueueData(
417
426
id: 2,
···
419
428
seenAt: latestSeenAt.toIso8601String(),
420
429
createdAt: now,
421
430
retryCount: 1,
431
+
ownerDid: ownerDid,
422
432
);
423
433
424
434
when(() => mockSyncQueue.cleanupOldFailedItems(any())).thenAnswer((_) async => 0);
425
-
when(() => mockSyncQueue.getLatestSeenAt()).thenAnswer((_) async => latestSeenAt);
435
+
when(() => mockSyncQueue.getLatestSeenAt(any())).thenAnswer((_) async => latestSeenAt);
426
436
when(
427
-
() => mockSyncQueue.getRetryableItems(),
437
+
() => mockSyncQueue.getRetryableItems(any()),
428
438
).thenAnswer((_) async => [mockItem1, mockItem2]);
429
-
when(() => mockDao.markAsSeenBefore(any())).thenAnswer((_) async {});
439
+
when(() => mockDao.markAsSeenBefore(any(), any())).thenAnswer((_) async {});
430
440
when(
431
441
() => mockApi.call(any(), body: any(named: 'body')),
432
442
).thenThrow(Exception('Network error'));
433
443
when(() => mockSyncQueue.incrementRetryCount(any())).thenAnswer((_) async => 1);
434
444
435
-
await repository.processSyncQueue();
445
+
await repository.processSyncQueue(ownerDid);
436
446
437
447
verify(() => mockSyncQueue.incrementRetryCount(1)).called(1);
438
448
verify(() => mockSyncQueue.incrementRetryCount(2)).called(1);
439
-
verifyNever(() => mockSyncQueue.deleteItemsUpTo(any()));
449
+
verifyNever(() => mockSyncQueue.deleteItemsUpTo(any(), any()));
440
450
});
441
451
442
452
test('cleans up old failed items', () async {
443
453
when(() => mockSyncQueue.cleanupOldFailedItems(any())).thenAnswer((_) async => 3);
444
-
when(() => mockSyncQueue.getLatestSeenAt()).thenAnswer((_) async => null);
454
+
when(() => mockSyncQueue.getLatestSeenAt(any())).thenAnswer((_) async => null);
445
455
446
-
await repository.processSyncQueue();
456
+
await repository.processSyncQueue(ownerDid);
447
457
448
458
verify(() => mockSyncQueue.cleanupOldFailedItems(any())).called(1);
449
459
verify(() => mockLogger.info(any(), any())).called(greaterThanOrEqualTo(1));
+13
-8
test/src/features/notifications/notifications_screen_test.dart
+13
-8
test/src/features/notifications/notifications_screen_test.dart
···
37
37
38
38
if (throwError) {
39
39
when(
40
-
() => mockRepository.watchNotifications(),
40
+
() => mockRepository.watchNotifications(any(named: 'ownerDid')),
41
41
).thenAnswer((_) => Stream.error(Exception('Network error')));
42
42
} else {
43
43
when(
44
-
() => mockRepository.watchNotifications(),
44
+
() => mockRepository.watchNotifications(any(named: 'ownerDid')),
45
45
).thenAnswer((_) => Stream.value(notifications));
46
46
}
47
47
···
60
60
when(() => mockLogger.info(any(), any())).thenReturn(null);
61
61
when(() => mockLogger.error(any(), any(), any())).thenReturn(null);
62
62
63
-
when(() => mockRepository.watchNotifications()).thenAnswer((_) => Stream.value([]));
63
+
when(
64
+
() => mockRepository.watchNotifications(any(named: 'ownerDid')),
65
+
).thenAnswer((_) => Stream.value([]));
64
66
when(
65
67
() => mockRepository.fetchNotifications(
66
68
cursor: any(named: 'cursor'),
67
69
limit: any(named: 'limit'),
70
+
ownerDid: any(named: 'ownerDid'),
68
71
),
69
72
).thenAnswer((_) async {});
70
-
when(() => mockRepository.fetchNotifications()).thenAnswer((_) async {});
71
-
when(() => mockRepository.getCursor()).thenAnswer((_) async => null);
72
-
when(() => mockRepository.markAllAsRead()).thenAnswer((_) async {});
73
+
when(
74
+
() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid')),
75
+
).thenAnswer((_) async {});
76
+
when(() => mockRepository.getCursor(any(named: 'ownerDid'))).thenAnswer((_) async => null);
77
+
when(() => mockRepository.markAllAsRead(any(named: 'ownerDid'))).thenAnswer((_) async {});
73
78
when(() => mockRepository.updateSeen(any())).thenAnswer((_) async {});
74
79
when(() => mockMarkAsSeenService.flush()).thenAnswer((_) async {});
75
80
···
125
130
await tester.pump();
126
131
await tester.pump(const Duration(milliseconds: 100));
127
132
128
-
verify(() => mockRepository.fetchNotifications()).called(1);
133
+
verify(() => mockRepository.fetchNotifications(ownerDid: any(named: 'ownerDid'))).called(1);
129
134
});
130
135
131
136
testWidgets('mark all as read button is visible when authenticated', (tester) async {
···
162
167
await tester.tap(find.byIcon(Icons.done_all));
163
168
await tester.pump();
164
169
165
-
verify(() => mockRepository.markAllAsRead()).called(1);
170
+
verify(() => mockRepository.markAllAsRead(any(named: 'ownerDid'))).called(1);
166
171
});
167
172
});
168
173
}
+1
-1
test/src/features/notifications/presentation/widgets/grouped_notification_item_test.dart
+1
-1
test/src/features/notifications/presentation/widgets/grouped_notification_item_test.dart
···
54
54
mockRepository = MockNotificationsRepository();
55
55
56
56
registerFallbackValue(DateTime.now());
57
-
when(() => mockMarkAsSeenService.markAsSeen(any())).thenReturn(null);
57
+
when(() => mockMarkAsSeenService.markAsSeen(any(), any(named: 'ownerDid'))).thenReturn(null);
58
58
});
59
59
60
60
group('GroupedNotificationItem', () {
+6
-4
test/src/features/profile/application/profile_notifier_test.dart
+6
-4
test/src/features/profile/application/profile_notifier_test.dart
···
36
36
],
37
37
);
38
38
39
-
when(() => mockRepository.getProfile(testDid)).thenAnswer((_) async => testProfile);
39
+
when(() => mockRepository.getProfile(testDid, any())).thenAnswer((_) async => testProfile);
40
40
});
41
41
42
42
tearDown(() {
···
48
48
when(() => mockRepository.muteActor(myDid, testDid)).thenAnswer((_) async {});
49
49
50
50
final notifier = container.read(profileProvider(testDid).notifier);
51
-
await container.read(profileProvider(testDid).future); // Wait for build
51
+
await container.read(profileProvider(testDid).future);
52
52
53
53
await notifier.toggleMute();
54
54
···
60
60
61
61
test('toggleMute un-mutes if already muted', () async {
62
62
final mutedProfile = testProfile.copyWith(viewerMuted: true);
63
-
when(() => mockRepository.getProfile(testDid)).thenAnswer((_) async => mutedProfile);
63
+
when(() => mockRepository.getProfile(testDid, any())).thenAnswer((_) async => mutedProfile);
64
64
when(() => mockRepository.unmuteActor(myDid, testDid)).thenAnswer((_) async {});
65
65
66
66
final notifier = container.read(profileProvider(testDid).notifier);
···
91
91
const blockUri = 'at://...';
92
92
final blockedProfile = testProfile.copyWith(viewerBlockingUri: blockUri);
93
93
94
-
when(() => mockRepository.getProfile(testDid)).thenAnswer((_) async => blockedProfile);
94
+
when(
95
+
() => mockRepository.getProfile(testDid, any()),
96
+
).thenAnswer((_) async => blockedProfile);
95
97
when(
96
98
() => mockRepository.unblockActor(myDid, blockUri, subjectDid: any(named: 'subjectDid')),
97
99
).thenAnswer((_) async {});
+2
-2
test/src/features/profile/infrastructure/profile_repository_pinned_post_test.dart
+2
-2
test/src/features/profile/infrastructure/profile_repository_pinned_post_test.dart
···
42
42
},
43
43
);
44
44
45
-
final profile = await repository.getProfile('test.bsky.social');
45
+
final profile = await repository.getProfile('test.bsky.social', any());
46
46
47
47
expect(profile.pinnedPostUri, 'at://did:plc:test123/app.bsky.feed.post/pinned123');
48
48
···
55
55
() => mockApi.call(any(), params: any(named: 'params')),
56
56
).thenAnswer((_) async => {'did': 'did:plc:test123', 'handle': 'test.bsky.social'});
57
57
58
-
final profile = await repository.getProfile('test.bsky.social');
58
+
final profile = await repository.getProfile('test.bsky.social', any());
59
59
60
60
expect(profile.pinnedPostUri, isNull);
61
61
});
+26
-11
test/src/features/profile/infrastructure/profile_repository_test.dart
+26
-11
test/src/features/profile/infrastructure/profile_repository_test.dart
···
37
37
() => mockApi.call(any(), params: any(named: 'params')),
38
38
).thenAnswer((_) async => _mockProfileResponse(withViewer: true));
39
39
40
-
final profile = await repository.getProfile('testuser.bsky.social');
40
+
final profile = await repository.getProfile('testuser.bsky.social', any());
41
41
42
42
expect(profile.did, 'did:plc:test123');
43
43
expect(profile.handle, 'testuser.bsky.social');
···
61
61
expect(cached, isNotNull);
62
62
expect(cached!.handle, 'testuser.bsky.social');
63
63
64
-
final relationship = await db.profileRelationshipDao.getRelationship('did:plc:test123');
64
+
final relationship = await db.profileRelationshipDao.getRelationship(
65
+
'did:plc:test123',
66
+
any(),
67
+
);
65
68
expect(relationship, isNotNull);
66
69
expect(relationship!.following, true);
67
70
expect(relationship.followingUri, 'at://did:plc:viewer/app.bsky.graph.follow/abc123');
···
72
75
() => mockApi.call(any(), params: any(named: 'params')),
73
76
).thenAnswer((_) async => {'did': 'did:plc:minimal', 'handle': 'minimal.bsky.social'});
74
77
75
-
final profile = await repository.getProfile('minimal.bsky.social');
78
+
final profile = await repository.getProfile('minimal.bsky.social', any());
76
79
77
80
expect(profile.did, 'did:plc:minimal');
78
81
expect(profile.handle, 'minimal.bsky.social');
···
85
88
final exception = Exception('Network error');
86
89
when(() => mockApi.call(any(), params: any(named: 'params'))).thenThrow(exception);
87
90
88
-
expect(() => repository.getProfile('testuser'), throwsA(isA<Exception>()));
91
+
expect(() => repository.getProfile('testuser', any()), throwsA(isA<Exception>()));
89
92
90
93
verify(() => mockLogger.error(any(), exception, any())).called(1);
91
94
});
···
97
100
() => mockApi.call(any(), params: any(named: 'params')),
98
101
).thenAnswer((_) async => _mockProfileResponse());
99
102
100
-
await repository.getProfile('testuser.bsky.social');
103
+
await repository.getProfile('testuser.bsky.social', any());
101
104
102
105
final result = await repository.getCachedProfile('did:plc:test123');
103
106
···
117
120
() => mockApi.call(any(), params: any(named: 'params')),
118
121
).thenAnswer((_) async => _mockProfileResponse());
119
122
120
-
await repository.getProfile('testuser.bsky.social');
123
+
await repository.getProfile('testuser.bsky.social', any());
121
124
122
125
final result = await repository.watchProfile('did:plc:test123').first;
123
126
expect(result, isNotNull);
···
173
176
() => mockApi.call(any(), params: any(named: 'params')),
174
177
).thenAnswer((_) async => _mockProfileResponse(withViewer: true));
175
178
176
-
final profile = await repository.getProfile('testuser');
179
+
final profile = await repository.getProfile('testuser', any());
177
180
178
181
expect(profile.viewerFollowing, isTrue);
179
182
expect(profile.viewerFollowUri, 'at://did:plc:viewer/app.bsky.graph.follow/abc123');
···
184
187
() => mockApi.call(any(), params: any(named: 'params')),
185
188
).thenAnswer((_) async => _mockProfileResponse());
186
189
187
-
final profile = await repository.getProfile('testuser');
190
+
final profile = await repository.getProfile('testuser', any());
188
191
189
192
expect(profile.viewerFollowing, isFalse);
190
193
expect(profile.viewerFollowUri, isNull);
···
314
317
ProfileRelationshipsCompanion.insert(
315
318
profileDid: 'did:plc:subject',
316
319
updatedAt: DateTime.now(),
320
+
ownerDid: 'did:plc:actor',
317
321
),
318
322
);
319
323
···
324
328
final uri = await repository.blockActor('did:plc:actor', 'did:plc:subject');
325
329
expect(uri, 'at://did:plc:actor/app.bsky.graph.block/rkey123');
326
330
327
-
final rel = await db.profileRelationshipDao.getRelationship('did:plc:subject');
331
+
final rel = await db.profileRelationshipDao.getRelationship(
332
+
'did:plc:subject',
333
+
'did:plc:actor',
334
+
);
328
335
expect(rel, isNotNull);
329
336
expect(rel!.blocked, isTrue);
330
337
expect(rel.blockingUri, uri);
···
339
346
blocked: const Value(true),
340
347
blockingUri: const Value('at://did:plc:actor/app.bsky.graph.block/rkey123'),
341
348
updatedAt: DateTime.now(),
349
+
ownerDid: 'did:plc:actor',
342
350
),
343
351
);
344
352
···
363
371
),
364
372
).called(1);
365
373
366
-
final rel = await db.profileRelationshipDao.getRelationship('did:plc:subject');
374
+
final rel = await db.profileRelationshipDao.getRelationship(
375
+
'did:plc:subject',
376
+
'did:plc:actor',
377
+
);
367
378
expect(rel, isNotNull);
368
379
expect(rel!.blocked, isFalse);
369
380
expect(rel.blockingUri, isNull);
···
376
387
blocked: const Value(true),
377
388
blockingUri: const Value('at://did:plc:actor/app.bsky.graph.block/rkey123'),
378
389
updatedAt: DateTime.now(),
390
+
ownerDid: 'did:plc:actor',
379
391
),
380
392
);
381
393
···
388
400
'at://did:plc:actor/app.bsky.graph.block/rkey123',
389
401
);
390
402
391
-
final rel = await db.profileRelationshipDao.getRelationship('did:plc:subject');
403
+
final rel = await db.profileRelationshipDao.getRelationship(
404
+
'did:plc:subject',
405
+
'did:plc:actor',
406
+
);
392
407
expect(rel, isNotNull);
393
408
expect(rel!.blocked, isTrue);
394
409
});
+1
-1
test/src/features/profile/presentation/widgets/profile_actions_sheet_test.dart
+1
-1
test/src/features/profile/presentation/widgets/profile_actions_sheet_test.dart
···
60
60
setUp(() {
61
61
mockProfileRepository = MockProfileRepository();
62
62
when(
63
-
() => mockProfileRepository.getProfile(any()),
63
+
() => mockProfileRepository.getProfile(any(), any()),
64
64
).thenAnswer((_) async => ProfileData(did: 'did:plc:test', handle: 'test.bsky.social'));
65
65
when(() => mockProfileRepository.watchProfile(any())).thenAnswer((_) => Stream.value(null));
66
66
when(
+12
-12
test/src/features/settings/application/preference_sync_controller_test.dart
+12
-12
test/src/features/settings/application/preference_sync_controller_test.dart
···
47
47
setUp(() {
48
48
mockRepository = MockBlueskyPreferencesRepository();
49
49
50
-
when(() => mockRepository.syncPreferencesFromRemote()).thenAnswer((_) async {});
51
-
when(() => mockRepository.processSyncQueue()).thenAnswer((_) async {});
52
-
when(() => mockRepository.clearAll()).thenAnswer((_) async {});
50
+
when(() => mockRepository.syncPreferencesFromRemote(any())).thenAnswer((_) async {});
51
+
when(() => mockRepository.processSyncQueue(any())).thenAnswer((_) async {});
52
+
when(() => mockRepository.clearAll(any())).thenAnswer((_) async {});
53
53
});
54
54
55
55
group('PreferenceSyncController', () {
···
70
70
container.read(preferenceSyncControllerProvider);
71
71
await Future<void>.delayed(Duration.zero);
72
72
73
-
verify(() => mockRepository.syncPreferencesFromRemote()).called(1);
74
-
verify(() => mockRepository.processSyncQueue()).called(1);
73
+
verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1);
74
+
verify(() => mockRepository.processSyncQueue(any())).called(1);
75
75
});
76
76
77
77
test('does not sync when user is not authenticated', () async {
···
89
89
container.read(preferenceSyncControllerProvider);
90
90
await Future<void>.delayed(Duration.zero);
91
91
92
-
verifyNever(() => mockRepository.syncPreferencesFromRemote());
93
-
verifyNever(() => mockRepository.processSyncQueue());
92
+
verifyNever(() => mockRepository.syncPreferencesFromRemote(any()));
93
+
verifyNever(() => mockRepository.processSyncQueue(any()));
94
94
});
95
95
});
96
96
···
100
100
final authState = AuthStateAuthenticated(fakeSession);
101
101
102
102
when(
103
-
() => mockRepository.syncPreferencesFromRemote(),
103
+
() => mockRepository.syncPreferencesFromRemote(any()),
104
104
).thenThrow(Exception('Network error'));
105
105
106
106
final container = ProviderContainer(
···
115
115
expect(() => container.read(preferenceSyncControllerProvider), returnsNormally);
116
116
await Future<void>.delayed(Duration.zero);
117
117
118
-
verify(() => mockRepository.syncPreferencesFromRemote()).called(1);
118
+
verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1);
119
119
});
120
120
121
121
test('handles queue processing errors gracefully', () async {
122
122
final fakeSession = _createFakeSession();
123
123
final authState = AuthStateAuthenticated(fakeSession);
124
124
125
-
when(() => mockRepository.processSyncQueue()).thenThrow(Exception('Queue error'));
125
+
when(() => mockRepository.processSyncQueue(any())).thenThrow(Exception('Queue error'));
126
126
127
127
final container = ProviderContainer(
128
128
overrides: [
···
138
138
});
139
139
140
140
test('does not crash when clearAll fails on logout', () async {
141
-
when(() => mockRepository.clearAll()).thenThrow(Exception('Clear failed'));
141
+
when(() => mockRepository.clearAll(any())).thenThrow(Exception('Clear failed'));
142
142
143
143
const authState = AuthStateUnauthenticated();
144
144
···
172
172
173
173
container.read(preferenceSyncControllerProvider);
174
174
await Future<void>.delayed(Duration.zero);
175
-
verify(() => mockRepository.syncPreferencesFromRemote()).called(1);
175
+
verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1);
176
176
});
177
177
});
178
178
});
+16
-12
test/src/features/settings/application/settings_providers_test.dart
+16
-12
test/src/features/settings/application/settings_providers_test.dart
···
26
26
test('adultContentPrefProvider calls repository watch method', () {
27
27
const expectedPref = AdultContentPref(enabled: true);
28
28
when(
29
-
() => mockRepository.watchAdultContentPref(),
29
+
() => mockRepository.watchAdultContentPref(any()),
30
30
).thenAnswer((_) => Stream.value(expectedPref));
31
31
32
32
container.listen(adultContentPrefProvider, (prev, next) {});
33
33
34
-
verify(() => mockRepository.watchAdultContentPref()).called(1);
34
+
verify(() => mockRepository.watchAdultContentPref(any())).called(1);
35
35
});
36
36
37
37
test('contentLabelPrefsProvider calls repository watch method', () {
38
38
const expectedPrefs = ContentLabelPrefs.empty;
39
39
when(
40
-
() => mockRepository.watchContentLabelPrefs(),
40
+
() => mockRepository.watchContentLabelPrefs(any()),
41
41
).thenAnswer((_) => Stream.value(expectedPrefs));
42
42
43
43
container.listen(contentLabelPrefsProvider, (prev, next) {});
44
44
45
-
verify(() => mockRepository.watchContentLabelPrefs()).called(1);
45
+
verify(() => mockRepository.watchContentLabelPrefs(any())).called(1);
46
46
});
47
47
48
48
test('labelersPrefProvider calls repository watch method', () {
49
49
const expectedPref = LabelersPref.empty;
50
-
when(() => mockRepository.watchLabelersPref()).thenAnswer((_) => Stream.value(expectedPref));
50
+
when(
51
+
() => mockRepository.watchLabelersPref(any()),
52
+
).thenAnswer((_) => Stream.value(expectedPref));
51
53
52
54
container.listen(labelersPrefProvider, (prev, next) {});
53
55
54
-
verify(() => mockRepository.watchLabelersPref()).called(1);
56
+
verify(() => mockRepository.watchLabelersPref(any())).called(1);
55
57
});
56
58
57
59
test('feedViewPrefProvider calls repository watch method', () {
58
60
const expectedPref = FeedViewPref.defaultPref;
59
-
when(() => mockRepository.watchFeedViewPref()).thenAnswer((_) => Stream.value(expectedPref));
61
+
when(
62
+
() => mockRepository.watchFeedViewPref(any()),
63
+
).thenAnswer((_) => Stream.value(expectedPref));
60
64
61
65
container.listen(feedViewPrefProvider, (prev, next) {});
62
66
63
-
verify(() => mockRepository.watchFeedViewPref()).called(1);
67
+
verify(() => mockRepository.watchFeedViewPref(any())).called(1);
64
68
});
65
69
66
70
test('threadViewPrefProvider calls repository watch method', () {
67
71
const expectedPref = ThreadViewPref.defaultPref;
68
72
when(
69
-
() => mockRepository.watchThreadViewPref(),
73
+
() => mockRepository.watchThreadViewPref(any()),
70
74
).thenAnswer((_) => Stream.value(expectedPref));
71
75
72
76
container.listen(threadViewPrefProvider, (prev, next) {});
73
77
74
-
verify(() => mockRepository.watchThreadViewPref()).called(1);
78
+
verify(() => mockRepository.watchThreadViewPref(any())).called(1);
75
79
});
76
80
77
81
test('mutedWordsPrefProvider calls repository watch method', () {
78
82
const expectedPref = MutedWordsPref.empty;
79
83
when(
80
-
() => mockRepository.watchMutedWordsPref(),
84
+
() => mockRepository.watchMutedWordsPref(any()),
81
85
).thenAnswer((_) => Stream.value(expectedPref));
82
86
83
87
container.listen(mutedWordsPrefProvider, (prev, next) {});
84
88
85
-
verify(() => mockRepository.watchMutedWordsPref()).called(1);
89
+
verify(() => mockRepository.watchMutedWordsPref(any())).called(1);
86
90
});
87
91
});
88
92
}
+21
-12
test/src/features/settings/presentation/screens/content_moderation_screen_test.dart
+21
-12
test/src/features/settings/presentation/screens/content_moderation_screen_test.dart
···
16
16
mockRepository = MockBlueskyPreferencesRepository();
17
17
18
18
when(
19
-
() => mockRepository.watchAdultContentPref(),
19
+
() => mockRepository.watchAdultContentPref(any()),
20
20
).thenAnswer((_) => Stream.value(const AdultContentPref(enabled: false)));
21
21
when(
22
-
() => mockRepository.watchContentLabelPrefs(),
22
+
() => mockRepository.watchContentLabelPrefs(any()),
23
23
).thenAnswer((_) => Stream.value(ContentLabelPrefs.empty));
24
24
when(
25
-
() => mockRepository.watchLabelersPref(),
25
+
() => mockRepository.watchLabelersPref(any()),
26
26
).thenAnswer((_) => Stream.value(LabelersPref.empty));
27
-
when(() => mockRepository.updateAdultContentPref(any())).thenAnswer((_) async {});
28
-
when(() => mockRepository.updateContentLabelPrefs(any())).thenAnswer((_) async {});
27
+
when(() => mockRepository.updateAdultContentPref(any(), any())).thenAnswer((_) async {});
28
+
when(() => mockRepository.updateContentLabelPrefs(any(), any())).thenAnswer((_) async {});
29
29
});
30
30
31
31
setUpAll(() {
···
38
38
ContentLabelPrefs labelPrefs = ContentLabelPrefs.empty,
39
39
LabelersPref labelersPref = LabelersPref.empty,
40
40
}) {
41
-
when(() => mockRepository.watchAdultContentPref()).thenAnswer((_) => Stream.value(adultPref));
41
+
when(
42
+
() => mockRepository.watchAdultContentPref(any()),
43
+
).thenAnswer((_) => Stream.value(adultPref));
42
44
when(
43
-
() => mockRepository.watchContentLabelPrefs(),
45
+
() => mockRepository.watchContentLabelPrefs(any()),
44
46
).thenAnswer((_) => Stream.value(labelPrefs));
45
-
when(() => mockRepository.watchLabelersPref()).thenAnswer((_) => Stream.value(labelersPref));
47
+
when(
48
+
() => mockRepository.watchLabelersPref(any()),
49
+
).thenAnswer((_) => Stream.value(labelersPref));
46
50
47
51
return ProviderScope(
48
52
overrides: [
49
53
blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository),
50
-
adultContentPrefProvider.overrideWith((ref) => mockRepository.watchAdultContentPref()),
51
-
contentLabelPrefsProvider.overrideWith((ref) => mockRepository.watchContentLabelPrefs()),
52
-
labelersPrefProvider.overrideWith((ref) => mockRepository.watchLabelersPref()),
54
+
adultContentPrefProvider.overrideWith(
55
+
(ref) => mockRepository.watchAdultContentPref(any()),
56
+
),
57
+
contentLabelPrefsProvider.overrideWith(
58
+
(ref) => mockRepository.watchContentLabelPrefs(any()),
59
+
),
60
+
labelersPrefProvider.overrideWith((ref) => mockRepository.watchLabelersPref(any())),
53
61
],
54
62
child: const MaterialApp(home: ContentModerationScreen()),
55
63
);
···
132
140
verify(
133
141
() => mockRepository.updateAdultContentPref(
134
142
any(that: predicate<AdultContentPref>((p) => p.enabled == true)),
143
+
any(),
135
144
),
136
145
).called(1);
137
146
});
···
168
177
await tester.tap(hideButtons.first);
169
178
await tester.pumpAndSettle();
170
179
171
-
verify(() => mockRepository.updateContentLabelPrefs(any())).called(1);
180
+
verify(() => mockRepository.updateContentLabelPrefs(any(), any())).called(1);
172
181
});
173
182
174
183
testWidgets('shows current label preferences', (tester) async {
+15
-8
test/src/features/settings/presentation/screens/feed_preferences_screen_test.dart
+15
-8
test/src/features/settings/presentation/screens/feed_preferences_screen_test.dart
···
16
16
mockRepository = MockBlueskyPreferencesRepository();
17
17
18
18
when(
19
-
() => mockRepository.watchFeedViewPref(),
19
+
() => mockRepository.watchFeedViewPref(any()),
20
20
).thenAnswer((_) => Stream.value(FeedViewPref.defaultPref));
21
21
when(
22
-
() => mockRepository.watchThreadViewPref(),
22
+
() => mockRepository.watchThreadViewPref(any()),
23
23
).thenAnswer((_) => Stream.value(ThreadViewPref.defaultPref));
24
-
when(() => mockRepository.updateFeedViewPref(any())).thenAnswer((_) async {});
25
-
when(() => mockRepository.updateThreadViewPref(any())).thenAnswer((_) async {});
24
+
when(() => mockRepository.updateFeedViewPref(any(), any())).thenAnswer((_) async {});
25
+
when(() => mockRepository.updateThreadViewPref(any(), any())).thenAnswer((_) async {});
26
26
});
27
27
28
28
setUpAll(() {
···
34
34
FeedViewPref feedPref = FeedViewPref.defaultPref,
35
35
ThreadViewPref threadPref = ThreadViewPref.defaultPref,
36
36
}) {
37
-
when(() => mockRepository.watchFeedViewPref()).thenAnswer((_) => Stream.value(feedPref));
38
-
when(() => mockRepository.watchThreadViewPref()).thenAnswer((_) => Stream.value(threadPref));
37
+
when(() => mockRepository.watchFeedViewPref(any())).thenAnswer((_) => Stream.value(feedPref));
38
+
when(
39
+
() => mockRepository.watchThreadViewPref(any()),
40
+
).thenAnswer((_) => Stream.value(threadPref));
39
41
40
42
return ProviderScope(
41
43
overrides: [
42
44
blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository),
43
-
feedViewPrefProvider.overrideWith((ref) => mockRepository.watchFeedViewPref()),
44
-
threadViewPrefProvider.overrideWith((ref) => mockRepository.watchThreadViewPref()),
45
+
feedViewPrefProvider.overrideWith((ref) => mockRepository.watchFeedViewPref(any())),
46
+
threadViewPrefProvider.overrideWith((ref) => mockRepository.watchThreadViewPref(any())),
45
47
],
46
48
child: const MaterialApp(home: FeedPreferencesScreen()),
47
49
);
···
88
90
verify(
89
91
() => mockRepository.updateFeedViewPref(
90
92
any(that: predicate<FeedViewPref>((p) => p.hideReplies == true)),
93
+
any(),
91
94
),
92
95
).called(1);
93
96
});
···
102
105
verify(
103
106
() => mockRepository.updateFeedViewPref(
104
107
any(that: predicate<FeedViewPref>((p) => p.hideReposts == true)),
108
+
any(),
105
109
),
106
110
).called(1);
107
111
});
···
116
120
verify(
117
121
() => mockRepository.updateFeedViewPref(
118
122
any(that: predicate<FeedViewPref>((p) => p.hideQuotePosts == true)),
123
+
any(),
119
124
),
120
125
).called(1);
121
126
});
···
140
145
verify(
141
146
() => mockRepository.updateThreadViewPref(
142
147
any(that: predicate<ThreadViewPref>((p) => p.sort == ThreadSortOrder.newest)),
148
+
any(),
143
149
),
144
150
).called(1);
145
151
});
···
157
163
verify(
158
164
() => mockRepository.updateThreadViewPref(
159
165
any(that: predicate<ThreadViewPref>((p) => p.prioritizeFollowedUsers == false)),
166
+
any(),
160
167
),
161
168
).called(1);
162
169
});
+4
-4
test/src/features/settings/presentation/screens/muted_words_screen_test.dart
+4
-4
test/src/features/settings/presentation/screens/muted_words_screen_test.dart
···
16
16
mockRepository = MockBlueskyPreferencesRepository();
17
17
18
18
when(
19
-
() => mockRepository.watchMutedWordsPref(),
19
+
() => mockRepository.watchMutedWordsPref(any()),
20
20
).thenAnswer((_) => Stream.value(MutedWordsPref.empty));
21
-
when(() => mockRepository.updateMutedWordsPref(any())).thenAnswer((_) async {});
21
+
when(() => mockRepository.updateMutedWordsPref(any(), any())).thenAnswer((_) async {});
22
22
});
23
23
24
24
setUpAll(() {
···
26
26
});
27
27
28
28
Widget buildTestWidget({MutedWordsPref pref = MutedWordsPref.empty}) {
29
-
when(() => mockRepository.watchMutedWordsPref()).thenAnswer((_) => Stream.value(pref));
29
+
when(() => mockRepository.watchMutedWordsPref(any())).thenAnswer((_) => Stream.value(pref));
30
30
31
31
return ProviderScope(
32
32
overrides: [
33
33
blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository),
34
-
mutedWordsPrefProvider.overrideWith((ref) => mockRepository.watchMutedWordsPref()),
34
+
mutedWordsPrefProvider.overrideWith((ref) => mockRepository.watchMutedWordsPref(any())),
35
35
],
36
36
child: const MaterialApp(home: MutedWordsScreen()),
37
37
);
+8
-4
test/src/features/thread/application/thread_notifier_test.dart
+8
-4
test/src/features/thread/application/thread_notifier_test.dart
···
36
36
37
37
group('ThreadNotifier', () {
38
38
test('build fetches thread successfully', () async {
39
-
when(() => mockRepository.getPostThread(postUri)).thenAnswer((_) async => expectedThread);
39
+
when(
40
+
() => mockRepository.getPostThread(postUri, any()),
41
+
).thenAnswer((_) async => expectedThread);
40
42
41
43
final result = await container.read(threadProvider(postUri).future);
42
44
43
45
expect(result, expectedThread);
44
-
verify(() => mockRepository.getPostThread(postUri)).called(1);
46
+
verify(() => mockRepository.getPostThread(postUri, any())).called(1);
45
47
});
46
48
47
49
test('refresh re-fetches thread', () async {
48
-
when(() => mockRepository.getPostThread(postUri)).thenAnswer((_) async => expectedThread);
50
+
when(
51
+
() => mockRepository.getPostThread(postUri, any()),
52
+
).thenAnswer((_) async => expectedThread);
49
53
50
54
await container.read(threadProvider(postUri).future);
51
55
52
56
await container.read(threadProvider(postUri).notifier).refresh();
53
57
54
-
verify(() => mockRepository.getPostThread(postUri)).called(2);
58
+
verify(() => mockRepository.getPostThread(postUri, any())).called(2);
55
59
});
56
60
});
57
61
}
+7
-7
test/src/features/thread/infrastructure/thread_repository_test.dart
+7
-7
test/src/features/thread/infrastructure/thread_repository_test.dart
···
63
63
() => mockApi.call(any(), params: any(named: 'params')),
64
64
).thenAnswer((_) async => mockResponse);
65
65
66
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
66
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
67
67
68
68
verify(
69
69
() => mockApi.call(
···
77
77
expect(thread.replies.first.post.uri, 'at://did:2/app.bsky.feed.post/2');
78
78
79
79
final cached = await db.feedContentDao
80
-
.watchFeedContent('thread:at://did:1/app.bsky.feed.post/1')
80
+
.watchFeedContent('thread:at://did:1/app.bsky.feed.post/1', 'did:1')
81
81
.first;
82
82
expect(cached, hasLength(2));
83
83
expect(
···
111
111
() => mockApi.call(any(), params: any(named: 'params')),
112
112
).thenAnswer((_) async => mockResponse);
113
113
114
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
114
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
115
115
expect(thread.replies, hasLength(2));
116
116
expect(thread.replies.first.post.placeholderReason, 'Post blocked');
117
117
expect(thread.replies.last.post.placeholderReason, 'Post not found');
···
150
150
() => mockApi.call(any(), params: any(named: 'params')),
151
151
).thenAnswer((_) async => mockResponse);
152
152
153
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
153
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
154
154
155
155
expect(thread.post.viewerLikeUri, 'at://did:viewer/app.bsky.feed.like/abc');
156
156
expect(thread.post.viewerRepostUri, 'at://did:viewer/app.bsky.feed.repost/def');
···
192
192
() => mockApi.call(any(), params: any(named: 'params')),
193
193
).thenAnswer((_) async => mockResponse);
194
194
195
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
195
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
196
196
197
197
expect(thread.threadgate, isNotNull);
198
198
expect(thread.threadgate!.uri, 'at://did:1/app.bsky.feed.threadgate/1');
···
226
226
() => mockApi.call(any(), params: any(named: 'params')),
227
227
).thenAnswer((_) async => mockResponse);
228
228
229
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
229
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
230
230
231
231
expect(thread.replies.first.isBlocked, true);
232
232
expect(thread.replies.first.post.isBlocked, true);
···
254
254
() => mockApi.call(any(), params: any(named: 'params')),
255
255
).thenAnswer((_) async => mockResponse);
256
256
257
-
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1');
257
+
final thread = await repository.getPostThread('at://did:1/app.bsky.feed.post/1', 'did:1');
258
258
259
259
expect(thread.replies.first.isNotFound, true);
260
260
expect(thread.replies.first.post.isNotFound, true);
+7
-7
test/src/features/thread/presentation/thread_screen_test.dart
+7
-7
test/src/features/thread/presentation/thread_screen_test.dart
···
37
37
const testUri = 'at://did:test/app.bsky.feed.post/1';
38
38
39
39
testWidgets('renders thread with focal post highlighted', (tester) async {
40
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
40
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
41
41
(_) async => ThreadViewPost(
42
42
post: ThreadPost(
43
43
uri: testUri,
···
73
73
),
74
74
);
75
75
76
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
76
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
77
77
(_) async => ThreadViewPost(
78
78
post: ThreadPost(
79
79
uri: testUri,
···
98
98
});
99
99
100
100
testWidgets('renders replies correctly', (tester) async {
101
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
101
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
102
102
(_) async => ThreadViewPost(
103
103
post: ThreadPost(
104
104
uri: testUri,
···
129
129
});
130
130
131
131
testWidgets('displays BlockedPostCard for blocked posts', (tester) async {
132
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
132
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
133
133
(_) async => ThreadViewPost(
134
134
post: ThreadPost(
135
135
uri: testUri,
···
159
159
});
160
160
161
161
testWidgets('displays NotFoundPostCard for deleted posts', (tester) async {
162
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
162
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
163
163
(_) async => ThreadViewPost(
164
164
post: ThreadPost(
165
165
uri: testUri,
···
189
189
});
190
190
191
191
testWidgets('displays ThreadgateIndicator when threadgate is present', (tester) async {
192
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
192
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
193
193
(_) async => ThreadViewPost(
194
194
post: ThreadPost(
195
195
uri: testUri,
···
218
218
});
219
219
220
220
testWidgets('toggles between tree and flattened view', (tester) async {
221
-
when(() => mockRepo.getPostThread(testUri)).thenAnswer(
221
+
when(() => mockRepo.getPostThread(testUri, any())).thenAnswer(
222
222
(_) async => ThreadViewPost(
223
223
post: ThreadPost(
224
224
uri: testUri,
+59
-27
test/src/infrastructure/db/daos/bluesky_preferences_dao_test.dart
+59
-27
test/src/infrastructure/db/daos/bluesky_preferences_dao_test.dart
···
6
6
void main() {
7
7
late AppDatabase db;
8
8
late BlueskyPreferencesDao dao;
9
+
const ownerDid = 'did:web:tester';
9
10
10
11
setUp(() {
11
12
db = AppDatabase(NativeDatabase.memory());
···
18
19
19
20
group('getPreferenceByType', () {
20
21
test('returns null for missing type', () async {
21
-
final result = await dao.getPreferenceByType('nonexistent');
22
+
final result = await dao.getPreferenceByType('nonexistent', ownerDid);
22
23
expect(result, isNull);
23
24
});
24
25
···
27
28
type: 'adultContent',
28
29
data: '{"enabled":true}',
29
30
lastSynced: DateTime.now(),
31
+
ownerDid: ownerDid,
30
32
);
31
-
final result = await dao.getPreferenceByType('adultContent');
33
+
final result = await dao.getPreferenceByType('adultContent', ownerDid);
32
34
expect(result, isNotNull);
33
35
expect(result!.type, 'adultContent');
34
36
expect(result.data, '{"enabled":true}');
···
37
39
38
40
group('upsertPreference', () {
39
41
test('inserts a new preference', () async {
40
-
await dao.upsertPreference(type: 'contentLabels', data: '[]', lastSynced: DateTime.now());
41
-
final result = await dao.getPreferenceByType('contentLabels');
42
+
await dao.upsertPreference(
43
+
type: 'contentLabels',
44
+
data: '[]',
45
+
lastSynced: DateTime.now(),
46
+
ownerDid: ownerDid,
47
+
);
48
+
final result = await dao.getPreferenceByType('contentLabels', ownerDid);
42
49
expect(result, isNotNull);
43
50
expect(result!.data, '[]');
44
51
});
···
51
58
type: 'feedView',
52
59
data: '{"hideReplies":false}',
53
60
lastSynced: firstSync,
61
+
ownerDid: ownerDid,
54
62
);
55
63
await dao.upsertPreference(
56
64
type: 'feedView',
57
65
data: '{"hideReplies":true}',
58
66
lastSynced: secondSync,
67
+
ownerDid: ownerDid,
59
68
);
60
69
61
-
final result = await dao.getPreferenceByType('feedView');
70
+
final result = await dao.getPreferenceByType('feedView', ownerDid);
62
71
expect(result, isNotNull);
63
72
expect(result!.data, '{"hideReplies":true}');
64
73
expect(result.lastSynced, secondSync);
···
67
76
68
77
group('getAllPreferences', () {
69
78
test('returns empty list when no preferences exist', () async {
70
-
final result = await dao.getAllPreferences();
79
+
final result = await dao.getAllPreferences(ownerDid);
71
80
expect(result, isEmpty);
72
81
});
73
82
74
83
test('returns all stored preferences', () async {
75
84
final now = DateTime.now();
76
-
await dao.upsertPreference(type: 'adultContent', data: '{"enabled":false}', lastSynced: now);
77
-
await dao.upsertPreference(type: 'threadView', data: '{"sort":"oldest"}', lastSynced: now);
85
+
await dao.upsertPreference(
86
+
type: 'adultContent',
87
+
data: '{"enabled":false}',
88
+
lastSynced: now,
89
+
ownerDid: ownerDid,
90
+
);
91
+
await dao.upsertPreference(
92
+
type: 'threadView',
93
+
data: '{"sort":"oldest"}',
94
+
lastSynced: now,
95
+
ownerDid: ownerDid,
96
+
);
78
97
79
-
final result = await dao.getAllPreferences();
98
+
final result = await dao.getAllPreferences(ownerDid);
80
99
expect(result, hasLength(2));
81
100
expect(result.map((p) => p.type), containsAll(['adultContent', 'threadView']));
82
101
});
···
84
103
85
104
group('watchPreferenceByType', () {
86
105
test('emits null for missing type', () async {
87
-
final result = await dao.watchPreferenceByType('nonexistent').first;
106
+
final result = await dao.watchPreferenceByType('nonexistent', ownerDid).first;
88
107
expect(result, isNull);
89
108
});
90
109
···
93
112
type: 'labelers',
94
113
data: '{"labelers":[]}',
95
114
lastSynced: DateTime.now(),
115
+
ownerDid: ownerDid,
96
116
);
97
-
final result = await dao.watchPreferenceByType('labelers').first;
117
+
final result = await dao.watchPreferenceByType('labelers', ownerDid).first;
98
118
expect(result, isNotNull);
99
119
expect(result!.type, 'labelers');
100
120
});
···
104
124
type: 'mutedWords',
105
125
data: '{"items":[]}',
106
126
lastSynced: DateTime.now(),
127
+
ownerDid: ownerDid,
107
128
);
108
129
109
130
final emissions = <String?>[];
110
131
final subscription = dao
111
-
.watchPreferenceByType('mutedWords')
132
+
.watchPreferenceByType('mutedWords', ownerDid)
112
133
.listen((pref) => emissions.add(pref?.data));
113
134
114
135
await Future<void>.delayed(const Duration(milliseconds: 50));
···
116
137
type: 'mutedWords',
117
138
data: '{"items":[{"id":"1","value":"test"}]}',
118
139
lastSynced: DateTime.now(),
140
+
ownerDid: ownerDid,
119
141
);
120
142
await Future<void>.delayed(const Duration(milliseconds: 50));
121
143
await subscription.cancel();
···
127
149
128
150
group('watchAllPreferences', () {
129
151
test('emits empty list initially', () async {
130
-
final result = await dao.watchAllPreferences().first;
152
+
final result = await dao.watchAllPreferences(ownerDid).first;
131
153
expect(result, isEmpty);
132
154
});
133
155
134
156
test('emits updates when preferences are added', () async {
135
157
final emissions = <int>[];
136
-
final subscription = dao.watchAllPreferences().listen(
137
-
(prefs) => emissions.add(prefs.length),
138
-
);
158
+
final subscription = dao
159
+
.watchAllPreferences(ownerDid)
160
+
.listen((prefs) => emissions.add(prefs.length));
139
161
140
162
await Future<void>.delayed(const Duration(milliseconds: 50));
141
-
await dao.upsertPreference(type: 'adultContent', data: '{}', lastSynced: DateTime.now());
163
+
await dao.upsertPreference(
164
+
type: 'adultContent',
165
+
data: '{}',
166
+
lastSynced: DateTime.now(),
167
+
ownerDid: ownerDid,
168
+
);
142
169
await Future<void>.delayed(const Duration(milliseconds: 50));
143
170
await subscription.cancel();
144
171
···
149
176
150
177
group('deletePreference', () {
151
178
test('removes a preference by type', () async {
152
-
await dao.upsertPreference(type: 'feedView', data: '{}', lastSynced: DateTime.now());
179
+
await dao.upsertPreference(
180
+
type: 'feedView',
181
+
data: '{}',
182
+
lastSynced: DateTime.now(),
183
+
ownerDid: ownerDid,
184
+
);
153
185
154
-
final deleted = await dao.deletePreference('feedView');
186
+
final deleted = await dao.deletePreference('feedView', ownerDid);
155
187
expect(deleted, 1);
156
188
157
-
final result = await dao.getPreferenceByType('feedView');
189
+
final result = await dao.getPreferenceByType('feedView', ownerDid);
158
190
expect(result, isNull);
159
191
});
160
192
161
193
test('returns 0 when type does not exist', () async {
162
-
final deleted = await dao.deletePreference('nonexistent');
194
+
final deleted = await dao.deletePreference('nonexistent', ownerDid);
163
195
expect(deleted, 0);
164
196
});
165
197
});
···
167
199
group('clearAll', () {
168
200
test('removes all preferences', () async {
169
201
final now = DateTime.now();
170
-
await dao.upsertPreference(type: 'type1', data: '{}', lastSynced: now);
171
-
await dao.upsertPreference(type: 'type2', data: '{}', lastSynced: now);
172
-
await dao.upsertPreference(type: 'type3', data: '{}', lastSynced: now);
202
+
await dao.upsertPreference(type: 'type1', data: '{}', lastSynced: now, ownerDid: ownerDid);
203
+
await dao.upsertPreference(type: 'type2', data: '{}', lastSynced: now, ownerDid: ownerDid);
204
+
await dao.upsertPreference(type: 'type3', data: '{}', lastSynced: now, ownerDid: ownerDid);
173
205
174
-
final cleared = await dao.clearAll();
206
+
final cleared = await dao.clearAll(ownerDid);
175
207
expect(cleared, 3);
176
208
177
-
final result = await dao.getAllPreferences();
209
+
final result = await dao.getAllPreferences(ownerDid);
178
210
expect(result, isEmpty);
179
211
});
180
212
181
213
test('returns 0 when no preferences exist', () async {
182
-
final cleared = await dao.clearAll();
214
+
final cleared = await dao.clearAll(ownerDid);
183
215
expect(cleared, 0);
184
216
});
185
217
});
+28
-11
test/src/infrastructure/db/daos/dm_convos_dao_test.dart
+28
-11
test/src/infrastructure/db/daos/dm_convos_dao_test.dart
···
7
7
void main() {
8
8
late AppDatabase database;
9
9
late DmConvosDao dao;
10
+
const ownerDid = 'did:web:tester';
10
11
11
12
setUp(() {
12
13
database = AppDatabase(NativeDatabase.memory());
···
35
36
lastMessageText: const Value('Hello!'),
36
37
lastMessageAt: Value(DateTime.now()),
37
38
cachedAt: DateTime.now(),
39
+
ownerDid: ownerDid,
38
40
),
39
41
];
40
42
41
43
await dao.insertConvosBatch(newConvos: convos, newProfiles: profiles);
42
44
43
-
final results = await dao.watchConversations().first;
45
+
final results = await dao.watchConversations(ownerDid).first;
44
46
expect(results, hasLength(1));
45
47
expect(results.first.convo.convoId, 'convo1');
46
48
expect(results.first.convo.lastMessageText, 'Hello!');
···
59
61
lastMessageText: const Value('First message'),
60
62
unreadCount: const Value(1),
61
63
cachedAt: DateTime.now(),
64
+
ownerDid: ownerDid,
62
65
);
63
66
64
67
await dao.insertConvosBatch(newConvos: [convo1], newProfiles: profiles);
···
69
72
lastMessageText: const Value('Updated message'),
70
73
unreadCount: const Value(3),
71
74
cachedAt: DateTime.now(),
75
+
ownerDid: ownerDid,
72
76
);
73
77
74
78
await dao.insertConvosBatch(newConvos: [convo2], newProfiles: profiles);
75
79
76
-
final results = await dao.watchConversations().first;
80
+
final results = await dao.watchConversations(ownerDid).first;
77
81
expect(results, hasLength(1));
78
82
expect(results.first.convo.lastMessageText, 'Updated message');
79
83
expect(results.first.convo.unreadCount, 3);
···
93
97
membersJson: '["did:plc:member1"]',
94
98
lastMessageAt: Value(now.subtract(const Duration(hours: 2))),
95
99
cachedAt: now,
100
+
ownerDid: ownerDid,
96
101
),
97
102
DmConvosCompanion.insert(
98
103
convoId: 'convo2',
99
104
membersJson: '["did:plc:member1"]',
100
105
lastMessageAt: Value(now),
101
106
cachedAt: now,
107
+
ownerDid: ownerDid,
102
108
),
103
109
DmConvosCompanion.insert(
104
110
convoId: 'convo3',
105
111
membersJson: '["did:plc:member1"]',
106
112
lastMessageAt: Value(now.subtract(const Duration(hours: 1))),
107
113
cachedAt: now,
114
+
ownerDid: ownerDid,
108
115
),
109
116
];
110
117
111
118
await dao.insertConvosBatch(newConvos: convos, newProfiles: profiles);
112
119
113
-
final results = await dao.watchConversations().first;
120
+
final results = await dao.watchConversations(ownerDid).first;
114
121
expect(results, hasLength(3));
115
122
expect(results[0].convo.convoId, 'convo2');
116
123
expect(results[1].convo.convoId, 'convo3');
···
130
137
convoId: 'convo1',
131
138
membersJson: '["did:plc:member1"]',
132
139
cachedAt: DateTime.now(),
140
+
ownerDid: ownerDid,
133
141
),
134
142
],
135
143
newProfiles: profiles,
136
144
);
137
145
138
-
final result = await dao.getConvo('convo1');
146
+
final result = await dao.getConvo('convo1', ownerDid);
139
147
expect(result, isNotNull);
140
148
expect(result!.convo.convoId, 'convo1');
141
149
});
142
150
143
151
test('returns null for non-existent conversation', () async {
144
-
final result = await dao.getConvo('nonexistent');
152
+
final result = await dao.getConvo('nonexistent', ownerDid);
145
153
expect(result, isNull);
146
154
});
147
155
});
···
159
167
membersJson: '["did:plc:member1"]',
160
168
unreadCount: const Value(5),
161
169
cachedAt: DateTime.now(),
170
+
ownerDid: ownerDid,
162
171
),
163
172
],
164
173
newProfiles: profiles,
165
174
);
166
175
167
-
await dao.updateReadState(convoId: 'convo1', lastReadMessageId: 'msg123', unreadCount: 0);
176
+
await dao.updateReadState(
177
+
convoId: 'convo1',
178
+
lastReadMessageId: 'msg123',
179
+
unreadCount: 0,
180
+
ownerDid: ownerDid,
181
+
);
168
182
169
-
final result = await dao.getConvo('convo1');
183
+
final result = await dao.getConvo('convo1', ownerDid);
170
184
expect(result!.convo.lastReadMessageId, 'msg123');
171
185
expect(result.convo.unreadCount, 0);
172
186
});
···
185
199
membersJson: '["did:plc:member1"]',
186
200
isAccepted: const Value(false),
187
201
cachedAt: DateTime.now(),
202
+
ownerDid: ownerDid,
188
203
),
189
204
],
190
205
newProfiles: profiles,
191
206
);
192
207
193
-
await dao.acceptConvo('convo1');
208
+
await dao.acceptConvo('convo1', ownerDid);
194
209
195
-
final result = await dao.getConvo('convo1');
210
+
final result = await dao.getConvo('convo1', ownerDid);
196
211
expect(result!.convo.isAccepted, isTrue);
197
212
});
198
213
});
···
209
224
convoId: 'convo1',
210
225
membersJson: '["did:plc:member1"]',
211
226
cachedAt: DateTime.now(),
227
+
ownerDid: ownerDid,
212
228
),
213
229
DmConvosCompanion.insert(
214
230
convoId: 'convo2',
215
231
membersJson: '["did:plc:member1"]',
216
232
cachedAt: DateTime.now(),
233
+
ownerDid: ownerDid,
217
234
),
218
235
],
219
236
newProfiles: profiles,
220
237
);
221
238
222
-
await dao.clearConversations();
239
+
await dao.clearConversations(ownerDid);
223
240
224
-
final results = await dao.watchConversations().first;
241
+
final results = await dao.watchConversations(ownerDid).first;
225
242
expect(results, isEmpty);
226
243
});
227
244
});
+29
-13
test/src/infrastructure/db/daos/dm_messages_dao_test.dart
+29
-13
test/src/infrastructure/db/daos/dm_messages_dao_test.dart
···
7
7
void main() {
8
8
late AppDatabase database;
9
9
late DmMessagesDao dao;
10
+
const ownerDid = 'did:web:tester';
10
11
11
12
setUp(() {
12
13
database = AppDatabase(NativeDatabase.memory());
···
37
38
sentAt: DateTime.now(),
38
39
status: 'sent',
39
40
cachedAt: DateTime.now(),
41
+
ownerDid: ownerDid,
40
42
),
41
43
];
42
44
43
45
await dao.insertMessagesBatch(newMessages: messages, newProfiles: profiles);
44
46
45
-
final results = await dao.watchMessagesByConvo('convo1').first;
47
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
46
48
expect(results, hasLength(1));
47
49
expect(results.first.message.content, 'Hello!');
48
50
expect(results.first.sender.handle, 'sender1.bsky.social');
···
61
63
sentAt: DateTime.now(),
62
64
status: 'pending',
63
65
cachedAt: DateTime.now(),
66
+
ownerDid: ownerDid,
64
67
);
65
68
66
69
await dao.insertMessagesBatch(newMessages: [msg1], newProfiles: profiles);
···
73
76
sentAt: DateTime.now(),
74
77
status: 'sent',
75
78
cachedAt: DateTime.now(),
79
+
ownerDid: ownerDid,
76
80
);
77
81
78
82
await dao.insertMessagesBatch(newMessages: [msg2], newProfiles: profiles);
79
83
80
-
final results = await dao.watchMessagesByConvo('convo1').first;
84
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
81
85
expect(results, hasLength(1));
82
86
expect(results.first.message.content, 'Updated');
83
87
expect(results.first.message.status, 'sent');
···
100
104
sentAt: now,
101
105
status: 'sent',
102
106
cachedAt: now,
107
+
ownerDid: ownerDid,
103
108
),
104
109
DmMessagesCompanion.insert(
105
110
messageId: 'msg1',
···
109
114
sentAt: now.subtract(const Duration(hours: 2)),
110
115
status: 'sent',
111
116
cachedAt: now,
117
+
ownerDid: ownerDid,
112
118
),
113
119
DmMessagesCompanion.insert(
114
120
messageId: 'msg2',
···
118
124
sentAt: now.subtract(const Duration(hours: 1)),
119
125
status: 'sent',
120
126
cachedAt: now,
127
+
ownerDid: ownerDid,
121
128
),
122
129
];
123
130
124
131
await dao.insertMessagesBatch(newMessages: messages, newProfiles: profiles);
125
132
126
-
final results = await dao.watchMessagesByConvo('convo1').first;
133
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
127
134
expect(results, hasLength(3));
128
135
expect(results[0].message.content, 'First');
129
136
expect(results[1].message.content, 'Second');
···
145
152
sentAt: DateTime.now(),
146
153
status: 'sent',
147
154
cachedAt: DateTime.now(),
155
+
ownerDid: ownerDid,
148
156
),
149
157
DmMessagesCompanion.insert(
150
158
messageId: 'msg2',
···
154
162
sentAt: DateTime.now(),
155
163
status: 'sent',
156
164
cachedAt: DateTime.now(),
165
+
ownerDid: ownerDid,
157
166
),
158
167
],
159
168
newProfiles: profiles,
160
169
);
161
170
162
-
final results = await dao.watchMessagesByConvo('convo1').first;
171
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
163
172
expect(results, hasLength(1));
164
173
expect(results.first.message.content, 'Convo 1 message');
165
174
});
···
181
190
sentAt: DateTime.now(),
182
191
status: 'pending',
183
192
cachedAt: DateTime.now(),
193
+
ownerDid: ownerDid,
184
194
),
185
195
],
186
196
newProfiles: profiles,
187
197
);
188
198
189
-
await dao.updateMessageStatus(messageId: 'msg1', status: 'sent');
199
+
await dao.updateMessageStatus(messageId: 'msg1', status: 'sent', ownerDid: ownerDid);
190
200
191
-
final results = await dao.watchMessagesByConvo('convo1').first;
201
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
192
202
expect(results.first.message.status, 'sent');
193
203
});
194
204
});
···
210
220
sentAt: now.subtract(const Duration(hours: 1)),
211
221
status: 'sent',
212
222
cachedAt: now,
223
+
ownerDid: ownerDid,
213
224
),
214
225
DmMessagesCompanion.insert(
215
226
messageId: 'msg2',
···
219
230
sentAt: now,
220
231
status: 'sent',
221
232
cachedAt: now,
233
+
ownerDid: ownerDid,
222
234
),
223
235
],
224
236
newProfiles: profiles,
225
237
);
226
238
227
-
final result = await dao.getLatestMessage('convo1');
239
+
final result = await dao.getLatestMessage('convo1', ownerDid);
228
240
expect(result, isNotNull);
229
241
expect(result!.content, 'Latest');
230
242
});
231
243
232
244
test('returns null for empty conversation', () async {
233
-
final result = await dao.getLatestMessage('nonexistent');
245
+
final result = await dao.getLatestMessage('nonexistent', ownerDid);
234
246
expect(result, isNull);
235
247
});
236
248
});
···
251
263
sentAt: DateTime.now(),
252
264
status: 'sent',
253
265
cachedAt: DateTime.now(),
266
+
ownerDid: ownerDid,
254
267
),
255
268
DmMessagesCompanion.insert(
256
269
messageId: 'msg2',
···
260
273
sentAt: DateTime.now(),
261
274
status: 'sent',
262
275
cachedAt: DateTime.now(),
276
+
ownerDid: ownerDid,
263
277
),
264
278
DmMessagesCompanion.insert(
265
279
messageId: 'msg3',
···
269
283
sentAt: DateTime.now(),
270
284
status: 'sent',
271
285
cachedAt: DateTime.now(),
286
+
ownerDid: ownerDid,
272
287
),
273
288
],
274
289
newProfiles: profiles,
275
290
);
276
291
277
-
final deletedCount = await dao.deleteMessagesByConvo('convo1');
292
+
final deletedCount = await dao.deleteMessagesByConvo('convo1', ownerDid);
278
293
expect(deletedCount, 2);
279
294
280
-
final convo1Results = await dao.watchMessagesByConvo('convo1').first;
295
+
final convo1Results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
281
296
expect(convo1Results, isEmpty);
282
297
283
-
final convo2Results = await dao.watchMessagesByConvo('convo2').first;
298
+
final convo2Results = await dao.watchMessagesByConvo('convo2', ownerDid).first;
284
299
expect(convo2Results, hasLength(1));
285
300
});
286
301
});
···
301
316
sentAt: DateTime.now(),
302
317
status: 'sent',
303
318
cachedAt: DateTime.now(),
319
+
ownerDid: ownerDid,
304
320
),
305
321
],
306
322
newProfiles: profiles,
307
323
);
308
324
309
-
await dao.clearMessages();
325
+
await dao.clearMessages(ownerDid);
310
326
311
-
final results = await dao.watchMessagesByConvo('convo1').first;
327
+
final results = await dao.watchMessagesByConvo('convo1', ownerDid).first;
312
328
expect(results, isEmpty);
313
329
});
314
330
});
+29
-8
test/src/infrastructure/db/daos/dm_outbox_dao_test.dart
+29
-8
test/src/infrastructure/db/daos/dm_outbox_dao_test.dart
···
7
7
void main() {
8
8
late AppDatabase database;
9
9
late DmOutboxDao dao;
10
+
const ownerDid = 'did:web:tester';
10
11
11
12
setUp(() {
12
13
database = AppDatabase(NativeDatabase.memory());
···
27
28
messageText: 'Hello!',
28
29
status: 'pending',
29
30
createdAt: DateTime.now(),
31
+
ownerDid: ownerDid,
30
32
),
31
33
);
32
34
33
-
final pending = await dao.getPending();
35
+
final pending = await dao.getPending(ownerDid);
34
36
expect(pending, hasLength(1));
35
37
expect(pending.first.outboxId, 'outbox1');
36
38
expect(pending.first.messageText, 'Hello!');
···
49
51
messageText: 'Second',
50
52
status: 'pending',
51
53
createdAt: now,
54
+
ownerDid: ownerDid,
52
55
),
53
56
);
54
57
await dao.enqueue(
···
58
61
messageText: 'First',
59
62
status: 'pending',
60
63
createdAt: now.subtract(const Duration(minutes: 1)),
64
+
ownerDid: ownerDid,
61
65
),
62
66
);
63
67
64
-
final pending = await dao.getPending();
68
+
final pending = await dao.getPending(ownerDid);
65
69
expect(pending, hasLength(2));
66
70
expect(pending[0].outboxId, 'outbox1');
67
71
expect(pending[1].outboxId, 'outbox2');
···
75
79
messageText: 'Pending',
76
80
status: 'pending',
77
81
createdAt: DateTime.now(),
82
+
ownerDid: ownerDid,
78
83
),
79
84
);
80
85
await dao.enqueue(
···
84
89
messageText: 'Failed',
85
90
status: 'failed',
86
91
createdAt: DateTime.now(),
92
+
ownerDid: ownerDid,
87
93
),
88
94
);
89
95
90
-
final pending = await dao.getPending();
96
+
final pending = await dao.getPending(ownerDid);
91
97
expect(pending, hasLength(1));
92
98
expect(pending.first.outboxId, 'outbox1');
93
99
});
···
102
108
messageText: 'Pending',
103
109
status: 'pending',
104
110
createdAt: DateTime.now(),
111
+
ownerDid: ownerDid,
105
112
),
106
113
);
107
114
await dao.enqueue(
···
111
118
messageText: 'Failed',
112
119
status: 'failed',
113
120
createdAt: DateTime.now(),
121
+
ownerDid: ownerDid,
114
122
),
115
123
);
116
124
117
-
final failed = await dao.getFailed();
125
+
final failed = await dao.getFailed(ownerDid);
118
126
expect(failed, hasLength(1));
119
127
expect(failed.first.outboxId, 'outbox2');
120
128
});
···
129
137
messageText: 'Test',
130
138
status: 'pending',
131
139
createdAt: DateTime.now(),
140
+
ownerDid: ownerDid,
132
141
),
133
142
);
134
143
···
152
161
messageText: 'Pending',
153
162
status: 'pending',
154
163
createdAt: DateTime.now(),
164
+
ownerDid: ownerDid,
155
165
),
156
166
);
157
167
await dao.enqueue(
···
161
171
messageText: 'Sending',
162
172
status: 'sending',
163
173
createdAt: DateTime.now(),
174
+
ownerDid: ownerDid,
164
175
),
165
176
);
166
177
await dao.enqueue(
···
170
181
messageText: 'Other convo',
171
182
status: 'pending',
172
183
createdAt: DateTime.now(),
184
+
ownerDid: ownerDid,
173
185
),
174
186
);
175
187
176
-
final results = await dao.getByConvo('convo1');
188
+
final results = await dao.getByConvo('convo1', ownerDid);
177
189
expect(results, hasLength(2));
178
190
});
179
191
});
···
187
199
messageText: 'Test',
188
200
status: 'pending',
189
201
createdAt: DateTime.now(),
202
+
ownerDid: ownerDid,
190
203
),
191
204
);
192
205
···
205
218
messageText: 'Test',
206
219
status: 'pending',
207
220
createdAt: DateTime.now(),
221
+
ownerDid: ownerDid,
208
222
),
209
223
);
210
224
···
230
244
status: 'pending',
231
245
retryCount: const Value(0),
232
246
createdAt: DateTime.now(),
247
+
ownerDid: ownerDid,
233
248
),
234
249
);
235
250
···
249
264
messageText: 'Test',
250
265
status: 'failed',
251
266
createdAt: DateTime.now(),
267
+
ownerDid: ownerDid,
252
268
),
253
269
);
254
270
await dao.updateStatus(outboxId: 'outbox1', status: 'failed', errorMessage: 'Error');
···
270
286
messageText: 'Test',
271
287
status: 'pending',
272
288
createdAt: DateTime.now(),
289
+
ownerDid: ownerDid,
273
290
),
274
291
);
275
292
···
290
307
messageText: 'Pending 1',
291
308
status: 'pending',
292
309
createdAt: DateTime.now(),
310
+
ownerDid: ownerDid,
293
311
),
294
312
);
295
313
await dao.enqueue(
···
299
317
messageText: 'Pending 2',
300
318
status: 'pending',
301
319
createdAt: DateTime.now(),
320
+
ownerDid: ownerDid,
302
321
),
303
322
);
304
323
await dao.enqueue(
···
308
327
messageText: 'Failed',
309
328
status: 'failed',
310
329
createdAt: DateTime.now(),
330
+
ownerDid: ownerDid,
311
331
),
312
332
);
313
333
314
-
final count = await dao.countPending();
334
+
final count = await dao.countPending(ownerDid);
315
335
expect(count, 2);
316
336
});
317
337
});
···
325
345
messageText: 'Test',
326
346
status: 'pending',
327
347
createdAt: DateTime.now(),
348
+
ownerDid: ownerDid,
328
349
),
329
350
);
330
351
331
-
await dao.clearOutbox();
352
+
await dao.clearOutbox(ownerDid);
332
353
333
-
final pending = await dao.getPending();
354
+
final pending = await dao.getPending(ownerDid);
334
355
expect(pending, isEmpty);
335
356
});
336
357
});
+52
-16
test/src/infrastructure/db/daos/notifications_dao_test.dart
+52
-16
test/src/infrastructure/db/daos/notifications_dao_test.dart
···
7
7
void main() {
8
8
late AppDatabase database;
9
9
late NotificationsDao dao;
10
+
const ownerDid = 'did:plc:owner';
10
11
11
12
setUp(() {
12
13
database = AppDatabase(NativeDatabase.memory());
···
32
33
NotificationsCompanion.insert(
33
34
uri: 'at://did:plc:user/app.bsky.notification/1',
34
35
actorDid: 'did:plc:actor1',
36
+
ownerDid: ownerDid,
35
37
type: 'like',
36
38
indexedAt: DateTime.now(),
37
39
cachedAt: DateTime.now(),
38
40
),
39
41
];
40
42
41
-
await dao.insertNotificationsBatch(newNotifications: notifications, newProfiles: profiles);
43
+
await dao.insertNotificationsBatch(
44
+
newNotifications: notifications,
45
+
newProfiles: profiles,
46
+
newCursor: null,
47
+
ownerDid: ownerDid,
48
+
);
42
49
43
-
final results = await dao.watchNotifications().first;
50
+
final results = await dao.watchNotifications(ownerDid).first;
44
51
expect(results, hasLength(1));
45
52
expect(results.first.notification.type, 'like');
46
53
expect(results.first.actor.handle, 'actor1.bsky.social');
···
51
58
newNotifications: [],
52
59
newProfiles: [],
53
60
newCursor: 'cursor123',
61
+
ownerDid: ownerDid,
54
62
);
55
63
56
-
final cursor = await dao.getCursor();
64
+
final cursor = await dao.getCursor(ownerDid);
57
65
expect(cursor, 'cursor123');
58
66
});
59
67
···
65
73
final notification1 = NotificationsCompanion.insert(
66
74
uri: 'at://did:plc:user/app.bsky.notification/1',
67
75
actorDid: 'did:plc:actor1',
76
+
ownerDid: ownerDid,
68
77
type: 'like',
69
78
isRead: const Value(false),
70
79
indexedAt: DateTime.now(),
···
74
83
await dao.insertNotificationsBatch(
75
84
newNotifications: [notification1],
76
85
newProfiles: profiles,
86
+
newCursor: null,
87
+
ownerDid: ownerDid,
77
88
);
78
89
79
90
final notification2 = NotificationsCompanion.insert(
80
91
uri: 'at://did:plc:user/app.bsky.notification/1',
81
92
actorDid: 'did:plc:actor1',
93
+
ownerDid: ownerDid,
82
94
type: 'like',
83
95
isRead: const Value(true),
84
96
indexedAt: DateTime.now(),
···
88
100
await dao.insertNotificationsBatch(
89
101
newNotifications: [notification2],
90
102
newProfiles: profiles,
103
+
newCursor: null,
104
+
ownerDid: ownerDid,
91
105
);
92
106
93
-
final results = await dao.watchNotifications().first;
107
+
final results = await dao.watchNotifications(ownerDid).first;
94
108
expect(results, hasLength(1));
95
109
expect(results.first.notification.isRead, isTrue);
96
110
});
···
107
121
NotificationsCompanion.insert(
108
122
uri: 'at://did:plc:user/app.bsky.notification/1',
109
123
actorDid: 'did:plc:actor1',
124
+
ownerDid: ownerDid,
110
125
type: 'like',
111
126
indexedAt: now.subtract(const Duration(hours: 2)),
112
127
cachedAt: now,
···
114
129
NotificationsCompanion.insert(
115
130
uri: 'at://did:plc:user/app.bsky.notification/2',
116
131
actorDid: 'did:plc:actor1',
132
+
ownerDid: ownerDid,
117
133
type: 'follow',
118
134
indexedAt: now.subtract(const Duration(hours: 1)),
119
135
cachedAt: now,
···
121
137
NotificationsCompanion.insert(
122
138
uri: 'at://did:plc:user/app.bsky.notification/3',
123
139
actorDid: 'did:plc:actor1',
140
+
ownerDid: ownerDid,
124
141
type: 'repost',
125
142
indexedAt: now,
126
143
cachedAt: now,
127
144
),
128
145
];
129
146
130
-
await dao.insertNotificationsBatch(newNotifications: notifications, newProfiles: profiles);
147
+
await dao.insertNotificationsBatch(
148
+
newNotifications: notifications,
149
+
newProfiles: profiles,
150
+
newCursor: null,
151
+
ownerDid: ownerDid,
152
+
);
131
153
132
-
final results = await dao.watchNotifications().first;
154
+
final results = await dao.watchNotifications(ownerDid).first;
133
155
expect(results, hasLength(3));
134
156
expect(results[0].notification.type, 'repost'); // Most recent
135
157
expect(results[1].notification.type, 'follow');
···
141
163
ProfilesCompanion.insert(did: 'did:plc:actor1', handle: 'actor1.bsky.social'),
142
164
];
143
165
144
-
final stream = dao.watchNotifications();
166
+
final stream = dao.watchNotifications(ownerDid);
145
167
final emissions = <List<NotificationWithActor>>[];
146
168
final subscription = stream.listen(emissions.add);
147
169
···
152
174
NotificationsCompanion.insert(
153
175
uri: 'at://did:plc:user/app.bsky.notification/1',
154
176
actorDid: 'did:plc:actor1',
177
+
ownerDid: ownerDid,
155
178
type: 'like',
156
179
indexedAt: DateTime.now(),
157
180
cachedAt: DateTime.now(),
158
181
),
159
182
],
160
183
newProfiles: profiles,
184
+
newCursor: null,
185
+
ownerDid: ownerDid,
161
186
);
162
187
163
188
await Future<void>.delayed(const Duration(milliseconds: 50));
···
172
197
173
198
group('getCursor', () {
174
199
test('returns null when no cursor exists', () async {
175
-
final cursor = await dao.getCursor();
200
+
final cursor = await dao.getCursor(ownerDid);
176
201
expect(cursor, isNull);
177
202
});
178
203
···
181
206
newNotifications: [],
182
207
newProfiles: [],
183
208
newCursor: 'test_cursor',
209
+
ownerDid: ownerDid,
184
210
);
185
211
186
-
final cursor = await dao.getCursor();
212
+
final cursor = await dao.getCursor(ownerDid);
187
213
expect(cursor, 'test_cursor');
188
214
});
189
215
});
···
199
225
NotificationsCompanion.insert(
200
226
uri: 'at://did:plc:user/app.bsky.notification/1',
201
227
actorDid: 'did:plc:actor1',
228
+
ownerDid: ownerDid,
202
229
type: 'like',
203
230
indexedAt: DateTime.now(),
204
231
cachedAt: DateTime.now(),
···
206
233
],
207
234
newProfiles: profiles,
208
235
newCursor: 'cursor123',
236
+
ownerDid: ownerDid,
209
237
);
210
238
211
-
await dao.clearNotifications();
239
+
await dao.clearNotifications(ownerDid);
212
240
213
-
final results = await dao.watchNotifications().first;
241
+
final results = await dao.watchNotifications(ownerDid).first;
214
242
expect(results, isEmpty);
215
243
216
-
final cursor = await dao.getCursor();
244
+
final cursor = await dao.getCursor(ownerDid);
217
245
expect(cursor, isNull);
218
246
});
219
247
});
···
233
261
NotificationsCompanion.insert(
234
262
uri: 'at://did:plc:user/app.bsky.notification/old',
235
263
actorDid: 'did:plc:actor1',
264
+
ownerDid: ownerDid,
236
265
type: 'like',
237
266
indexedAt: stale,
238
267
cachedAt: stale,
···
240
269
NotificationsCompanion.insert(
241
270
uri: 'at://did:plc:user/app.bsky.notification/new',
242
271
actorDid: 'did:plc:actor1',
272
+
ownerDid: ownerDid,
243
273
type: 'follow',
244
274
indexedAt: fresh,
245
275
cachedAt: fresh,
246
276
),
247
277
],
248
278
newProfiles: profiles,
279
+
newCursor: null,
280
+
ownerDid: ownerDid,
249
281
);
250
282
251
283
final threshold = now.subtract(const Duration(days: 30));
252
-
final deletedCount = await dao.deleteStaleNotifications(threshold);
284
+
final deletedCount = await dao.deleteStaleNotifications(threshold, ownerDid);
253
285
254
286
expect(deletedCount, 1);
255
287
256
-
final results = await dao.watchNotifications().first;
288
+
final results = await dao.watchNotifications(ownerDid).first;
257
289
expect(results, hasLength(1));
258
290
expect(results.first.notification.type, 'follow');
259
291
});
···
270
302
NotificationsCompanion.insert(
271
303
uri: 'at://did:plc:user/app.bsky.notification/1',
272
304
actorDid: 'did:plc:actor1',
305
+
ownerDid: ownerDid,
273
306
type: 'like',
274
307
isRead: const Value(false),
275
308
indexedAt: DateTime.now(),
···
278
311
NotificationsCompanion.insert(
279
312
uri: 'at://did:plc:user/app.bsky.notification/2',
280
313
actorDid: 'did:plc:actor1',
314
+
ownerDid: ownerDid,
281
315
type: 'follow',
282
316
isRead: const Value(false),
283
317
indexedAt: DateTime.now(),
···
285
319
),
286
320
],
287
321
newProfiles: profiles,
322
+
newCursor: null,
323
+
ownerDid: ownerDid,
288
324
);
289
325
290
-
await dao.markAllAsRead();
326
+
await dao.markAllAsRead(ownerDid);
291
327
292
-
final results = await dao.watchNotifications().first;
328
+
final results = await dao.watchNotifications(ownerDid).first;
293
329
expect(results, hasLength(2));
294
330
expect(results.every((n) => n.notification.isRead), isTrue);
295
331
});
+42
-35
test/src/infrastructure/db/daos/notifications_sync_queue_dao_test.dart
+42
-35
test/src/infrastructure/db/daos/notifications_sync_queue_dao_test.dart
···
8
8
group('NotificationsSyncQueueDao', () {
9
9
late AppDatabase db;
10
10
late NotificationsSyncQueueDao dao;
11
+
const ownerDid = 'did:web:tester';
11
12
12
13
setUp(() {
13
14
db = AppDatabase(NativeDatabase.memory());
···
20
21
21
22
test('enqueueMarkSeen adds item to queue', () async {
22
23
final now = DateTime.now();
23
-
await dao.enqueueMarkSeen(now);
24
+
await dao.enqueueMarkSeen(now, ownerDid);
24
25
25
-
final pending = await dao.getRetryableItems();
26
+
final pending = await dao.getRetryableItems(ownerDid);
26
27
expect(pending.length, 1);
27
28
expect(pending.first.type, 'mark_seen');
28
29
expect(pending.first.seenAt, now.toIso8601String());
···
33
34
final now = DateTime.now();
34
35
final earlier = now.subtract(const Duration(hours: 1));
35
36
36
-
await dao.enqueueMarkSeen(earlier);
37
-
await dao.enqueueMarkSeen(now);
37
+
await dao.enqueueMarkSeen(earlier, ownerDid);
38
+
await dao.enqueueMarkSeen(now, ownerDid);
38
39
39
-
final pending = await dao.getRetryableItems();
40
+
final pending = await dao.getRetryableItems(ownerDid);
40
41
expect(pending.length, 2);
41
42
expect(DateTime.parse(pending[0].seenAt), earlier);
42
43
expect(DateTime.parse(pending[1].seenAt), now);
···
47
48
final earlier = now.subtract(const Duration(hours: 1));
48
49
final earliest = now.subtract(const Duration(hours: 2));
49
50
50
-
await dao.enqueueMarkSeen(earliest);
51
-
await dao.enqueueMarkSeen(earlier);
52
-
await dao.enqueueMarkSeen(now);
51
+
await dao.enqueueMarkSeen(earliest, ownerDid);
52
+
await dao.enqueueMarkSeen(earlier, ownerDid);
53
+
await dao.enqueueMarkSeen(now, ownerDid);
53
54
54
-
final latest = await dao.getLatestSeenAt();
55
+
final latest = await dao.getLatestSeenAt(ownerDid);
55
56
expect(latest, now);
56
57
});
57
58
58
59
test('getLatestSeenAt returns null when queue is empty', () async {
59
-
final latest = await dao.getLatestSeenAt();
60
+
final latest = await dao.getLatestSeenAt(ownerDid);
60
61
expect(latest, null);
61
62
});
62
63
···
64
65
final now = DateTime.now();
65
66
final older = now.subtract(const Duration(hours: 1));
66
67
67
-
await dao.enqueueMarkSeen(older);
68
+
await dao.enqueueMarkSeen(older, ownerDid);
68
69
69
70
await db
70
71
.into(db.notificationsSyncQueue)
···
74
75
seenAt: now.toIso8601String(),
75
76
createdAt: DateTime.now(),
76
77
retryCount: const Value(kMaxNotificationSyncRetries),
78
+
ownerDid: ownerDid,
77
79
),
78
80
);
79
81
80
-
final latest = await dao.getLatestSeenAt();
82
+
final latest = await dao.getLatestSeenAt(ownerDid);
81
83
expect(latest, older);
82
84
});
83
85
84
86
test('deleteItem removes specific item', () async {
85
87
final now = DateTime.now();
86
-
await dao.enqueueMarkSeen(now);
88
+
await dao.enqueueMarkSeen(now, ownerDid);
87
89
88
-
var pending = await dao.getRetryableItems();
90
+
var pending = await dao.getRetryableItems(ownerDid);
89
91
final id = pending.first.id;
90
92
91
93
await dao.deleteItem(id);
92
94
93
-
pending = await dao.getRetryableItems();
95
+
pending = await dao.getRetryableItems(ownerDid);
94
96
expect(pending, isEmpty);
95
97
});
96
98
···
100
102
final earliest = now.subtract(const Duration(hours: 2));
101
103
final later = now.add(const Duration(hours: 1));
102
104
103
-
await dao.enqueueMarkSeen(earliest);
104
-
await dao.enqueueMarkSeen(earlier);
105
-
await dao.enqueueMarkSeen(now);
106
-
await dao.enqueueMarkSeen(later);
105
+
await dao.enqueueMarkSeen(earliest, ownerDid);
106
+
await dao.enqueueMarkSeen(earlier, ownerDid);
107
+
await dao.enqueueMarkSeen(now, ownerDid);
108
+
await dao.enqueueMarkSeen(later, ownerDid);
107
109
108
-
await dao.deleteItemsUpTo(now);
110
+
await dao.deleteItemsUpTo(now, ownerDid);
109
111
110
-
final remaining = await dao.getRetryableItems();
112
+
final remaining = await dao.getRetryableItems(ownerDid);
111
113
expect(remaining.length, 1);
112
114
expect(DateTime.parse(remaining.first.seenAt), later);
113
115
});
114
116
115
117
test('clearQueue removes all items', () async {
116
118
final now = DateTime.now();
117
-
await dao.enqueueMarkSeen(now);
118
-
await dao.enqueueMarkSeen(now.add(const Duration(hours: 1)));
119
+
await dao.enqueueMarkSeen(now, ownerDid);
120
+
await dao.enqueueMarkSeen(now.add(const Duration(hours: 1)), ownerDid);
119
121
120
-
await dao.clearQueue();
122
+
await dao.clearQueue(ownerDid);
121
123
122
-
final pending = await dao.getRetryableItems();
124
+
final pending = await dao.getRetryableItems(ownerDid);
123
125
expect(pending, isEmpty);
124
126
});
125
127
···
129
131
() async {
130
132
final now = DateTime.now();
131
133
132
-
await dao.enqueueMarkSeen(now);
134
+
await dao.enqueueMarkSeen(now, ownerDid);
133
135
134
136
await db
135
137
.into(db.notificationsSyncQueue)
···
139
141
seenAt: now.subtract(const Duration(hours: 1)).toIso8601String(),
140
142
createdAt: now,
141
143
retryCount: const Value(4),
144
+
ownerDid: ownerDid,
142
145
),
143
146
);
144
147
···
150
153
seenAt: now.subtract(const Duration(hours: 2)).toIso8601String(),
151
154
createdAt: now,
152
155
retryCount: const Value(5),
156
+
ownerDid: ownerDid,
153
157
),
154
158
);
155
159
156
-
final retryable = await dao.getRetryableItems();
160
+
final retryable = await dao.getRetryableItems(ownerDid);
157
161
expect(retryable.length, 2);
158
162
expect(retryable.map((r) => r.retryCount), containsAll([0, 4]));
159
163
},
···
161
165
162
166
test('incrementRetryCount updates the retry count', () async {
163
167
final now = DateTime.now();
164
-
final id = await dao.enqueueMarkSeen(now);
168
+
final id = await dao.enqueueMarkSeen(now, ownerDid);
165
169
166
-
var items = await dao.getRetryableItems();
170
+
var items = await dao.getRetryableItems(ownerDid);
167
171
expect(items.first.retryCount, 0);
168
172
169
173
await dao.incrementRetryCount(id);
170
174
171
-
items = await dao.getRetryableItems();
175
+
items = await dao.getRetryableItems(ownerDid);
172
176
expect(items.first.retryCount, 1);
173
177
174
178
await dao.incrementRetryCount(id);
175
179
await dao.incrementRetryCount(id);
176
180
177
-
items = await dao.getRetryableItems();
181
+
items = await dao.getRetryableItems(ownerDid);
178
182
expect(items.first.retryCount, 3);
179
183
});
180
184
···
190
194
seenAt: now.subtract(const Duration(days: 50)).toIso8601String(),
191
195
createdAt: now.subtract(const Duration(days: 45)),
192
196
retryCount: const Value(5),
197
+
ownerDid: ownerDid,
193
198
),
194
199
);
195
200
···
201
206
seenAt: now.subtract(const Duration(days: 50)).toIso8601String(),
202
207
createdAt: now.subtract(const Duration(days: 45)),
203
208
retryCount: const Value(3),
209
+
ownerDid: ownerDid,
204
210
),
205
211
);
206
212
···
212
218
seenAt: now.toIso8601String(),
213
219
createdAt: now.subtract(const Duration(days: 5)),
214
220
retryCount: const Value(5),
221
+
ownerDid: ownerDid,
215
222
),
216
223
);
217
224
218
-
await dao.enqueueMarkSeen(now);
225
+
await dao.enqueueMarkSeen(now, ownerDid);
219
226
220
227
final deleted = await dao.cleanupOldFailedItems(threshold);
221
228
expect(deleted, 1);
222
229
223
-
final remaining = await dao.getRetryableItems();
230
+
final remaining = await dao.getRetryableItems(ownerDid);
224
231
expect(remaining.length, 2);
225
232
});
226
233
227
234
test('cleanupOldFailedItems returns 0 when nothing to clean', () async {
228
235
final now = DateTime.now();
229
-
await dao.enqueueMarkSeen(now);
236
+
await dao.enqueueMarkSeen(now, ownerDid);
230
237
231
238
final deleted = await dao.cleanupOldFailedItems(now.subtract(const Duration(days: 30)));
232
239
expect(deleted, 0);
233
240
234
-
final remaining = await dao.getRetryableItems();
241
+
final remaining = await dao.getRetryableItems(ownerDid);
235
242
expect(remaining.length, 1);
236
243
});
237
244
});
+44
-21
test/src/infrastructure/db/daos/preference_sync_queue_dao_test.dart
+44
-21
test/src/infrastructure/db/daos/preference_sync_queue_dao_test.dart
···
8
8
group('PreferenceSyncQueueDao', () {
9
9
late AppDatabase db;
10
10
late PreferenceSyncQueueDao dao;
11
+
const ownerDid = 'did:web:tester';
11
12
12
13
setUp(() {
13
14
db = AppDatabase(NativeDatabase.memory());
···
26
27
type: 'save',
27
28
payload: 'at://test',
28
29
createdAt: now,
30
+
ownerDid: ownerDid,
29
31
),
30
32
);
31
33
32
-
final pending = await dao.getPendingItems();
34
+
final pending = await dao.getPendingItems(ownerDid);
33
35
expect(pending.length, 1);
34
36
expect(pending.first.category, 'feed');
35
37
expect(pending.first.type, 'save');
···
37
39
});
38
40
39
41
test('enqueueFeedSync adds feed item with correct category', () async {
40
-
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://test-feed');
42
+
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://test-feed', ownerDid: ownerDid);
41
43
42
-
final pending = await dao.getPendingItems();
44
+
final pending = await dao.getPendingItems(ownerDid);
43
45
expect(pending.length, 1);
44
46
expect(pending.first.category, 'feed');
45
47
expect(pending.first.type, 'save');
···
50
52
await dao.enqueueBlueskyPrefSync(
51
53
preferenceType: 'adultContent',
52
54
preferenceData: '{"enabled": true}',
55
+
ownerDid: ownerDid,
53
56
);
54
57
55
-
final pending = await dao.getPendingItems();
58
+
final pending = await dao.getPendingItems(ownerDid);
56
59
expect(pending.length, 1);
57
60
expect(pending.first.category, 'bluesky_pref');
58
61
expect(pending.first.type, 'adultContent');
···
67
70
type: 'save',
68
71
payload: 'first',
69
72
createdAt: now,
73
+
ownerDid: ownerDid,
70
74
),
71
75
);
72
76
await dao.enqueue(
···
75
79
type: 'remove',
76
80
payload: 'second',
77
81
createdAt: now.add(const Duration(seconds: 1)),
82
+
ownerDid: ownerDid,
78
83
),
79
84
);
80
85
81
-
final pending = await dao.getPendingItems();
86
+
final pending = await dao.getPendingItems(ownerDid);
82
87
expect(pending.length, 2);
83
88
expect(pending[0].payload, 'first');
84
89
expect(pending[1].payload, 'second');
···
92
97
type: 'save',
93
98
payload: 'at://test',
94
99
createdAt: now,
100
+
ownerDid: ownerDid,
95
101
),
96
102
);
97
103
98
-
var pending = await dao.getPendingItems();
104
+
var pending = await dao.getPendingItems(ownerDid);
99
105
final id = pending.first.id;
100
106
101
107
await dao.deleteItem(id);
102
108
103
-
pending = await dao.getPendingItems();
109
+
pending = await dao.getPendingItems(ownerDid);
104
110
expect(pending, isEmpty);
105
111
});
106
112
···
112
118
type: 'save',
113
119
payload: '1',
114
120
createdAt: now,
121
+
ownerDid: ownerDid,
115
122
),
116
123
);
117
124
await dao.enqueue(
···
120
127
type: 'save',
121
128
payload: '2',
122
129
createdAt: now,
130
+
ownerDid: ownerDid,
123
131
),
124
132
);
125
133
126
-
await dao.clearQueue();
134
+
await dao.clearQueue(ownerDid);
127
135
128
-
final pending = await dao.getPendingItems();
136
+
final pending = await dao.getPendingItems(ownerDid);
129
137
expect(pending, isEmpty);
130
138
});
131
139
···
139
147
type: 'save',
140
148
payload: 'at://retryable',
141
149
createdAt: now,
150
+
ownerDid: ownerDid,
142
151
),
143
152
);
144
153
···
151
160
payload: 'at://almost-maxed',
152
161
createdAt: now,
153
162
retryCount: const Value(4),
163
+
ownerDid: ownerDid,
154
164
),
155
165
);
156
166
···
163
173
payload: 'at://maxed-out',
164
174
createdAt: now,
165
175
retryCount: const Value(5),
176
+
ownerDid: ownerDid,
166
177
),
167
178
);
168
179
169
-
final retryable = await dao.getRetryableItems();
180
+
final retryable = await dao.getRetryableItems(ownerDid);
170
181
expect(retryable.length, 2);
171
182
expect(
172
183
retryable.map((r) => r.payload),
···
176
187
});
177
188
178
189
test('getRetryableFeedItems filters by feed category', () async {
179
-
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://feed1');
180
-
await dao.enqueueFeedSync(type: 'remove', feedUri: 'at://feed2');
190
+
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://feed1', ownerDid: ownerDid);
191
+
await dao.enqueueFeedSync(type: 'remove', feedUri: 'at://feed2', ownerDid: ownerDid);
181
192
await dao.enqueueBlueskyPrefSync(
182
193
preferenceType: 'adultContent',
183
194
preferenceData: '{"enabled": true}',
195
+
ownerDid: ownerDid,
184
196
);
185
197
186
-
final feedItems = await dao.getRetryableFeedItems();
198
+
final feedItems = await dao.getRetryableFeedItems(ownerDid);
187
199
expect(feedItems.length, 2);
188
200
expect(feedItems.every((item) => item.category == 'feed'), true);
189
201
expect(feedItems.map((r) => r.payload), containsAll(['at://feed1', 'at://feed2']));
190
202
});
191
203
192
204
test('getRetryableBlueskyPrefItems filters by bluesky_pref category', () async {
193
-
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://feed1');
205
+
await dao.enqueueFeedSync(type: 'save', feedUri: 'at://feed1', ownerDid: ownerDid);
194
206
await dao.enqueueBlueskyPrefSync(
195
207
preferenceType: 'adultContent',
196
208
preferenceData: '{"enabled": true}',
209
+
ownerDid: ownerDid,
197
210
);
198
-
await dao.enqueueBlueskyPrefSync(preferenceType: 'contentLabels', preferenceData: '[]');
211
+
await dao.enqueueBlueskyPrefSync(
212
+
preferenceType: 'contentLabels',
213
+
preferenceData: '[]',
214
+
ownerDid: ownerDid,
215
+
);
199
216
200
-
final prefItems = await dao.getRetryableBlueskyPrefItems();
217
+
final prefItems = await dao.getRetryableBlueskyPrefItems(ownerDid);
201
218
expect(prefItems.length, 2);
202
219
expect(prefItems.every((item) => item.category == 'bluesky_pref'), true);
203
220
expect(prefItems.map((r) => r.payload), containsAll(['{"enabled": true}', '[]']));
···
211
228
type: 'save',
212
229
payload: 'at://test',
213
230
createdAt: now,
231
+
ownerDid: ownerDid,
214
232
),
215
233
);
216
234
217
-
var items = await dao.getPendingItems();
235
+
var items = await dao.getPendingItems(ownerDid);
218
236
expect(items.first.retryCount, 0);
219
237
220
238
await dao.incrementRetryCount(id);
221
239
222
-
items = await dao.getPendingItems();
240
+
items = await dao.getPendingItems(ownerDid);
223
241
expect(items.first.retryCount, 1);
224
242
225
243
await dao.incrementRetryCount(id);
226
244
await dao.incrementRetryCount(id);
227
245
228
-
items = await dao.getPendingItems();
246
+
items = await dao.getPendingItems(ownerDid);
229
247
expect(items.first.retryCount, 3);
230
248
});
231
249
···
242
260
payload: 'at://old-failed',
243
261
createdAt: now.subtract(const Duration(days: 45)),
244
262
retryCount: const Value(5),
263
+
ownerDid: ownerDid,
245
264
),
246
265
);
247
266
···
254
273
payload: 'at://old-retryable',
255
274
createdAt: now.subtract(const Duration(days: 45)),
256
275
retryCount: const Value(3),
276
+
ownerDid: ownerDid,
257
277
),
258
278
);
259
279
···
266
286
payload: 'at://recent-failed',
267
287
createdAt: now.subtract(const Duration(days: 5)),
268
288
retryCount: const Value(5),
289
+
ownerDid: ownerDid,
269
290
),
270
291
);
271
292
···
275
296
type: 'save',
276
297
payload: 'at://new-item',
277
298
createdAt: now,
299
+
ownerDid: ownerDid,
278
300
),
279
301
);
280
302
281
303
final deleted = await dao.cleanupOldFailedItems(threshold);
282
304
expect(deleted, 1);
283
305
284
-
final remaining = await dao.getPendingItems();
306
+
final remaining = await dao.getPendingItems(ownerDid);
285
307
expect(remaining.length, 3);
286
308
expect(
287
309
remaining.map((r) => r.payload),
···
298
320
type: 'save',
299
321
payload: 'at://test',
300
322
createdAt: now,
323
+
ownerDid: ownerDid,
301
324
),
302
325
);
303
326
304
327
final deleted = await dao.cleanupOldFailedItems(now.subtract(const Duration(days: 30)));
305
328
expect(deleted, 0);
306
329
307
-
final remaining = await dao.getPendingItems();
330
+
final remaining = await dao.getPendingItems(ownerDid);
308
331
expect(remaining.length, 1);
309
332
});
310
333
});
+65
-22
test/src/infrastructure/db/daos/saved_feeds_dao_test.dart
+65
-22
test/src/infrastructure/db/daos/saved_feeds_dao_test.dart
···
7
7
void main() {
8
8
late AppDatabase db;
9
9
late SavedFeedsDao dao;
10
+
const ownerDid = 'did:plc:owner';
10
11
11
12
setUp(() {
12
13
db = AppDatabase(NativeDatabase.memory());
···
23
24
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
24
25
displayName: 'Test Feed',
25
26
creatorDid: 'did:plc:abc',
27
+
ownerDid: ownerDid,
26
28
sortOrder: 0,
27
29
lastSynced: DateTime(2025, 1, 1),
28
30
);
29
31
30
32
await dao.upsertFeed(feed);
31
33
32
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
34
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
33
35
expect(result, isNotNull);
34
36
expect(result!.displayName, 'Test Feed');
35
37
expect(result.creatorDid, 'did:plc:abc');
···
42
44
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
43
45
displayName: 'Test Feed',
44
46
creatorDid: 'did:plc:abc',
47
+
ownerDid: ownerDid,
45
48
sortOrder: 0,
46
49
lastSynced: DateTime(2025, 1, 1),
47
50
likeCount: const Value(10),
···
53
56
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
54
57
displayName: 'Updated Feed',
55
58
creatorDid: 'did:plc:abc',
59
+
ownerDid: ownerDid,
56
60
sortOrder: 1,
57
61
lastSynced: DateTime(2025, 1, 2),
58
62
likeCount: const Value(20),
···
60
64
61
65
await dao.upsertFeed(updated);
62
66
63
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
67
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
64
68
expect(result!.displayName, 'Updated Feed');
65
69
expect(result.sortOrder, 1);
66
70
expect(result.likeCount, 20);
···
74
78
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
75
79
displayName: 'Feed 1',
76
80
creatorDid: 'did:plc:abc',
81
+
ownerDid: ownerDid,
77
82
sortOrder: 0,
78
83
lastSynced: DateTime(2025, 1, 1),
79
84
),
···
81
86
uri: 'at://did:plc:def/app.bsky.feed.generator/feed2',
82
87
displayName: 'Feed 2',
83
88
creatorDid: 'did:plc:def',
89
+
ownerDid: ownerDid,
84
90
sortOrder: 1,
85
91
lastSynced: DateTime(2025, 1, 1),
86
92
),
···
88
94
89
95
await dao.upsertFeeds(feeds);
90
96
91
-
final result = await dao.getAllFeeds();
97
+
final result = await dao.getAllFeeds(ownerDid);
92
98
expect(result, hasLength(2));
93
99
expect(result[0].displayName, 'Feed 1');
94
100
expect(result[1].displayName, 'Feed 2');
···
100
106
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
101
107
displayName: 'Feed 1',
102
108
creatorDid: 'did:plc:abc',
109
+
ownerDid: ownerDid,
103
110
sortOrder: 0,
104
111
lastSynced: DateTime(2025, 1, 1),
105
112
),
···
110
117
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
111
118
displayName: 'Updated Feed 1',
112
119
creatorDid: 'did:plc:abc',
120
+
ownerDid: ownerDid,
113
121
sortOrder: 0,
114
122
lastSynced: DateTime(2025, 1, 2),
115
123
),
···
117
125
118
126
await dao.upsertFeeds(updates);
119
127
120
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/feed1');
128
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/feed1', ownerDid);
121
129
expect(result!.displayName, 'Updated Feed 1');
122
130
});
123
131
});
···
129
137
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
130
138
displayName: 'Test Feed',
131
139
creatorDid: 'did:plc:abc',
140
+
ownerDid: ownerDid,
132
141
sortOrder: 0,
133
142
lastSynced: DateTime(2025, 1, 1),
134
143
),
135
144
);
136
145
137
-
final deleted = await dao.deleteFeed('at://did:plc:abc/app.bsky.feed.generator/test');
146
+
final deleted = await dao.deleteFeed(
147
+
'at://did:plc:abc/app.bsky.feed.generator/test',
148
+
ownerDid,
149
+
);
138
150
expect(deleted, 1);
139
151
140
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
152
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
141
153
expect(result, isNull);
142
154
});
143
155
144
156
test('returns 0 when feed does not exist', () async {
145
157
final deleted = await dao.deleteFeed(
146
158
'at://did:plc:nonexistent/app.bsky.feed.generator/test',
159
+
ownerDid,
147
160
);
148
161
expect(deleted, 0);
149
162
});
···
156
169
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
157
170
displayName: 'Feed 1',
158
171
creatorDid: 'did:plc:abc',
172
+
ownerDid: ownerDid,
159
173
sortOrder: 0,
160
174
lastSynced: DateTime(2025, 1, 1),
161
175
),
···
163
177
uri: 'at://did:plc:def/app.bsky.feed.generator/feed2',
164
178
displayName: 'Feed 2',
165
179
creatorDid: 'did:plc:def',
180
+
ownerDid: ownerDid,
166
181
sortOrder: 1,
167
182
lastSynced: DateTime(2025, 1, 1),
168
183
),
169
184
]);
170
185
171
-
final deleted = await dao.deleteAllFeeds();
186
+
final deleted = await dao.deleteAllFeeds(ownerDid);
172
187
expect(deleted, 2);
173
188
174
-
final result = await dao.getAllFeeds();
189
+
final result = await dao.getAllFeeds(ownerDid);
175
190
expect(result, isEmpty);
176
191
});
177
192
});
···
183
198
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
184
199
displayName: 'Test Feed',
185
200
creatorDid: 'did:plc:abc',
201
+
ownerDid: ownerDid,
186
202
sortOrder: 0,
187
203
lastSynced: DateTime(2025, 1, 1),
188
204
),
189
205
);
190
206
191
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
207
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
192
208
expect(result, isNotNull);
193
209
expect(result!.displayName, 'Test Feed');
194
210
});
195
211
196
212
test('returns null when feed does not exist', () async {
197
-
final result = await dao.getFeed('at://did:plc:nonexistent/app.bsky.feed.generator/test');
213
+
final result = await dao.getFeed(
214
+
'at://did:plc:nonexistent/app.bsky.feed.generator/test',
215
+
ownerDid,
216
+
);
198
217
expect(result, isNull);
199
218
});
200
219
});
···
206
225
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
207
226
displayName: 'Test Feed',
208
227
creatorDid: 'did:plc:abc',
228
+
ownerDid: ownerDid,
209
229
sortOrder: 0,
210
230
lastSynced: DateTime(2025, 1, 1),
211
231
),
212
232
);
213
233
214
-
final result = await dao.watchFeed('at://did:plc:abc/app.bsky.feed.generator/test').first;
234
+
final result = await dao
235
+
.watchFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid)
236
+
.first;
215
237
expect(result, isNotNull);
216
238
expect(result!.displayName, 'Test Feed');
217
239
});
218
240
219
241
test('emits null when feed does not exist', () async {
220
242
final result = await dao
221
-
.watchFeed('at://did:plc:nonexistent/app.bsky.feed.generator/test')
243
+
.watchFeed('at://did:plc:nonexistent/app.bsky.feed.generator/test', ownerDid)
222
244
.first;
223
245
expect(result, isNull);
224
246
});
···
231
253
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed3',
232
254
displayName: 'Feed 3',
233
255
creatorDid: 'did:plc:abc',
256
+
ownerDid: ownerDid,
234
257
sortOrder: 2,
235
258
lastSynced: DateTime(2025, 1, 1),
236
259
),
···
238
261
uri: 'at://did:plc:def/app.bsky.feed.generator/feed1',
239
262
displayName: 'Feed 1',
240
263
creatorDid: 'did:plc:def',
264
+
ownerDid: ownerDid,
241
265
sortOrder: 0,
242
266
lastSynced: DateTime(2025, 1, 1),
243
267
),
···
245
269
uri: 'at://did:plc:ghi/app.bsky.feed.generator/feed2',
246
270
displayName: 'Feed 2',
247
271
creatorDid: 'did:plc:ghi',
272
+
ownerDid: ownerDid,
248
273
sortOrder: 1,
249
274
lastSynced: DateTime(2025, 1, 1),
250
275
),
251
276
]);
252
277
253
-
final result = await dao.getAllFeeds();
278
+
final result = await dao.getAllFeeds(ownerDid);
254
279
expect(result, hasLength(3));
255
280
expect(result[0].displayName, 'Feed 1');
256
281
expect(result[1].displayName, 'Feed 2');
···
258
283
});
259
284
260
285
test('returns empty list when no feeds exist', () async {
261
-
final result = await dao.getAllFeeds();
286
+
final result = await dao.getAllFeeds(ownerDid);
262
287
expect(result, isEmpty);
263
288
});
264
289
});
···
270
295
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed2',
271
296
displayName: 'Feed 2',
272
297
creatorDid: 'did:plc:abc',
298
+
ownerDid: ownerDid,
273
299
sortOrder: 1,
274
300
lastSynced: DateTime(2025, 1, 1),
275
301
),
···
277
303
uri: 'at://did:plc:def/app.bsky.feed.generator/feed1',
278
304
displayName: 'Feed 1',
279
305
creatorDid: 'did:plc:def',
306
+
ownerDid: ownerDid,
280
307
sortOrder: 0,
281
308
lastSynced: DateTime(2025, 1, 1),
282
309
),
283
310
]);
284
311
285
-
final result = await dao.watchAllFeeds().first;
312
+
final result = await dao.watchAllFeeds(ownerDid).first;
286
313
expect(result, hasLength(2));
287
314
expect(result[0].displayName, 'Feed 1');
288
315
expect(result[1].displayName, 'Feed 2');
···
296
323
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
297
324
displayName: 'Feed 1',
298
325
creatorDid: 'did:plc:abc',
326
+
ownerDid: ownerDid,
299
327
sortOrder: 0,
300
328
isPinned: const Value(true),
301
329
lastSynced: DateTime(2025, 1, 1),
···
304
332
uri: 'at://did:plc:def/app.bsky.feed.generator/feed2',
305
333
displayName: 'Feed 2',
306
334
creatorDid: 'did:plc:def',
335
+
ownerDid: ownerDid,
307
336
sortOrder: 1,
308
337
isPinned: const Value(false),
309
338
lastSynced: DateTime(2025, 1, 1),
···
312
341
uri: 'at://did:plc:ghi/app.bsky.feed.generator/feed3',
313
342
displayName: 'Feed 3',
314
343
creatorDid: 'did:plc:ghi',
344
+
ownerDid: ownerDid,
315
345
sortOrder: 2,
316
346
isPinned: const Value(true),
317
347
lastSynced: DateTime(2025, 1, 1),
318
348
),
319
349
]);
320
350
321
-
final result = await dao.getPinnedFeeds();
351
+
final result = await dao.getPinnedFeeds(ownerDid);
322
352
expect(result, hasLength(2));
323
353
expect(result[0].displayName, 'Feed 1');
324
354
expect(result[1].displayName, 'Feed 3');
···
330
360
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
331
361
displayName: 'Feed 1',
332
362
creatorDid: 'did:plc:abc',
363
+
ownerDid: ownerDid,
333
364
sortOrder: 0,
334
365
isPinned: const Value(false),
335
366
lastSynced: DateTime(2025, 1, 1),
336
367
),
337
368
);
338
369
339
-
final result = await dao.getPinnedFeeds();
370
+
final result = await dao.getPinnedFeeds(ownerDid);
340
371
expect(result, isEmpty);
341
372
});
342
373
});
···
348
379
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed1',
349
380
displayName: 'Feed 1',
350
381
creatorDid: 'did:plc:abc',
382
+
ownerDid: ownerDid,
351
383
sortOrder: 0,
352
384
isPinned: const Value(true),
353
385
lastSynced: DateTime(2025, 1, 1),
···
356
388
uri: 'at://did:plc:def/app.bsky.feed.generator/feed2',
357
389
displayName: 'Feed 2',
358
390
creatorDid: 'did:plc:def',
391
+
ownerDid: ownerDid,
359
392
sortOrder: 1,
360
393
isPinned: const Value(false),
361
394
lastSynced: DateTime(2025, 1, 1),
362
395
),
363
396
]);
364
397
365
-
final result = await dao.watchPinnedFeeds().first;
398
+
final result = await dao.watchPinnedFeeds(ownerDid).first;
366
399
expect(result, hasLength(1));
367
400
expect(result[0].displayName, 'Feed 1');
368
401
});
···
379
412
uri: 'at://did:plc:abc/app.bsky.feed.generator/fresh',
380
413
displayName: 'Fresh Feed',
381
414
creatorDid: 'did:plc:abc',
415
+
ownerDid: ownerDid,
382
416
sortOrder: 0,
383
417
lastSynced: now,
384
418
),
···
386
420
uri: 'at://did:plc:def/app.bsky.feed.generator/stale1',
387
421
displayName: 'Stale Feed 1',
388
422
creatorDid: 'did:plc:def',
423
+
ownerDid: ownerDid,
389
424
sortOrder: 1,
390
425
lastSynced: yesterday,
391
426
),
···
393
428
uri: 'at://did:plc:ghi/app.bsky.feed.generator/stale2',
394
429
displayName: 'Stale Feed 2',
395
430
creatorDid: 'did:plc:ghi',
431
+
ownerDid: ownerDid,
396
432
sortOrder: 2,
397
433
lastSynced: weekAgo,
398
434
),
399
435
]);
400
436
401
437
final threshold = DateTime(2025, 1, 4, 12);
402
-
final result = await dao.getStaleFeeds(threshold);
438
+
final result = await dao.getStaleFeeds(threshold, ownerDid);
403
439
404
440
expect(result, hasLength(2));
405
441
expect(result.any((f) => f.displayName == 'Stale Feed 1'), true);
···
412
448
uri: 'at://did:plc:abc/app.bsky.feed.generator/fresh',
413
449
displayName: 'Fresh Feed',
414
450
creatorDid: 'did:plc:abc',
451
+
ownerDid: ownerDid,
415
452
sortOrder: 0,
416
453
lastSynced: DateTime(2025, 1, 5),
417
454
),
418
455
);
419
456
420
457
final threshold = DateTime(2025, 1, 1);
421
-
final result = await dao.getStaleFeeds(threshold);
458
+
final result = await dao.getStaleFeeds(threshold, ownerDid);
422
459
expect(result, isEmpty);
423
460
});
424
461
});
···
430
467
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
431
468
displayName: 'Test Feed',
432
469
creatorDid: 'did:plc:abc',
470
+
ownerDid: ownerDid,
433
471
sortOrder: 0,
434
472
lastSynced: DateTime(2025, 1, 1),
435
473
),
···
438
476
final updated = await dao.updateSortOrder(
439
477
'at://did:plc:abc/app.bsky.feed.generator/test',
440
478
5,
479
+
ownerDid,
441
480
);
442
481
expect(updated, 1);
443
482
444
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
483
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
445
484
expect(result!.sortOrder, 5);
446
485
});
447
486
···
449
488
final updated = await dao.updateSortOrder(
450
489
'at://did:plc:nonexistent/app.bsky.feed.generator/test',
451
490
5,
491
+
ownerDid,
452
492
);
453
493
expect(updated, 0);
454
494
});
···
461
501
uri: 'at://did:plc:abc/app.bsky.feed.generator/test',
462
502
displayName: 'Test Feed',
463
503
creatorDid: 'did:plc:abc',
504
+
ownerDid: ownerDid,
464
505
sortOrder: 0,
465
506
isPinned: const Value(false),
466
507
lastSynced: DateTime(2025, 1, 1),
···
470
511
final updated = await dao.updatePinnedStatus(
471
512
'at://did:plc:abc/app.bsky.feed.generator/test',
472
513
true,
514
+
ownerDid,
473
515
);
474
516
expect(updated, 1);
475
517
476
-
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test');
518
+
final result = await dao.getFeed('at://did:plc:abc/app.bsky.feed.generator/test', ownerDid);
477
519
expect(result!.isPinned, true);
478
520
});
479
521
···
481
523
final updated = await dao.updatePinnedStatus(
482
524
'at://did:plc:nonexistent/app.bsky.feed.generator/test',
483
525
true,
526
+
ownerDid,
484
527
);
485
528
expect(updated, 0);
486
529
});
+4
-2
test/src/infrastructure/network/providers_test.dart
+4
-2
test/src/infrastructure/network/providers_test.dart
···
74
74
when(() => mockSessionStorage.getSession()).thenAnswer((_) async => null);
75
75
when(() => mockSessionStorage.clearSession()).thenAnswer((_) async {});
76
76
when(() => mockDatabase.feedContentDao).thenReturn(mockFeedContentDao);
77
-
when(() => mockFeedContentDao.clearFeedContent(any())).thenAnswer((_) async {});
77
+
when(
78
+
() => mockFeedContentDao.clearFeedContent(any(), any(named: 'ownerDid')),
79
+
).thenAnswer((_) async {});
78
80
});
79
81
80
82
test('returns null when user is not authenticated', () {
···
137
139
await Future<void>.delayed(Duration.zero);
138
140
139
141
expect(notifier.logoutInvoked, isTrue);
140
-
verify(() => mockFeedContentDao.clearFeedContent('home')).called(1);
142
+
verify(() => mockFeedContentDao.clearFeedContent('home', any(named: 'ownerDid'))).called(1);
141
143
});
142
144
});
143
145
}
+66
-61
test/src/infrastructure/preferences/bluesky_preferences_repository_test.dart
+66
-61
test/src/infrastructure/preferences/bluesky_preferences_repository_test.dart
···
9
9
import '../../../helpers/mocks.dart';
10
10
11
11
void main() {
12
+
const ownerDid = 'did:web:tester';
12
13
late AppDatabase db;
13
14
late MockXrpcClient mockApi;
14
15
late Logger logger;
···
34
35
test('skips sync for unauthenticated user', () async {
35
36
when(() => mockApi.isAuthenticated).thenReturn(false);
36
37
37
-
await repository.syncPreferencesFromRemote();
38
+
await repository.syncPreferencesFromRemote(ownerDid);
38
39
39
40
verifyNever(() => mockApi.call(any()));
40
41
});
···
49
50
},
50
51
);
51
52
52
-
await repository.syncPreferencesFromRemote();
53
+
await repository.syncPreferencesFromRemote(ownerDid);
53
54
54
-
final pref = await repository.getAdultContentPref();
55
+
final pref = await repository.getAdultContentPref(ownerDid);
55
56
expect(pref.enabled, isTrue);
56
57
});
57
58
···
74
75
},
75
76
);
76
77
77
-
await repository.syncPreferencesFromRemote();
78
+
await repository.syncPreferencesFromRemote(ownerDid);
78
79
79
-
final prefs = await repository.getContentLabelPrefs();
80
+
final prefs = await repository.getContentLabelPrefs(ownerDid);
80
81
expect(prefs.items, hasLength(2));
81
82
expect(prefs.getVisibility('sexual'), LabelVisibility.hide);
82
83
expect(prefs.getVisibility('nudity'), LabelVisibility.warn);
···
98
99
},
99
100
);
100
101
101
-
await repository.syncPreferencesFromRemote();
102
+
await repository.syncPreferencesFromRemote(ownerDid);
102
103
103
-
final pref = await repository.getLabelersPref();
104
+
final pref = await repository.getLabelersPref(ownerDid);
104
105
expect(pref.labelerDids, ['did:plc:test1', 'did:plc:test2']);
105
106
});
106
107
···
118
119
},
119
120
);
120
121
121
-
await repository.syncPreferencesFromRemote();
122
+
await repository.syncPreferencesFromRemote(ownerDid);
122
123
123
-
final pref = await repository.getFeedViewPref();
124
+
final pref = await repository.getFeedViewPref(ownerDid);
124
125
expect(pref.hideReplies, isTrue);
125
126
expect(pref.hideReposts, isTrue);
126
127
});
···
139
140
},
140
141
);
141
142
142
-
await repository.syncPreferencesFromRemote();
143
+
await repository.syncPreferencesFromRemote(ownerDid);
143
144
144
-
final pref = await repository.getThreadViewPref();
145
+
final pref = await repository.getThreadViewPref(ownerDid);
145
146
expect(pref.sort, ThreadSortOrder.newest);
146
147
expect(pref.prioritizeFollowedUsers, isFalse);
147
148
});
···
165
166
},
166
167
);
167
168
168
-
await repository.syncPreferencesFromRemote();
169
+
await repository.syncPreferencesFromRemote(ownerDid);
169
170
170
-
final pref = await repository.getMutedWordsPref();
171
+
final pref = await repository.getMutedWordsPref(ownerDid);
171
172
expect(pref.items, hasLength(1));
172
173
expect(pref.items[0].value, 'test-word');
173
174
});
···
191
192
},
192
193
);
193
194
194
-
await repository.syncPreferencesFromRemote();
195
+
await repository.syncPreferencesFromRemote(ownerDid);
195
196
196
-
expect((await repository.getAdultContentPref()).enabled, isTrue);
197
-
expect((await repository.getContentLabelPrefs()).items, hasLength(1));
198
-
expect((await repository.getLabelersPref()).labelers, isEmpty);
199
-
expect((await repository.getFeedViewPref()).hideQuotePosts, isTrue);
200
-
expect((await repository.getThreadViewPref()).sort, ThreadSortOrder.hotness);
201
-
expect((await repository.getMutedWordsPref()).items, isEmpty);
197
+
expect((await repository.getAdultContentPref(ownerDid)).enabled, isTrue);
198
+
expect((await repository.getContentLabelPrefs(ownerDid)).items, hasLength(1));
199
+
expect((await repository.getLabelersPref(ownerDid)).labelers, isEmpty);
200
+
expect((await repository.getFeedViewPref(ownerDid)).hideQuotePosts, isTrue);
201
+
expect((await repository.getThreadViewPref(ownerDid)).sort, ThreadSortOrder.hotness);
202
+
expect((await repository.getMutedWordsPref(ownerDid)).items, isEmpty);
202
203
});
203
204
204
205
test('handles malformed preference gracefully', () async {
···
206
207
when(() => mockApi.call('app.bsky.actor.getPreferences')).thenAnswer(
207
208
(_) async => {
208
209
'preferences': [
209
-
'not-a-map', // Invalid
210
+
'not-a-map',
210
211
{r'$type': 'app.bsky.actor.defs#adultContentPref', 'enabled': true},
211
212
],
212
213
},
213
214
);
214
215
215
-
await repository.syncPreferencesFromRemote();
216
+
await repository.syncPreferencesFromRemote(ownerDid);
216
217
217
-
final pref = await repository.getAdultContentPref();
218
+
final pref = await repository.getAdultContentPref(ownerDid);
218
219
expect(pref.enabled, isTrue);
219
220
});
220
221
});
221
222
222
223
group('getters return defaults when not synced', () {
223
224
test('getAdultContentPref returns false by default', () async {
224
-
final pref = await repository.getAdultContentPref();
225
+
final pref = await repository.getAdultContentPref(ownerDid);
225
226
expect(pref.enabled, isFalse);
226
227
});
227
228
228
229
test('getContentLabelPrefs returns empty by default', () async {
229
-
final prefs = await repository.getContentLabelPrefs();
230
+
final prefs = await repository.getContentLabelPrefs(ownerDid);
230
231
expect(prefs.items, isEmpty);
231
232
});
232
233
233
234
test('getLabelersPref returns empty by default', () async {
234
-
final pref = await repository.getLabelersPref();
235
+
final pref = await repository.getLabelersPref(ownerDid);
235
236
expect(pref.labelers, isEmpty);
236
237
});
237
238
238
239
test('getFeedViewPref returns defaults', () async {
239
-
final pref = await repository.getFeedViewPref();
240
+
final pref = await repository.getFeedViewPref(ownerDid);
240
241
expect(pref.hideReplies, isFalse);
241
242
expect(pref.hideReposts, isFalse);
242
243
});
243
244
244
245
test('getThreadViewPref returns defaults', () async {
245
-
final pref = await repository.getThreadViewPref();
246
+
final pref = await repository.getThreadViewPref(ownerDid);
246
247
expect(pref.sort, ThreadSortOrder.oldest);
247
248
expect(pref.prioritizeFollowedUsers, isTrue);
248
249
});
249
250
250
251
test('getMutedWordsPref returns empty by default', () async {
251
-
final pref = await repository.getMutedWordsPref();
252
+
final pref = await repository.getMutedWordsPref(ownerDid);
252
253
expect(pref.items, isEmpty);
253
254
});
254
255
});
···
264
265
},
265
266
);
266
267
267
-
final stream = repository.watchAdultContentPref();
268
+
final stream = repository.watchAdultContentPref(ownerDid);
268
269
expect((await stream.first).enabled, isFalse);
269
270
270
-
await repository.syncPreferencesFromRemote();
271
+
await repository.syncPreferencesFromRemote(ownerDid);
271
272
expect((await stream.first).enabled, isTrue);
272
273
});
273
274
});
···
283
284
},
284
285
);
285
286
286
-
await repository.syncPreferencesFromRemote();
287
-
expect((await repository.getAdultContentPref()).enabled, isTrue);
287
+
await repository.syncPreferencesFromRemote(ownerDid);
288
+
expect((await repository.getAdultContentPref(ownerDid)).enabled, isTrue);
288
289
289
-
await repository.clearAll();
290
+
await repository.clearAll(ownerDid);
290
291
291
-
expect((await repository.getAdultContentPref()).enabled, isFalse);
292
+
expect((await repository.getAdultContentPref(ownerDid)).enabled, isFalse);
292
293
});
293
294
});
294
295
···
296
297
test('persists preference to database', () async {
297
298
const pref = FeedViewPref(hideReplies: true, hideReposts: true, hideQuotePosts: false);
298
299
299
-
await repository.updateFeedViewPref(pref);
300
+
await repository.updateFeedViewPref(pref, ownerDid);
300
301
301
-
final stored = await repository.getFeedViewPref();
302
+
final stored = await repository.getFeedViewPref(ownerDid);
302
303
expect(stored.hideReplies, isTrue);
303
304
expect(stored.hideReposts, isTrue);
304
305
expect(stored.hideQuotePosts, isFalse);
···
307
308
test('queues sync item', () async {
308
309
const pref = FeedViewPref(hideReplies: true);
309
310
310
-
await repository.updateFeedViewPref(pref);
311
+
await repository.updateFeedViewPref(pref, ownerDid);
311
312
312
-
final queued = await db.preferenceSyncQueueDao.getPendingItems();
313
+
final queued = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
313
314
expect(queued, hasLength(1));
314
315
expect(queued[0].type, 'feedView');
315
316
});
···
319
320
test('persists preference to database', () async {
320
321
const pref = ThreadViewPref(sort: ThreadSortOrder.newest, prioritizeFollowedUsers: false);
321
322
322
-
await repository.updateThreadViewPref(pref);
323
+
await repository.updateThreadViewPref(pref, ownerDid);
323
324
324
-
final stored = await repository.getThreadViewPref();
325
+
final stored = await repository.getThreadViewPref(ownerDid);
325
326
expect(stored.sort, ThreadSortOrder.newest);
326
327
expect(stored.prioritizeFollowedUsers, isFalse);
327
328
});
···
329
330
test('queues sync item', () async {
330
331
const pref = ThreadViewPref(sort: ThreadSortOrder.mostLikes);
331
332
332
-
await repository.updateThreadViewPref(pref);
333
+
await repository.updateThreadViewPref(pref, ownerDid);
333
334
334
-
final queued = await db.preferenceSyncQueueDao.getPendingItems();
335
+
final queued = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
335
336
expect(queued, hasLength(1));
336
337
expect(queued[0].type, 'threadView');
337
338
});
···
341
342
test('persists preference to database', () async {
342
343
const pref = AdultContentPref(enabled: true);
343
344
344
-
await repository.updateAdultContentPref(pref);
345
+
await repository.updateAdultContentPref(pref, ownerDid);
345
346
346
-
final stored = await repository.getAdultContentPref();
347
+
final stored = await repository.getAdultContentPref(ownerDid);
347
348
expect(stored.enabled, isTrue);
348
349
});
349
350
350
351
test('queues sync item', () async {
351
352
const pref = AdultContentPref(enabled: true);
352
353
353
-
await repository.updateAdultContentPref(pref);
354
+
await repository.updateAdultContentPref(pref, ownerDid);
354
355
355
-
final queued = await db.preferenceSyncQueueDao.getPendingItems();
356
+
final queued = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
356
357
expect(queued, hasLength(1));
357
358
expect(queued[0].type, 'adultContent');
358
359
});
359
360
360
361
test('updates from enabled to disabled', () async {
361
-
await repository.updateAdultContentPref(const AdultContentPref(enabled: true));
362
-
expect((await repository.getAdultContentPref()).enabled, isTrue);
362
+
await repository.updateAdultContentPref(const AdultContentPref(enabled: true), ownerDid);
363
+
expect((await repository.getAdultContentPref(ownerDid)).enabled, isTrue);
363
364
364
-
await repository.updateAdultContentPref(const AdultContentPref(enabled: false));
365
-
expect((await repository.getAdultContentPref()).enabled, isFalse);
365
+
await repository.updateAdultContentPref(const AdultContentPref(enabled: false), ownerDid);
366
+
expect((await repository.getAdultContentPref(ownerDid)).enabled, isFalse);
366
367
});
367
368
});
368
369
···
375
376
],
376
377
);
377
378
378
-
await repository.updateContentLabelPrefs(prefs);
379
+
await repository.updateContentLabelPrefs(prefs, ownerDid);
379
380
380
-
final stored = await repository.getContentLabelPrefs();
381
+
final stored = await repository.getContentLabelPrefs(ownerDid);
381
382
expect(stored.items, hasLength(2));
382
383
expect(stored.getVisibility('sexual'), LabelVisibility.hide);
383
384
expect(stored.getVisibility('gore'), LabelVisibility.warn);
···
388
389
items: [ContentLabelPref(label: 'spam', visibility: LabelVisibility.hide)],
389
390
);
390
391
391
-
await repository.updateContentLabelPrefs(prefs);
392
+
await repository.updateContentLabelPrefs(prefs, ownerDid);
392
393
393
-
final queued = await db.preferenceSyncQueueDao.getPendingItems();
394
+
final queued = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
394
395
expect(queued, hasLength(1));
395
396
expect(queued[0].type, 'contentLabels');
396
397
});
···
400
401
const ContentLabelPrefs(
401
402
items: [ContentLabelPref(label: 'sexual', visibility: LabelVisibility.hide)],
402
403
),
404
+
ownerDid,
403
405
);
404
406
405
407
await repository.updateContentLabelPrefs(
406
408
const ContentLabelPrefs(
407
409
items: [ContentLabelPref(label: 'gore', visibility: LabelVisibility.warn)],
408
410
),
411
+
ownerDid,
409
412
);
410
413
411
-
final stored = await repository.getContentLabelPrefs();
414
+
final stored = await repository.getContentLabelPrefs(ownerDid);
412
415
expect(stored.items, hasLength(1));
413
416
expect(stored.getVisibility('gore'), LabelVisibility.warn);
414
417
expect(stored.getVisibility('sexual'), isNull);
···
429
432
],
430
433
);
431
434
432
-
await repository.updateMutedWordsPref(pref);
435
+
await repository.updateMutedWordsPref(pref, ownerDid);
433
436
434
-
final stored = await repository.getMutedWordsPref();
437
+
final stored = await repository.getMutedWordsPref(ownerDid);
435
438
expect(stored.items, hasLength(2));
436
439
expect(stored.items[0].value, 'spam');
437
440
expect(stored.items[1].value, 'scam');
···
445
448
],
446
449
);
447
450
448
-
await repository.updateMutedWordsPref(pref);
451
+
await repository.updateMutedWordsPref(pref, ownerDid);
449
452
450
-
final queued = await db.preferenceSyncQueueDao.getPendingItems();
453
+
final queued = await db.preferenceSyncQueueDao.getPendingItems(ownerDid);
451
454
expect(queued, hasLength(1));
452
455
expect(queued[0].type, 'mutedWords');
453
456
});
···
459
462
MutedWord(id: '1', value: 'old', targets: [MutedWordTarget.content]),
460
463
],
461
464
),
465
+
ownerDid,
462
466
);
463
467
464
468
await repository.updateMutedWordsPref(
···
467
471
MutedWord(id: '2', value: 'new', targets: [MutedWordTarget.content]),
468
472
],
469
473
),
474
+
ownerDid,
470
475
);
471
476
472
-
final stored = await repository.getMutedWordsPref();
477
+
final stored = await repository.getMutedWordsPref(ownerDid);
473
478
expect(stored.items, hasLength(1));
474
479
expect(stored.items[0].value, 'new');
475
480
});