import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/database/drift_db.dart'; import 'package:island/models/account.dart'; import 'package:island/models/chat.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/database.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'chat_room.g.dart'; final isSyncingProvider = StateProvider.autoDispose((ref) => false); final flashingMessagesProvider = StateProvider>((ref) => {}); @riverpod class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { @override Future> build() async { final db = ref.watch(databaseProvider); try { final localRoomsData = await db.select(db.chatRooms).get(); if (localRoomsData.isNotEmpty) { final localRooms = await Future.wait( localRoomsData.map((row) async { final membersRows = await (db.select(db.chatMembers) ..where((m) => m.chatRoomId.equals(row.id))).get(); final members = membersRows.map((mRow) { final account = SnAccount.fromJson(mRow.account); return SnChatMember( id: mRow.id, chatRoomId: mRow.chatRoomId, accountId: mRow.accountId, account: account, nick: mRow.nick, notify: mRow.notify, joinedAt: mRow.joinedAt, breakUntil: mRow.breakUntil, timeoutUntil: mRow.timeoutUntil, status: null, createdAt: mRow.createdAt, updatedAt: mRow.updatedAt, deletedAt: mRow.deletedAt, chatRoom: null, ); }).toList(); return SnChatRoom( id: row.id, name: row.name, description: row.description, type: row.type, isPublic: row.isPublic!, isCommunity: row.isCommunity!, picture: row.picture != null ? SnCloudFile.fromJson(row.picture!) : null, background: row.background != null ? SnCloudFile.fromJson(row.background!) : null, realmId: row.realmId, accountId: row.accountId, realm: null, createdAt: row.createdAt, updatedAt: row.updatedAt, deletedAt: row.deletedAt, members: members, ); }), ); // Background sync Future(() async { try { final client = ref.read(apiClientProvider); final resp = await client.get('/sphere/chat'); final remoteRooms = resp.data .map((e) => SnChatRoom.fromJson(e)) .cast() .toList(); await db.saveChatRooms(remoteRooms, override: true); // Update state with fresh data state = AsyncData(await _buildRoomsFromDb(db)); } catch (_) {} }).ignore(); return localRooms; } } catch (_) {} // Fallback to API final client = ref.watch(apiClientProvider); final resp = await client.get('/sphere/chat'); final rooms = resp.data .map((e) => SnChatRoom.fromJson(e)) .cast() .toList(); await db.saveChatRooms(rooms, override: true); return rooms; } Future> _buildRoomsFromDb(AppDatabase db) async { final localRoomsData = await db.select(db.chatRooms).get(); return Future.wait( localRoomsData.map((row) async { final membersRows = await (db.select(db.chatMembers) ..where((m) => m.chatRoomId.equals(row.id))).get(); final members = membersRows.map((mRow) { final account = SnAccount.fromJson(mRow.account); return SnChatMember( id: mRow.id, chatRoomId: mRow.chatRoomId, accountId: mRow.accountId, account: account, nick: mRow.nick, notify: mRow.notify, joinedAt: mRow.joinedAt, breakUntil: mRow.breakUntil, timeoutUntil: mRow.timeoutUntil, status: null, createdAt: mRow.createdAt, updatedAt: mRow.updatedAt, deletedAt: mRow.deletedAt, chatRoom: null, ); }).toList(); return SnChatRoom( id: row.id, name: row.name, description: row.description, type: row.type, isPublic: row.isPublic!, isCommunity: row.isCommunity!, picture: row.picture != null ? SnCloudFile.fromJson(row.picture!) : null, background: row.background != null ? SnCloudFile.fromJson(row.background!) : null, realmId: row.realmId, accountId: row.accountId, realm: null, createdAt: row.createdAt, updatedAt: row.updatedAt, deletedAt: row.deletedAt, members: members, ); }), ); } } @riverpod class ChatRoomNotifier extends _$ChatRoomNotifier { @override Future build(String? identifier) async { if (identifier == null) return null; final db = ref.watch(databaseProvider); try { // Try to get from local database first final localRoomData = await (db.select(db.chatRooms) ..where((r) => r.id.equals(identifier))).getSingleOrNull(); if (localRoomData != null) { // Fetch members for this room final membersRows = await (db.select(db.chatMembers) ..where((m) => m.chatRoomId.equals(localRoomData.id))).get(); final members = membersRows.map((mRow) { final account = SnAccount.fromJson(mRow.account); return SnChatMember( id: mRow.id, chatRoomId: mRow.chatRoomId, accountId: mRow.accountId, account: account, nick: mRow.nick, notify: mRow.notify, joinedAt: mRow.joinedAt, breakUntil: mRow.breakUntil, timeoutUntil: mRow.timeoutUntil, status: null, createdAt: mRow.createdAt, updatedAt: mRow.updatedAt, deletedAt: mRow.deletedAt, chatRoom: null, ); }).toList(); final localRoom = SnChatRoom( id: localRoomData.id, name: localRoomData.name, description: localRoomData.description, type: localRoomData.type, isPublic: localRoomData.isPublic!, isCommunity: localRoomData.isCommunity!, picture: localRoomData.picture != null ? SnCloudFile.fromJson(localRoomData.picture!) : null, background: localRoomData.background != null ? SnCloudFile.fromJson(localRoomData.background!) : null, realmId: localRoomData.realmId, accountId: localRoomData.accountId, realm: null, createdAt: localRoomData.createdAt, updatedAt: localRoomData.updatedAt, deletedAt: localRoomData.deletedAt, members: members, ); // Background sync Future(() async { try { final client = ref.read(apiClientProvider); final resp = await client.get('/sphere/chat/$identifier'); final remoteRoom = SnChatRoom.fromJson(resp.data); // Update state with fresh data directly without saving to DB // DB will be updated by ChatRoomJoinedNotifier's full sync state = AsyncData(remoteRoom); } catch (_) {} }).ignore(); return localRoom; } } catch (_) {} // Fallback to API try { final client = ref.watch(apiClientProvider); final resp = await client.get('/sphere/chat/$identifier'); final room = SnChatRoom.fromJson(resp.data); await db.saveChatRooms([room]); return room; } catch (err) { if (err is DioException && err.response?.statusCode == 404) { return null; // Chat room not found } rethrow; // Rethrow other errors } } } @riverpod class ChatRoomIdentityNotifier extends _$ChatRoomIdentityNotifier { @override Future build(String? identifier) async { if (identifier == null) return null; final db = ref.watch(databaseProvider); final userInfo = ref.watch(userInfoProvider); try { // Try to get from local database first if (userInfo.value != null) { final localMemberData = await (db.select(db.chatMembers) ..where((m) => m.chatRoomId.equals(identifier)) ..where((m) => m.accountId.equals(userInfo.value!.id))) .getSingleOrNull(); if (localMemberData != null) { final account = SnAccount.fromJson(localMemberData.account); final localMember = SnChatMember( id: localMemberData.id, chatRoomId: localMemberData.chatRoomId, accountId: localMemberData.accountId, account: account, nick: localMemberData.nick, notify: localMemberData.notify, joinedAt: localMemberData.joinedAt, breakUntil: localMemberData.breakUntil, timeoutUntil: localMemberData.timeoutUntil, status: null, createdAt: localMemberData.createdAt, updatedAt: localMemberData.updatedAt, deletedAt: localMemberData.deletedAt, chatRoom: null, ); // Background sync Future(() async { try { final client = ref.read(apiClientProvider); final resp = await client.get( '/sphere/chat/$identifier/members/me', ); final remoteMember = SnChatMember.fromJson(resp.data); await db.saveMember(remoteMember); // Update state with fresh data if (userInfo.value != null) { state = AsyncData( await _buildMemberFromDb(db, identifier, userInfo.value!.id), ); } } catch (_) {} }).ignore(); return localMember; } } } catch (_) {} // Fallback to API try { final client = ref.watch(apiClientProvider); final resp = await client.get('/sphere/chat/$identifier/members/me'); final member = SnChatMember.fromJson(resp.data); await db.saveMember(member); return member; } catch (err) { if (err is DioException && err.response?.statusCode == 404) { return null; // Chat member not found } rethrow; // Rethrow other errors } } Future _buildMemberFromDb( AppDatabase db, String identifier, String accountId, ) async { final localMemberData = await (db.select(db.chatMembers) ..where((m) => m.chatRoomId.equals(identifier)) ..where((m) => m.accountId.equals(accountId))) .getSingleOrNull(); if (localMemberData == null) return null; final account = SnAccount.fromJson(localMemberData.account); return SnChatMember( id: localMemberData.id, chatRoomId: localMemberData.chatRoomId, accountId: localMemberData.accountId, account: account, nick: localMemberData.nick, notify: localMemberData.notify, joinedAt: localMemberData.joinedAt, breakUntil: localMemberData.breakUntil, timeoutUntil: localMemberData.timeoutUntil, status: null, createdAt: localMemberData.createdAt, updatedAt: localMemberData.updatedAt, deletedAt: localMemberData.deletedAt, chatRoom: null, ); } } @riverpod Future> chatroomInvites(Ref ref) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/sphere/chat/invites'); return resp.data .map((e) => SnChatMember.fromJson(e)) .cast() .toList(); }