✨ Chat shows realm it is belongs to
This commit is contained in:
@@ -5,16 +5,19 @@ import 'package:island/database/draft.dart';
|
|||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
import 'package:island/models/realm.dart';
|
||||||
|
|
||||||
part 'drift_db.g.dart';
|
part 'drift_db.g.dart';
|
||||||
|
|
||||||
// Define the database
|
// Define the database
|
||||||
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
@DriftDatabase(
|
||||||
|
tables: [Realms, ChatRooms, ChatMembers, ChatMessages, PostDrafts],
|
||||||
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 9;
|
int get schemaVersion => 10;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -71,6 +74,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
'ALTER TABLE chat_members DROP COLUMN last_typed',
|
'ALTER TABLE chat_members DROP COLUMN last_typed',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (from < 10) {
|
||||||
|
// Add realms table and update chat_rooms foreign key
|
||||||
|
await m.createTable(realms);
|
||||||
|
// The realmId column in chat_rooms already exists, just need to ensure the foreign key constraint
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -92,11 +100,10 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
|
|
||||||
// Migrate existing data if any
|
// Migrate existing data if any
|
||||||
try {
|
try {
|
||||||
final oldDrafts =
|
final oldDrafts = await customSelect(
|
||||||
await customSelect(
|
'SELECT id, post, lastModified FROM post_drafts_old',
|
||||||
'SELECT id, post, lastModified FROM post_drafts_old',
|
readsFrom: {postDrafts},
|
||||||
readsFrom: {postDrafts},
|
).get();
|
||||||
).get();
|
|
||||||
|
|
||||||
for (final row in oldDrafts) {
|
for (final row in oldDrafts) {
|
||||||
final postJson = row.read<String>('post');
|
final postJson = row.read<String>('post');
|
||||||
@@ -150,9 +157,9 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> updateMessageStatus(String id, MessageStatus status) {
|
Future<int> updateMessageStatus(String id, MessageStatus status) {
|
||||||
return (update(chatMessages)..where(
|
return (update(chatMessages)..where((m) => m.id.equals(id))).write(
|
||||||
(m) => m.id.equals(id),
|
ChatMessagesCompanion(status: Value(status)),
|
||||||
)).write(ChatMessagesCompanion(status: Value(status)));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteMessage(String id) {
|
Future<int> deleteMessage(String id) {
|
||||||
@@ -176,29 +183,28 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
|
|
||||||
if (query.isNotEmpty) {
|
if (query.isNotEmpty) {
|
||||||
final searchTerm = '%$query%';
|
final searchTerm = '%$query%';
|
||||||
selectStatement =
|
selectStatement = selectStatement
|
||||||
selectStatement..where(
|
..where(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.content.like(searchTerm) |
|
m.content.like(searchTerm) |
|
||||||
m.meta.like(searchTerm) |
|
m.meta.like(searchTerm) |
|
||||||
m.attachments.like(searchTerm) |
|
m.attachments.like(searchTerm) |
|
||||||
m.type.like(searchTerm),
|
m.type.like(searchTerm),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withAttachments == true) {
|
if (withAttachments == true) {
|
||||||
selectStatement =
|
selectStatement = selectStatement
|
||||||
selectStatement..where((m) => m.attachments.equals('[]').not());
|
..where((m) => m.attachments.equals('[]').not());
|
||||||
}
|
}
|
||||||
|
|
||||||
final messages =
|
final messages =
|
||||||
await (selectStatement
|
await (selectStatement
|
||||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||||
.get();
|
.get();
|
||||||
final messageFutures =
|
final messageFutures = messages
|
||||||
messages
|
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
|
||||||
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
|
.toList();
|
||||||
.toList();
|
|
||||||
return await Future.wait(messageFutures);
|
return await Future.wait(messageFutures);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,9 +240,9 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
final data = jsonDecode(dbMessage.data);
|
final data = jsonDecode(dbMessage.data);
|
||||||
SnChatMember? sender;
|
SnChatMember? sender;
|
||||||
try {
|
try {
|
||||||
final senderRow =
|
final senderRow = await (select(
|
||||||
await (select(chatMembers)
|
chatMembers,
|
||||||
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
)..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
||||||
SnAccount senderAccount;
|
SnAccount senderAccount;
|
||||||
senderAccount = SnAccount.fromJson(senderRow.account);
|
senderAccount = SnAccount.fromJson(senderRow.account);
|
||||||
|
|
||||||
@@ -358,6 +364,20 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RealmsCompanion companionFromRealm(SnRealm realm) {
|
||||||
|
return RealmsCompanion(
|
||||||
|
id: Value(realm.id),
|
||||||
|
name: Value(realm.name),
|
||||||
|
description: Value(realm.description),
|
||||||
|
picture: Value(realm.picture?.toJson()),
|
||||||
|
background: Value(realm.background?.toJson()),
|
||||||
|
accountId: Value(realm.accountId),
|
||||||
|
createdAt: Value(realm.createdAt),
|
||||||
|
updatedAt: Value(realm.updatedAt),
|
||||||
|
deletedAt: Value(realm.deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveChatRooms(
|
Future<void> saveChatRooms(
|
||||||
List<SnChatRoom> rooms, {
|
List<SnChatRoom> rooms, {
|
||||||
bool override = false,
|
bool override = false,
|
||||||
@@ -373,17 +393,35 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
if (idsToRemove.isNotEmpty) {
|
if (idsToRemove.isNotEmpty) {
|
||||||
final idsList = idsToRemove.toList();
|
final idsList = idsToRemove.toList();
|
||||||
// Remove messages
|
// Remove messages
|
||||||
await (delete(chatMessages)
|
await (delete(
|
||||||
..where((t) => t.roomId.isIn(idsList))).go();
|
chatMessages,
|
||||||
|
)..where((t) => t.roomId.isIn(idsList))).go();
|
||||||
// Remove members
|
// Remove members
|
||||||
await (delete(chatMembers)
|
await (delete(
|
||||||
..where((t) => t.chatRoomId.isIn(idsList))).go();
|
chatMembers,
|
||||||
|
)..where((t) => t.chatRoomId.isIn(idsList))).go();
|
||||||
// Remove rooms
|
// Remove rooms
|
||||||
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
|
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Upsert remote rooms
|
// 2. Upsert realms first
|
||||||
|
final realmsToSave = rooms
|
||||||
|
.where((room) => room.realm != null)
|
||||||
|
.map((room) => room.realm!)
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
await batch((batch) {
|
||||||
|
for (final realm in realmsToSave) {
|
||||||
|
batch.insert(
|
||||||
|
realms,
|
||||||
|
companionFromRealm(realm),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Upsert remote rooms
|
||||||
await batch((batch) {
|
await batch((batch) {
|
||||||
for (final room in rooms) {
|
for (final room in rooms) {
|
||||||
batch.insert(
|
batch.insert(
|
||||||
@@ -445,8 +483,9 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<PostDraft?> getPostDraftById(String id) async {
|
Future<PostDraft?> getPostDraftById(String id) async {
|
||||||
return await (select(postDrafts)
|
return await (select(
|
||||||
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
postDrafts,
|
||||||
|
)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveMember(SnChatMember member) async {
|
Future<void> saveMember(SnChatMember member) async {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,21 @@ class ListMapConverter
|
|||||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Realms extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get accountId => text().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
class ChatRooms extends Table {
|
class ChatRooms extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get name => text().nullable()();
|
TextColumn get name => text().nullable()();
|
||||||
@@ -47,7 +62,7 @@ class ChatRooms extends Table {
|
|||||||
boolean().nullable().withDefault(const Constant(false))();
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||||
TextColumn get background => text().map(const MapConverter()).nullable()();
|
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||||
TextColumn get realmId => text().nullable()();
|
TextColumn get realmId => text().references(Realms, #id).nullable()();
|
||||||
TextColumn get accountId => text().nullable()();
|
TextColumn get accountId => text().nullable()();
|
||||||
DateTimeColumn get createdAt => dateTime()();
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
DateTimeColumn get updatedAt => dateTime()();
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
@@ -91,10 +106,9 @@ class ChatMessages extends Table {
|
|||||||
TextColumn get type => text().withDefault(const Constant('text'))();
|
TextColumn get type => text().withDefault(const Constant('text'))();
|
||||||
TextColumn get meta =>
|
TextColumn get meta =>
|
||||||
text().map(const MapConverter()).withDefault(const Constant('{}'))();
|
text().map(const MapConverter()).withDefault(const Constant('{}'))();
|
||||||
TextColumn get membersMentioned =>
|
TextColumn get membersMentioned => text()
|
||||||
text()
|
.map(const ListStringConverter())
|
||||||
.map(const ListStringConverter())
|
.withDefault(const Constant('[]'))();
|
||||||
.withDefault(const Constant('[]'))();
|
|
||||||
DateTimeColumn get editedAt => dateTime().nullable()();
|
DateTimeColumn get editedAt => dateTime().nullable()();
|
||||||
TextColumn get attachments =>
|
TextColumn get attachments =>
|
||||||
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:island/database/drift_db.dart';
|
|||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/database.dart';
|
import 'package:island/pods/database.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
@@ -49,96 +50,10 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
|
|||||||
if (localRoomsData.isNotEmpty) {
|
if (localRoomsData.isNotEmpty) {
|
||||||
final localRooms = await Future.wait(
|
final localRooms = await Future.wait(
|
||||||
localRoomsData.map((row) async {
|
localRoomsData.map((row) async {
|
||||||
final membersRows =
|
final membersRows = await (db.select(
|
||||||
await (db.select(db.chatMembers)
|
db.chatMembers,
|
||||||
..where((m) => m.chatRoomId.equals(row.id))).get();
|
)..where((m) => m.chatRoomId.equals(row.id))).get();
|
||||||
final members =
|
final members = membersRows.map((mRow) {
|
||||||
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<SnChatRoom>()
|
|
||||||
.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<SnChatRoom>()
|
|
||||||
.toList();
|
|
||||||
await db.saveChatRooms(rooms, override: true);
|
|
||||||
return rooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<SnChatRoom>> _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);
|
final account = SnAccount.fromJson(mRow.account);
|
||||||
return SnChatMember(
|
return SnChatMember(
|
||||||
id: mRow.id,
|
id: mRow.id,
|
||||||
@@ -157,6 +72,121 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
|
|||||||
chatRoom: null,
|
chatRoom: null,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).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<SnChatRoom>()
|
||||||
|
.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<SnChatRoom>()
|
||||||
|
.toList();
|
||||||
|
await db.saveChatRooms(rooms, override: true);
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnChatRoom>> _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();
|
||||||
|
|
||||||
|
// Load realm if it exists
|
||||||
|
SnRealm? realm;
|
||||||
|
if (row.realmId != null) {
|
||||||
|
try {
|
||||||
|
final realmRow = await (db.select(
|
||||||
|
db.realms,
|
||||||
|
)..where((r) => r.id.equals(row.realmId!))).getSingleOrNull();
|
||||||
|
if (realmRow != null) {
|
||||||
|
realm = SnRealm(
|
||||||
|
id: realmRow.id,
|
||||||
|
slug: '', // Not stored in DB
|
||||||
|
name: realmRow.name ?? '',
|
||||||
|
description: realmRow.description ?? '',
|
||||||
|
verifiedAs: null, // Not stored in DB
|
||||||
|
verifiedAt: null, // Not stored in DB
|
||||||
|
isCommunity: false, // Not stored in DB
|
||||||
|
isPublic: true, // Not stored in DB
|
||||||
|
picture: realmRow.picture != null
|
||||||
|
? SnCloudFile.fromJson(realmRow.picture!)
|
||||||
|
: null,
|
||||||
|
background: realmRow.background != null
|
||||||
|
? SnCloudFile.fromJson(realmRow.background!)
|
||||||
|
: null,
|
||||||
|
accountId: realmRow.accountId ?? '',
|
||||||
|
createdAt: realmRow.createdAt,
|
||||||
|
updatedAt: realmRow.updatedAt,
|
||||||
|
deletedAt: realmRow.deletedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Realm not found, keep as null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SnChatRoom(
|
return SnChatRoom(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -164,15 +194,15 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
|
|||||||
type: row.type,
|
type: row.type,
|
||||||
isPublic: row.isPublic!,
|
isPublic: row.isPublic!,
|
||||||
isCommunity: row.isCommunity!,
|
isCommunity: row.isCommunity!,
|
||||||
picture:
|
picture: row.picture != null
|
||||||
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
|
? SnCloudFile.fromJson(row.picture!)
|
||||||
background:
|
: null,
|
||||||
row.background != null
|
background: row.background != null
|
||||||
? SnCloudFile.fromJson(row.background!)
|
? SnCloudFile.fromJson(row.background!)
|
||||||
: null,
|
: null,
|
||||||
realmId: row.realmId,
|
realmId: row.realmId,
|
||||||
accountId: row.accountId,
|
accountId: row.accountId,
|
||||||
realm: null,
|
realm: realm,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
deletedAt: row.deletedAt,
|
deletedAt: row.deletedAt,
|
||||||
@@ -192,35 +222,34 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get from local database first
|
// Try to get from local database first
|
||||||
final localRoomData =
|
final localRoomData = await (db.select(
|
||||||
await (db.select(db.chatRooms)
|
db.chatRooms,
|
||||||
..where((r) => r.id.equals(identifier))).getSingleOrNull();
|
)..where((r) => r.id.equals(identifier))).getSingleOrNull();
|
||||||
|
|
||||||
if (localRoomData != null) {
|
if (localRoomData != null) {
|
||||||
// Fetch members for this room
|
// Fetch members for this room
|
||||||
final membersRows =
|
final membersRows = await (db.select(
|
||||||
await (db.select(db.chatMembers)
|
db.chatMembers,
|
||||||
..where((m) => m.chatRoomId.equals(localRoomData.id))).get();
|
)..where((m) => m.chatRoomId.equals(localRoomData.id))).get();
|
||||||
final members =
|
final members = membersRows.map((mRow) {
|
||||||
membersRows.map((mRow) {
|
final account = SnAccount.fromJson(mRow.account);
|
||||||
final account = SnAccount.fromJson(mRow.account);
|
return SnChatMember(
|
||||||
return SnChatMember(
|
id: mRow.id,
|
||||||
id: mRow.id,
|
chatRoomId: mRow.chatRoomId,
|
||||||
chatRoomId: mRow.chatRoomId,
|
accountId: mRow.accountId,
|
||||||
accountId: mRow.accountId,
|
account: account,
|
||||||
account: account,
|
nick: mRow.nick,
|
||||||
nick: mRow.nick,
|
notify: mRow.notify,
|
||||||
notify: mRow.notify,
|
joinedAt: mRow.joinedAt,
|
||||||
joinedAt: mRow.joinedAt,
|
breakUntil: mRow.breakUntil,
|
||||||
breakUntil: mRow.breakUntil,
|
timeoutUntil: mRow.timeoutUntil,
|
||||||
timeoutUntil: mRow.timeoutUntil,
|
status: null,
|
||||||
status: null,
|
createdAt: mRow.createdAt,
|
||||||
createdAt: mRow.createdAt,
|
updatedAt: mRow.updatedAt,
|
||||||
updatedAt: mRow.updatedAt,
|
deletedAt: mRow.deletedAt,
|
||||||
deletedAt: mRow.deletedAt,
|
chatRoom: null,
|
||||||
chatRoom: null,
|
);
|
||||||
);
|
}).toList();
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final localRoom = SnChatRoom(
|
final localRoom = SnChatRoom(
|
||||||
id: localRoomData.id,
|
id: localRoomData.id,
|
||||||
@@ -229,14 +258,12 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
|
|||||||
type: localRoomData.type,
|
type: localRoomData.type,
|
||||||
isPublic: localRoomData.isPublic!,
|
isPublic: localRoomData.isPublic!,
|
||||||
isCommunity: localRoomData.isCommunity!,
|
isCommunity: localRoomData.isCommunity!,
|
||||||
picture:
|
picture: localRoomData.picture != null
|
||||||
localRoomData.picture != null
|
? SnCloudFile.fromJson(localRoomData.picture!)
|
||||||
? SnCloudFile.fromJson(localRoomData.picture!)
|
: null,
|
||||||
: null,
|
background: localRoomData.background != null
|
||||||
background:
|
? SnCloudFile.fromJson(localRoomData.background!)
|
||||||
localRoomData.background != null
|
: null,
|
||||||
? SnCloudFile.fromJson(localRoomData.background!)
|
|
||||||
: null,
|
|
||||||
realmId: localRoomData.realmId,
|
realmId: localRoomData.realmId,
|
||||||
accountId: localRoomData.accountId,
|
accountId: localRoomData.accountId,
|
||||||
realm: null,
|
realm: null,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ final class ChatRoomJoinedNotifierProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$chatRoomJoinedNotifierHash() =>
|
String _$chatRoomJoinedNotifierHash() =>
|
||||||
r'c8092225ba0d9c08b2b5bca6f800f1877303b4ff';
|
r'65961aac28b5188900c4b25308f6fd080a14d5ab';
|
||||||
|
|
||||||
abstract class _$ChatRoomJoinedNotifier
|
abstract class _$ChatRoomJoinedNotifier
|
||||||
extends $AsyncNotifier<List<SnChatRoom>> {
|
extends $AsyncNotifier<List<SnChatRoom>> {
|
||||||
|
|||||||
@@ -14,208 +14,15 @@ import 'package:island/services/event_bus.dart';
|
|||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/chat_room_widgets.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/navigation/fab_menu.dart';
|
import 'package:island/widgets/navigation/fab_menu.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
import 'package:island/pods/chat/chat_room.dart';
|
import 'package:island/pods/chat/chat_room.dart';
|
||||||
|
|
||||||
class ChatRoomListTile extends HookConsumerWidget {
|
|
||||||
final SnChatRoom room;
|
|
||||||
final bool isDirect;
|
|
||||||
final Widget? subtitle;
|
|
||||||
final Widget? trailing;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
const ChatRoomListTile({
|
|
||||||
super.key,
|
|
||||||
required this.room,
|
|
||||||
this.isDirect = false,
|
|
||||||
this.subtitle,
|
|
||||||
this.trailing,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final summary = ref
|
|
||||||
.watch(chatSummaryProvider)
|
|
||||||
.whenData((summaries) => summaries[room.id]);
|
|
||||||
|
|
||||||
var validMembers = room.members ?? [];
|
|
||||||
if (validMembers.isNotEmpty) {
|
|
||||||
final userInfo = ref.watch(userInfoProvider);
|
|
||||||
if (userInfo.value != null) {
|
|
||||||
validMembers = validMembers
|
|
||||||
.where((e) => e.accountId != userInfo.value!.id)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildSubtitle() {
|
|
||||||
if (subtitle != null) return subtitle!;
|
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
children: [
|
|
||||||
...previousChildren,
|
|
||||||
if (currentChild != null) currentChild,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: summary.when(
|
|
||||||
data: (data) => Container(
|
|
||||||
key: const ValueKey('data'),
|
|
||||||
child: data == null
|
|
||||||
? isDirect && room.description == null
|
|
||||||
? Text(
|
|
||||||
validMembers
|
|
||||||
.map((e) => '@${e.account.name}')
|
|
||||||
.join(', '),
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
room.description ?? 'descriptionNone'.tr(),
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
if (data.unreadCount > 0)
|
|
||||||
Text(
|
|
||||||
'unreadMessages'.plural(data.unreadCount),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (data.lastMessage == null)
|
|
||||||
Text(
|
|
||||||
room.description ?? 'descriptionNone'.tr(),
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Row(
|
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
Badge(
|
|
||||||
label: Text(
|
|
||||||
data.lastMessage!.sender.account.nick,
|
|
||||||
),
|
|
||||||
textColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
backgroundColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
(data.lastMessage!.content?.isNotEmpty ?? false)
|
|
||||||
? data.lastMessage!.content!
|
|
||||||
: 'messageNone'.tr(),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text(
|
|
||||||
RelativeTime(
|
|
||||||
context,
|
|
||||||
).format(data.lastMessage!.createdAt),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading: () => Container(
|
|
||||||
key: const ValueKey('loading'),
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final seed = DateTime.now().microsecondsSinceEpoch;
|
|
||||||
final len = 4 + (seed % 17); // 4..20 inclusive
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
var s = seed;
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
||||||
buffer.write(chars[s % chars.length]);
|
|
||||||
}
|
|
||||||
return Skeletonizer(
|
|
||||||
enabled: true,
|
|
||||||
child: Text(buffer.toString()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
error: (_, _) => Container(
|
|
||||||
key: const ValueKey('error'),
|
|
||||||
child: isDirect && room.description == null
|
|
||||||
? Text(
|
|
||||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String titleText;
|
|
||||||
if (isDirect && room.name == null) {
|
|
||||||
if (room.members?.isNotEmpty ?? false) {
|
|
||||||
titleText = validMembers.map((e) => e.account.nick).join(', ');
|
|
||||||
} else {
|
|
||||||
titleText = 'Direct Message';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
titleText = room.name ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: Badge(
|
|
||||||
isLabelVisible: summary.when(
|
|
||||||
data: (data) => (data?.unreadCount ?? 0) > 0,
|
|
||||||
loading: () => false,
|
|
||||||
error: (_, _) => false,
|
|
||||||
),
|
|
||||||
child: (isDirect && room.picture?.id == null)
|
|
||||||
? SplitAvatarWidget(
|
|
||||||
filesId: validMembers
|
|
||||||
.map((e) => e.account.profile.picture?.id)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
: room.picture?.id == null
|
|
||||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
|
||||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
|
||||||
),
|
|
||||||
title: Text(titleText),
|
|
||||||
subtitle: buildSubtitle(),
|
|
||||||
trailing: trailing, // Add this line
|
|
||||||
onTap: () async {
|
|
||||||
// Clear unread count if there are unread messages
|
|
||||||
ref.read(chatSummaryProvider.future).then((summary) {
|
|
||||||
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
|
|
||||||
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onTap?.call();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatListBodyWidget extends HookConsumerWidget {
|
class ChatListBodyWidget extends HookConsumerWidget {
|
||||||
final bool isFloating;
|
final bool isFloating;
|
||||||
final TabController tabController;
|
final TabController tabController;
|
||||||
@@ -648,3 +455,75 @@ class _ChatInvitesSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChatRoomListTile extends HookConsumerWidget {
|
||||||
|
final SnChatRoom room;
|
||||||
|
final bool isDirect;
|
||||||
|
final Widget? subtitle;
|
||||||
|
final Widget? trailing;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const ChatRoomListTile({
|
||||||
|
super.key,
|
||||||
|
required this.room,
|
||||||
|
this.isDirect = false,
|
||||||
|
this.subtitle,
|
||||||
|
this.trailing,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final summary = ref
|
||||||
|
.watch(chatSummaryProvider)
|
||||||
|
.whenData((summaries) => summaries[room.id]);
|
||||||
|
|
||||||
|
var validMembers = room.members ?? [];
|
||||||
|
if (validMembers.isNotEmpty) {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value != null) {
|
||||||
|
validMembers = validMembers
|
||||||
|
.where((e) => e.accountId != userInfo.value!.id)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String titleText;
|
||||||
|
if (isDirect && room.name == null) {
|
||||||
|
if (room.members?.isNotEmpty ?? false) {
|
||||||
|
titleText = validMembers.map((e) => e.account.nick).join(', ');
|
||||||
|
} else {
|
||||||
|
titleText = 'Direct Message';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleText = room.name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: ChatRoomAvatar(
|
||||||
|
room: room,
|
||||||
|
isDirect: isDirect,
|
||||||
|
summary: summary,
|
||||||
|
validMembers: validMembers,
|
||||||
|
),
|
||||||
|
title: Text(titleText),
|
||||||
|
subtitle: ChatRoomSubtitle(
|
||||||
|
room: room,
|
||||||
|
isDirect: isDirect,
|
||||||
|
validMembers: validMembers,
|
||||||
|
summary: summary,
|
||||||
|
subtitle: subtitle,
|
||||||
|
),
|
||||||
|
trailing: trailing, // Add this line
|
||||||
|
onTap: () async {
|
||||||
|
// Clear unread count if there are unread messages
|
||||||
|
ref.read(chatSummaryProvider.future).then((summary) {
|
||||||
|
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
|
||||||
|
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onTap?.call();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
201
lib/widgets/chat_room_widgets.dart
Normal file
201
lib/widgets/chat_room_widgets.dart
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
class ChatRoomAvatar extends StatelessWidget {
|
||||||
|
final SnChatRoom room;
|
||||||
|
final bool isDirect;
|
||||||
|
final AsyncValue<SnChatSummary?> summary;
|
||||||
|
final List<SnChatMember> validMembers;
|
||||||
|
|
||||||
|
const ChatRoomAvatar({
|
||||||
|
super.key,
|
||||||
|
required this.room,
|
||||||
|
required this.isDirect,
|
||||||
|
required this.summary,
|
||||||
|
required this.validMembers,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final avatarChild = (isDirect && room.picture?.id == null)
|
||||||
|
? SplitAvatarWidget(
|
||||||
|
filesId: validMembers
|
||||||
|
.map((e) => e.account.profile.picture?.id)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
: room.picture?.id == null
|
||||||
|
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
|
||||||
|
: ProfilePictureWidget(fileId: room.picture?.id);
|
||||||
|
|
||||||
|
final badgeChild = Badge(
|
||||||
|
isLabelVisible: summary.when(
|
||||||
|
data: (data) => (data?.unreadCount ?? 0) > 0,
|
||||||
|
loading: () => false,
|
||||||
|
error: (_, _) => false,
|
||||||
|
),
|
||||||
|
child: avatarChild,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show realm avatar as small overlay if chat belongs to a realm
|
||||||
|
if (room.realm != null) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
badgeChild,
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.25),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipOval(
|
||||||
|
child: ProfilePictureWidget(file: room.realm!.picture),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badgeChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatRoomSubtitle extends StatelessWidget {
|
||||||
|
final SnChatRoom room;
|
||||||
|
final bool isDirect;
|
||||||
|
final List<SnChatMember> validMembers;
|
||||||
|
final AsyncValue<SnChatSummary?> summary;
|
||||||
|
final Widget? subtitle;
|
||||||
|
|
||||||
|
const ChatRoomSubtitle({
|
||||||
|
super.key,
|
||||||
|
required this.room,
|
||||||
|
required this.isDirect,
|
||||||
|
required this.validMembers,
|
||||||
|
required this.summary,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (subtitle != null) return subtitle!;
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [...previousChildren, if (currentChild != null) currentChild],
|
||||||
|
),
|
||||||
|
child: summary.when(
|
||||||
|
data: (data) => Container(
|
||||||
|
key: const ValueKey('data'),
|
||||||
|
child: data == null
|
||||||
|
? isDirect && room.description == null
|
||||||
|
? Text(
|
||||||
|
validMembers
|
||||||
|
.map((e) => '@${e.account.name}')
|
||||||
|
.join(', '),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
room.description ?? 'descriptionNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (data.unreadCount > 0)
|
||||||
|
Text(
|
||||||
|
'unreadMessages'.plural(data.unreadCount),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (data.lastMessage == null)
|
||||||
|
Text(
|
||||||
|
room.description ?? 'descriptionNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Badge(
|
||||||
|
label: Text(data.lastMessage!.sender.account.nick),
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(data.lastMessage!.content?.isNotEmpty ?? false)
|
||||||
|
? data.lastMessage!.content!
|
||||||
|
: 'messageNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
RelativeTime(
|
||||||
|
context,
|
||||||
|
).format(data.lastMessage!.createdAt),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loading: () => Container(
|
||||||
|
key: const ValueKey('loading'),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final seed = DateTime.now().microsecondsSinceEpoch;
|
||||||
|
final len = 4 + (seed % 17); // 4..20 inclusive
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
var s = seed;
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
||||||
|
buffer.write(chars[s % chars.length]);
|
||||||
|
}
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Text(buffer.toString()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (_, _) => Container(
|
||||||
|
key: const ValueKey('error'),
|
||||||
|
child: isDirect && room.description == null
|
||||||
|
? Text(
|
||||||
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import 'package:island/pods/chat/chat_summary.dart';
|
|||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/chat_room_widgets.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -698,22 +698,11 @@ class _ChatRoomSearchResult extends HookConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Badge(
|
leading: ChatRoomAvatar(
|
||||||
isLabelVisible: summary.maybeWhen(
|
room: room,
|
||||||
data: (data) => (data?.unreadCount ?? 0) > 0,
|
isDirect: isDirect,
|
||||||
orElse: () => false,
|
summary: summary,
|
||||||
),
|
validMembers: validMembers,
|
||||||
child: (isDirect && room.picture?.id == null)
|
|
||||||
? SplitAvatarWidget(
|
|
||||||
filesId: validMembers
|
|
||||||
.map((e) => e.account.profile.picture?.id)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
: room.picture?.id == null
|
|
||||||
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
|
|
||||||
: ProfilePictureWidget(
|
|
||||||
fileId: room.picture?.id,
|
|
||||||
), // Placeholder for now
|
|
||||||
),
|
),
|
||||||
title: Text(titleText),
|
title: Text(titleText),
|
||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
|
|||||||
@@ -2635,6 +2635,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
snow_fall_animation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: snow_fall_animation
|
||||||
|
sha256: "41b97ec1e5c47aeb5886924346f95b1b12e917b85b03d394c925e98a67d8f90e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1+3"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ dependencies:
|
|||||||
hotkey_manager: ^0.2.3
|
hotkey_manager: ^0.2.3
|
||||||
shake: ^3.0.0
|
shake: ^3.0.0
|
||||||
in_app_review: ^2.0.11
|
in_app_review: ^2.0.11
|
||||||
|
snow_fall_animation: ^0.0.1+3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user