feat: галочки у официальных пользователей, ботов и каналов

This commit is contained in:
klockky
2026-05-14 17:06:11 +03:00
parent c2712c999d
commit 0f4ac31ae2
6 changed files with 143 additions and 32 deletions
+28 -3
View File
@@ -38,6 +38,7 @@ class CachedChat {
final bool isOnline;
final int seenTime;
final Map<int, int> participants;
final Set<String> options;
const CachedChat({
required this.id,
@@ -57,8 +58,11 @@ class CachedChat {
required this.isOnline,
required this.seenTime,
required this.participants,
this.options = const {},
});
bool get isOfficial => options.contains('OFFICIAL');
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
id: row['id'] as int,
accountId: row['account_id'] as int,
@@ -76,9 +80,15 @@ class CachedChat {
dontDisturbUntil: row['dont_disturb_until'] as int,
isOnline: (row['is_online'] as int) == 1,
seenTime: row['seen_time'] as int,
participants: _parseParticipants(row['participants'])
participants: _parseParticipants(row['participants']),
options: _decodeOptions(row['options']),
);
static Set<String> _decodeOptions(dynamic raw) {
if (raw is! String || raw.isEmpty) return const {};
return raw.split(',').where((s) => s.isNotEmpty).toSet();
}
Map<String, dynamic> toDbRow() => {
'id': id,
'account_id': accountId,
@@ -96,7 +106,8 @@ class CachedChat {
'dont_disturb_until': dontDisturbUntil,
'is_online': isOnline ? 1 : 0,
'seen_time': seenTime,
'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v)))
'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v))),
'options': options.isEmpty ? null : options.join(','),
};
}
@@ -210,6 +221,7 @@ class ChatsModule {
String? title;
String? iconUrl;
Set<String> options = const {};
if (type == 'DIALOG') {
otherId = _otherParticipantId(chat['participants'], currentUserId);
@@ -218,13 +230,25 @@ class ChatsModule {
if (contact != null) {
title = _nameFromContact(contact);
iconUrl = contact['baseUrl'] as String?;
final contactOpts = contact['options'];
if (contactOpts is List) {
options = contactOpts.whereType<String>().toSet();
}
} else {
title = existing[id]?.title;
iconUrl = existing[id]?.iconUrl;
options = existing[id]?.options ?? const {};
}
} else {
title = chat['title'] as String?;
iconUrl = chat['baseIconUrl'] as String?;
final chatOpts = chat['options'];
if (chatOpts is Map) {
options = {
for (final entry in chatOpts.entries)
if (entry.value == true && entry.key is String) entry.key as String,
};
}
}
final lastMsg = chat['lastMessage'];
@@ -276,7 +300,8 @@ class ChatsModule {
dontDisturbUntil: dontDisturbUntil,
isOnline: isOnline,
seenTime: seenTime,
participants: participants
participants: participants,
options: options,
);
} catch (e) {
logger.e("Ошибка при парсинге чата: $e");
+20
View File
@@ -11,6 +11,7 @@ class CachedContact {
final String? baseUrl;
final String? baseRawUrl;
final int updateTime;
final Set<String> options;
const CachedContact({
required this.id,
@@ -22,8 +23,14 @@ class CachedContact {
this.baseUrl,
this.baseRawUrl,
required this.updateTime,
this.options = const {},
});
bool get isOfficial => options.contains('OFFICIAL');
bool get isBot => options.contains('BOT');
bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
bool get isVerified => isOfficial || isBot || isServiceAccount;
factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
id: row['id'] as int,
accountId: row['account_id'] as int,
@@ -34,7 +41,13 @@ class CachedContact {
baseUrl: row['base_url'] as String?,
baseRawUrl: row['base_raw_url'] as String?,
updateTime: row['update_time'] as int,
options: _decodeOptions(row['options']),
);
static Set<String> _decodeOptions(dynamic raw) {
if (raw is! String || raw.isEmpty) return const {};
return raw.split(',').where((s) => s.isNotEmpty).toSet();
}
}
class ContactsModule {
@@ -126,6 +139,12 @@ class ContactsModule {
lastName = name['lastName'] as String?;
}
final optionsRaw = contact['options'];
String? optionsStr;
if (optionsRaw is List) {
optionsStr = optionsRaw.whereType<String>().join(',');
}
return {
'id': id,
'account_id': accountId,
@@ -136,6 +155,7 @@ class ContactsModule {
'base_url': contact['baseUrl'] as String?,
'base_raw_url': contact['baseRawUrl'] as String?,
'update_time': (contact['updateTime'] as int?) ?? 0,
'options': optionsStr,
};
}
}
+12 -2
View File
@@ -159,7 +159,7 @@ class AppDatabase {
final dbPath = await getDatabasesPath();
return openDatabase(
join(dbPath, 'komet.db'),
version: 8,
version: 9,
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: (db, _) => _createTables(db),
onUpgrade: (db, oldVersion, newVersion) async {
@@ -193,6 +193,14 @@ class AppDatabase {
'ALTER TABLE chats_cache ADD COLUMN participants TEXT',
);
}
if (oldVersion < 9) {
await db.execute(
'ALTER TABLE contacts ADD COLUMN options TEXT',
);
await db.execute(
'ALTER TABLE chats_cache ADD COLUMN options TEXT',
);
}
},
);
}
@@ -230,7 +238,8 @@ class AppDatabase {
photo_id INTEGER,
base_url TEXT,
base_raw_url TEXT,
update_time INTEGER NOT NULL DEFAULT 0
update_time INTEGER NOT NULL DEFAULT 0,
options TEXT
)
''';
@@ -262,6 +271,7 @@ class AppDatabase {
is_online INTEGER NOT NULL DEFAULT 0,
seen_time INTEGER NOT NULL DEFAULT 0,
participants TEXT NOT NULL DEFAULT "",
options TEXT,
PRIMARY KEY (id, account_id)
)
''';
@@ -1102,6 +1102,7 @@ class _ChatListScreenState extends State<ChatListScreen>
isOnline: chat.isOnline,
unreadCount: chat.unreadCount,
isMuted: chat.dontDisturbUntil > 0,
isVerified: chat.isOfficial,
chatType: "DIALOG",
);
} else {
@@ -1134,6 +1135,7 @@ class _ChatListScreenState extends State<ChatListScreen>
isOnline: chat.isOnline,
unreadCount: chat.unreadCount,
isMuted: chat.dontDisturbUntil > 0,
isVerified: chat.isOfficial,
chatType: chat.type,
);
}
@@ -1749,6 +1751,7 @@ class _ChatListScreenState extends State<ChatListScreen>
bool isRead = false,
int unreadCount = 0,
bool isMuted = false,
bool isVerified = false,
String chatType = "CHAT",
}) {
final cs = Theme.of(context).colorScheme;
@@ -1849,16 +1852,33 @@ Navigator.push(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
],
],
),
),
if (isMuted) ...[
+27 -8
View File
@@ -304,14 +304,33 @@ class _ChatScreenState extends State<ChatScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
widget.name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (chat?.isOfficial ?? false) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
],
],
),
Text(
status ?? "",
@@ -100,15 +100,32 @@ class _ContactsTabState extends State<ContactsTab> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nameToDisplay,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
nameToDisplay,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (contact.isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
],
],
),
const SizedBox(height: 4),
Text(