mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter

refactor: add ownerDid to scope db

Changed files
+3278 -1561
lib
src
features
infrastructure
test
src
app
features
infrastructure
+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
··· 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
··· 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
··· 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
··· 89 89 } 90 90 } 91 91 92 - String _$outboxRepositoryHash() => r'49973f0d95b8739001506afcede74bc7eb1ba8e8'; 92 + String _$outboxRepositoryHash() => r'63bdc933665b73616d72dca13d38650bbe4823c6';
+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
··· 56 56 } 57 57 } 58 58 59 - String _$feedContentCleanupControllerHash() => r'eb2027b4d945ded8b13c6919bc993e8881fe1e5d'; 59 + String _$feedContentCleanupControllerHash() => r'721c568678ab6838264702798fcb43a441122d06';
+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
··· 65 65 } 66 66 } 67 67 68 - String _$feedContentNotifierHash() => r'6d04a306a6b6625862ed21fdc876ccf04261ec09'; 68 + String _$feedContentNotifierHash() => r'a227f64fce3a48aa8ef51d0ab265822339feb477'; 69 69 70 70 /// Notifier for managing feed content (posts from the active feed). 71 71 ///
+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
··· 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
··· 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
··· 56 56 } 57 57 } 58 58 59 - String _$feedSyncControllerHash() => r'a6ef5c6d7e5ee562e735cf8ba62d237f824e93e4'; 59 + String _$feedSyncControllerHash() => r'6efc143eb558a7c9db8355b39c5caa10ed56b8b9';
+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
··· 40 40 } 41 41 } 42 42 43 - String _$hasPendingSyncHash() => r'33e3e83f5f00b3051b137776becab72bea426bc0'; 43 + String _$hasPendingSyncHash() => r'24d7f04743e85d3d36ec4107179db9937abd89af';
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 156 156 } 157 157 } 158 158 159 - String _$profileNotifierHash() => r'8427c3f560281abad899f53fb5b2c4e3f59c33fa'; 159 + String _$profileNotifierHash() => r'7ccce6122fcf09098774583f7a1568dc94d50343'; 160 160 161 161 final class ProfileNotifierFamily extends $Family 162 162 with
+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
··· 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
··· 56 56 } 57 57 } 58 58 59 - String _$preferenceSyncControllerHash() => r'394099cdba86119b64aa45ae68b7bbc1e39f760e'; 59 + String _$preferenceSyncControllerHash() => r'305b3f9687000b6b52bcb4da4ea77629f45d66bb';
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 49 49 } 50 50 } 51 51 52 - String _$threadNotifierHash() => r'acb87e725d16f18afdd4e7e4f65be411ee23664e'; 52 + String _$threadNotifierHash() => r'779c8a2cb5b70214e9972972796a6820c41d42b1'; 53 53 54 54 final class ThreadNotifierFamily extends $Family 55 55 with
+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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 39 39 onSessionInvalidated: () { 40 40 try { 41 41 final db = ref.read(appDatabaseProvider); 42 - db.feedContentDao.clearFeedContent('home'); 42 + db.feedContentDao.clearFeedContent('home', session.did); 43 43 } catch (e) { 44 44 /* Ignore if database not available */ 45 45 }
+1 -1
lib/src/infrastructure/network/providers.g.dart
··· 101 101 } 102 102 } 103 103 104 - String _$dioPdsHash() => r'd1e9b53f22f3cdf12fa0cafcbc26908b1f10b36d'; 104 + String _$dioPdsHash() => r'58b56b8b3713905fbe532dff9f7e5e80f3c47288'; 105 105 106 106 /// Provides the XRPC client for making API requests. 107 107 ///
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 });