feat: users in chats list, avatars and nicknames, users count in chats

This commit is contained in:
InviseDivine
2026-04-15 17:46:40 +02:00
parent 24b7ff6aff
commit ff8a4c1ade
5 changed files with 363 additions and 185 deletions
+143 -106
View File
@@ -1,4 +1,7 @@
import 'dart:convert';
import '../../core/storage/app_database.dart'; import '../../core/storage/app_database.dart';
import '../../core/utils/logger.dart';
class CachedChat { class CachedChat {
final int id; final int id;
@@ -17,6 +20,7 @@ class CachedChat {
final int dontDisturbUntil; final int dontDisturbUntil;
final bool isOnline; final bool isOnline;
final int seenTime; final int seenTime;
final Map<int, int> participants;
const CachedChat({ const CachedChat({
required this.id, required this.id,
@@ -35,6 +39,7 @@ class CachedChat {
required this.dontDisturbUntil, required this.dontDisturbUntil,
required this.isOnline, required this.isOnline,
required this.seenTime, required this.seenTime,
required this.participants,
}); });
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat( factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
@@ -54,6 +59,8 @@ class CachedChat {
dontDisturbUntil: row['dont_disturb_until'] as int, dontDisturbUntil: row['dont_disturb_until'] as int,
isOnline: (row['is_online'] as int) == 1, isOnline: (row['is_online'] as int) == 1,
seenTime: row['seen_time'] as int, seenTime: row['seen_time'] as int,
// watafuc
participants: Map<String, int>.from(jsonDecode(row['participants'])).map((k, v) => MapEntry(int.parse(k), v))
); );
Map<String, dynamic> toDbRow() => { Map<String, dynamic> toDbRow() => {
@@ -73,6 +80,7 @@ class CachedChat {
'dont_disturb_until': dontDisturbUntil, 'dont_disturb_until': dontDisturbUntil,
'is_online': isOnline ? 1 : 0, 'is_online': isOnline ? 1 : 0,
'seen_time': seenTime, 'seen_time': seenTime,
'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v)))
}; };
} }
@@ -87,51 +95,72 @@ class ChatsModule {
int accountId, int accountId,
int currentUserId, int currentUserId,
) async { ) async {
final chats = data['chats']; try {
if (chats is! List || chats.isEmpty) return; final chats = data['chats'];
if (chats is! List || chats.isEmpty) return;
final contactsMap = _buildContactsMap(data['contacts']); final contactsMap = _buildContactsMap(data['contacts']);
// Config contains mute setup and fav indexes: config -> chats -> id // Config contains mute setup and fav indexes: config -> chats -> id
final configMap = data['config'] is Map ? data['config'] as Map : {}; final configMap = data['config'] is Map ? data['config'] as Map : {};
final chatsConfig = configMap['chats'] is Map final chatsConfig = configMap['chats'] is Map
? configMap['chats'] as Map ? configMap['chats'] as Map
: {}; : {};
// Presence for online statuses // Presence for online statuses
final presenceMap = data['presence'] is Map ? data['presence'] as Map : {}; final presenceMap = data['presence'] is Map ? data['presence'] as Map : {};
final cachedAt = DateTime.now().millisecondsSinceEpoch; final cachedAt = DateTime.now().millisecondsSinceEpoch;
final existingRows = await AppDatabase.loadChats(accountId); final existingRows = await AppDatabase.loadChats(accountId);
final existing = { final existing = {
for (final row in existingRows) for (final row in existingRows)
row['id'] as int: CachedChat.fromDbRow(row), row['id'] as int: CachedChat.fromDbRow(row),
}; };
final rows = chats final rows = chats
.whereType<Map>() .whereType<Map>()
.map( .map(
(c) => _parseChat( (c) => _parseChat(
c.cast<dynamic, dynamic>(), c.cast<dynamic, dynamic>(),
accountId, accountId,
currentUserId, currentUserId,
contactsMap, contactsMap,
chatsConfig, chatsConfig,
presenceMap, presenceMap,
existing, existing,
cachedAt, cachedAt,
), ),
) )
.whereType<CachedChat>() .whereType<CachedChat>()
.map((c) => c.toDbRow()) .map((c) => c.toDbRow())
.toList(); .toList();
if (rows.isNotEmpty) { if (rows.isNotEmpty) {
await AppDatabase.saveChats(rows); await AppDatabase.saveChats(rows);
}
} catch (e) {
logger.e("Ошибка при синке: $e");
} }
} }
static Future<List<CachedChat>> getChats(int accountId) async { static Future<List<CachedChat>> getChats(int accountId) async {
final rows = await AppDatabase.loadChats(accountId); try {
return rows.map(CachedChat.fromDbRow).toList(); final rows = await AppDatabase.loadChats(accountId);
return rows.map(CachedChat.fromDbRow).toList();
} catch (e) {
logger.e("Ошибка при получении чатов: $e");
return [];
}
}
static Future<List<CachedChat>> getChat(int accountId, int chatId) async {
try {
final rows = await AppDatabase.loadChat(accountId, chatId);
return rows.map(CachedChat.fromDbRow).toList();
} catch (e) {
logger.e("Ошибка при получении чата: $e");
return [];
}
} }
static Future<void> clearCache(int accountId) => static Future<void> clearCache(int accountId) =>
@@ -157,80 +186,88 @@ class ChatsModule {
Map<int, CachedChat> existing, Map<int, CachedChat> existing,
int cachedAt, int cachedAt,
) { ) {
final id = chat['id']; try {
if (id is! int) return null; final id = chat['id'];
if (id is! int) return null;
final type = (chat['type'] as String?) ?? 'DIALOG'; final type = (chat['type'] as String?) ?? 'DIALOG';
int? otherId; int? otherId;
String? title; String? title;
String? iconUrl; String? iconUrl;
if (type == 'DIALOG') { if (type == 'DIALOG') {
otherId = _otherParticipantId(chat['participants'], currentUserId); otherId = _otherParticipantId(chat['participants'], currentUserId);
final contact = otherId != null ? contactsMap[otherId] : null; final contact = otherId != null ? contactsMap[otherId] : null;
if (contact != null) { if (contact != null) {
title = _nameFromContact(contact); title = _nameFromContact(contact);
iconUrl = contact['baseUrl'] as String?; iconUrl = contact['baseUrl'] as String?;
} else { } else {
title = existing[id]?.title; title = existing[id]?.title;
iconUrl = existing[id]?.iconUrl; iconUrl = existing[id]?.iconUrl;
} }
} else { } else {
title = chat['title'] as String?; title = chat['title'] as String?;
iconUrl = chat['baseIconUrl'] as String?; iconUrl = chat['baseIconUrl'] as String?;
}
final lastMsg = chat['lastMessage'];
int? lastMsgId;
int? lastMsgTime;
String? lastMsgText;
int? lastMsgSenderId;
if (lastMsg is Map) {
lastMsgId = lastMsg['id'] as int?;
lastMsgTime = lastMsg['time'] as int?;
lastMsgText = lastMsg['text'] as String?;
lastMsgSenderId = lastMsg['sender'] as int?;
}
final config = chatsConfig[id.toString()] ?? chatsConfig[id];
int? favIndex;
int dontDisturbUntil = 0;
if (config is Map) {
favIndex = config['favIndex'] as int?;
dontDisturbUntil = (config['dontDisturbUntil'] as int?) ?? 0;
}
int seenTime = 0;
bool isOnline = false;
if (type == 'DIALOG' && otherId != null) {
final presence = presenceMap[otherId.toString()] ?? presenceMap[otherId];
if (presence is Map) {
seenTime = (presence['seen'] as int?) ?? 0;
isOnline = (presence['status'] as int?) == 1;
}
}
Map<int, int> participants = Map<int, int>.from(chat['participants']);
return CachedChat(
id: id,
accountId: accountId,
type: type,
title: title,
iconUrl: iconUrl,
lastMsgId: lastMsgId,
lastMsgTime: lastMsgTime,
lastMsgText: lastMsgText,
lastMsgSenderId: lastMsgSenderId,
unreadCount: (chat['newMessages'] as int?) ?? 0,
lastEventTime: (chat['lastEventTime'] as int?) ?? 0,
cachedAt: cachedAt,
favIndex: favIndex,
dontDisturbUntil: dontDisturbUntil,
isOnline: isOnline,
seenTime: seenTime,
participants: participants
);
} catch (e) {
logger.e("Ошибка при парсинге чата: $e");
return null;
} }
final lastMsg = chat['lastMessage'];
int? lastMsgId;
int? lastMsgTime;
String? lastMsgText;
int? lastMsgSenderId;
if (lastMsg is Map) {
lastMsgId = lastMsg['id'] as int?;
lastMsgTime = lastMsg['time'] as int?;
lastMsgText = lastMsg['text'] as String?;
lastMsgSenderId = lastMsg['sender'] as int?;
}
final config = chatsConfig[id.toString()] ?? chatsConfig[id];
int? favIndex;
int dontDisturbUntil = 0;
if (config is Map) {
favIndex = config['favIndex'] as int?;
dontDisturbUntil = (config['dontDisturbUntil'] as int?) ?? 0;
}
int seenTime = 0;
bool isOnline = false;
if (type == 'DIALOG' && otherId != null) {
final presence = presenceMap[otherId.toString()] ?? presenceMap[otherId];
if (presence is Map) {
seenTime = (presence['seen'] as int?) ?? 0;
isOnline = (presence['status'] as int?) == 1;
}
}
return CachedChat(
id: id,
accountId: accountId,
type: type,
title: title,
iconUrl: iconUrl,
lastMsgId: lastMsgId,
lastMsgTime: lastMsgTime,
lastMsgText: lastMsgText,
lastMsgSenderId: lastMsgSenderId,
unreadCount: (chat['newMessages'] as int?) ?? 0,
lastEventTime: (chat['lastEventTime'] as int?) ?? 0,
cachedAt: cachedAt,
favIndex: favIndex,
dontDisturbUntil: dontDisturbUntil,
isOnline: isOnline,
seenTime: seenTime,
);
} }
static int? _otherParticipantId(dynamic participants, int currentUserId) { static int? _otherParticipantId(dynamic participants, int currentUserId) {
+31 -10
View File
@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:komet/core/utils/logger.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
@@ -138,7 +139,7 @@ class AppDatabase {
final dbPath = await getDatabasesPath(); final dbPath = await getDatabasesPath();
return openDatabase( return openDatabase(
join(dbPath, 'komet.db'), join(dbPath, 'komet.db'),
version: 7, version: 8,
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'), onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: (db, _) => _createTables(db), onCreate: (db, _) => _createTables(db),
onUpgrade: (db, oldVersion, newVersion) async { onUpgrade: (db, oldVersion, newVersion) async {
@@ -167,6 +168,11 @@ class AppDatabase {
'ALTER TABLE profile ADD COLUMN profile_options TEXT', 'ALTER TABLE profile ADD COLUMN profile_options TEXT',
); );
} }
if (oldVersion < 8) {
await db.execute(
'ALTER TABLE chats_cache ADD COLUMN participants TEXT',
);
}
}, },
); );
} }
@@ -235,6 +241,7 @@ class AppDatabase {
dont_disturb_until INTEGER NOT NULL DEFAULT 0, dont_disturb_until INTEGER NOT NULL DEFAULT 0,
is_online INTEGER NOT NULL DEFAULT 0, is_online INTEGER NOT NULL DEFAULT 0,
seen_time INTEGER NOT NULL DEFAULT 0, seen_time INTEGER NOT NULL DEFAULT 0,
participants TEXT NOT NULL DEFAULT "",
PRIMARY KEY (id, account_id) PRIMARY KEY (id, account_id)
) )
'''; ''';
@@ -375,18 +382,32 @@ class AppDatabase {
// Chats cache // Chats cache
static Future<void> saveChats(List<Map<String, dynamic>> rows) async { static Future<void> saveChats(List<Map<String, dynamic>> rows) async {
final db = await _instance; try {
final batch = db.batch(); final db = await _instance;
for (final row in rows) { final batch = db.batch();
batch.insert( for (final row in rows) {
'chats_cache', batch.insert(
row, 'chats_cache',
conflictAlgorithm: ConflictAlgorithm.replace, row,
); conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
} catch (e) {
logger.e("Ошибка при сохранении чата: $e");
} }
await batch.commit(noResult: true);
} }
static Future<List<Map<String, dynamic>>> loadChat(int accountId, int chatId) async {
final db = await _instance;
return db.query(
'chats_cache',
where: 'account_id = ? AND id = ?',
whereArgs: [accountId, chatId],
orderBy: 'last_event_time DESC',
);
}
static Future<List<Map<String, dynamic>>> loadChats(int accountId) async { static Future<List<Map<String, dynamic>>> loadChats(int accountId) async {
final db = await _instance; final db = await _instance;
return db.query( return db.query(
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:komet/backend/modules/messages.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -15,7 +16,7 @@ import '../../../backend/modules/account.dart';
import '../../../backend/modules/chats.dart'; import '../../../backend/modules/chats.dart';
import '../../../backend/modules/folders.dart'; import '../../../backend/modules/folders.dart';
import '../../../core/storage/app_database.dart'; import '../../../core/storage/app_database.dart';
import '../../../main.dart' show accountModule, api; import '../../../main.dart' show accountModule, api, messagesModule;
class _StoriesScrollPhysics extends BouncingScrollPhysics { class _StoriesScrollPhysics extends BouncingScrollPhysics {
final bool Function() blockPositive; final bool Function() blockPositive;
@@ -60,40 +61,49 @@ class ChatListScreen extends StatefulWidget {
class _ChatListScreenState extends State<ChatListScreen> class _ChatListScreenState extends State<ChatListScreen>
with TickerProviderStateMixin { with TickerProviderStateMixin {
String? _selectedFolderId; String? _selectedFolderId;
List<ChatFolder> _folders = []; List<ChatFolder> _folders = [];
int _currentNavIndex = 0; int _currentNavIndex = 0;
bool _navDragging = false;
double _navDragDx = 0;
double _navDragBaseLeft = 0;
late AnimationController _navPageAnimController;
double _navPageAnimStart = 0; double _navPageAnimStart = 0;
double _navPageAnimEnd = 0; double _navPageAnimEnd = 0;
bool _isFabOpen = false; double _navDragDx = 0;
bool _showCacheWarning = false; double _navDragBaseLeft = 0;
late AnimationController _fabController;
final Set<String> _selectedChats = {};
late PageController _folderPageController;
final List<ScrollController> _folderChatScrollControllers = [];
final List<VoidCallback> _folderChatScrollListenerFns = [];
double _pullRatio = 0.0;
static const double _kStoriesPullTriggerPx = 16.0;
late AnimationController _storiesRevealController;
double _revealAnimBegin = 0.0; double _revealAnimBegin = 0.0;
double _closeAnimBegin = 0.0; double _closeAnimBegin = 0.0;
double _pullRatio = 0.0;
static const double _kStoriesPullTriggerPx = 16.0;
bool _navDragging = false;
bool _isFabOpen = false;
bool _showCacheWarning = false;
bool _storiesAnimClosing = false; bool _storiesAnimClosing = false;
bool _storiesDockedOpen = false; bool _storiesDockedOpen = false;
bool _storiesOverscrollRevealArmed = true; bool _storiesOverscrollRevealArmed = true;
bool _shouldCollapseSearch = false;
bool get _isSelectionMode => _selectedChats.isNotEmpty;
bool? _foldersListKnown;
late AnimationController _navPageAnimController;
late AnimationController _fabController;
late PageController _folderPageController;
late AnimationController _storiesRevealController;
final List<ScrollController> _folderChatScrollControllers = [];
final List<VoidCallback> _folderChatScrollListenerFns = [];
final Set<String> _selectedChats = {};
DateTime _storiesRevealLayoutSettleUntil = DateTime _storiesRevealLayoutSettleUntil =
DateTime.fromMillisecondsSinceEpoch(0); DateTime.fromMillisecondsSinceEpoch(0);
ProfileData? _profile; ProfileData? _profile;
List<CachedChat> _chats = []; List<CachedChat> _chats = [];
SessionState _sessionState = SessionState.disconnected; SessionState _sessionState = SessionState.disconnected;
StreamSubscription? _stateSub; StreamSubscription? _stateSub;
StreamSubscription<LoginStatus>? _loginSub; StreamSubscription<LoginStatus>? _loginSub;
bool? _foldersListKnown;
bool _shouldCollapseSearch = false;
bool get _isSelectionMode => _selectedChats.isNotEmpty;
void _toggleSelection(String chatId) { void _toggleSelection(String chatId) {
setState(() { setState(() {
@@ -1015,18 +1025,60 @@ class _ChatListScreenState extends State<ChatListScreen>
return _buildChatShimmer(); return _buildChatShimmer();
} }
final chat = chats[index]; final chat = chats[index];
return _buildChatItem(
chat.id.toString(), if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) {
chat.title ?? 'Чат', final secondId = chat.participants.entries.where((entry) => entry.key != _profile?.id).first.key;
chat.lastMsgText ?? '', // TODO: Нормальное кеширование контактов
_formatTime(chat.lastMsgTime), final ss = messagesModule.searchContactById(secondId);
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty) final name = ContactCache.get(secondId);
? chat.iconUrl! final avatar = ContactCache.getAvatar(secondId);
: '',
isOnline: chat.isOnline, return _buildChatItem(
unreadCount: chat.unreadCount, chat.id.toString(),
isMuted: chat.dontDisturbUntil > 0, name ?? "Пользователь",
); chat.lastMsgText?.replaceAll('\n', ' ') ?? '',
_formatTime(chat.lastMsgTime),
avatar ?? "",
isOnline: chat.isOnline,
unreadCount: chat.unreadCount,
isMuted: chat.dontDisturbUntil > 0,
);
} else {
if (chat.lastMsgSenderId != null ) {
final ss = messagesModule.searchContactById(chat.lastMsgSenderId!);
}
final name = chat.lastMsgSenderId != null
? ContactCache.get(chat.lastMsgSenderId!)
: null;
final avatar = chat.lastMsgSenderId != null
? ContactCache.getAvatar(chat.lastMsgSenderId!)
: null;
String fullMsg = "";
if (name?.isNotEmpty == true && chat.id != 0) {
fullMsg += "$name: ";
}
if (chat.lastMsgText?.isNotEmpty == true) {
fullMsg += chat.lastMsgText ?? "";
}
return _buildChatItem(
chat.id.toString(),
chat.id == 0 ? "Избранное" : chat.title ?? "Чат",
fullMsg,
_formatTime(chat.lastMsgTime),
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
? chat.iconUrl!
: '',
isOnline: chat.isOnline,
unreadCount: chat.unreadCount,
isMuted: chat.dontDisturbUntil > 0,
);
}
}, childCount: _isInitialLoading ? 10 : chats.length), }, childCount: _isInitialLoading ? 10 : chats.length),
), ),
SliverPadding( SliverPadding(
+14 -2
View File
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:komet/backend/modules/chats.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../backend/api.dart'; import '../../../backend/api.dart';
@@ -35,7 +36,8 @@ class _ChatScreenState extends State<ChatScreen>
late AnimationController _shimmerController; late AnimationController _shimmerController;
List<CachedMessage> _messages = []; List<CachedMessage> _messages = [];
int _myId = 0; int _myId = 0;
CachedChat? chat;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -51,6 +53,11 @@ class _ChatScreenState extends State<ChatScreen>
Future<void> _loadHistory() async { Future<void> _loadHistory() async {
final activeProfile = await AppDatabase.loadActiveProfile(); final activeProfile = await AppDatabase.loadActiveProfile();
_myId = activeProfile?.id ?? 0; _myId = activeProfile?.id ?? 0;
ChatsModule.getChat(_myId, widget.chatId).then((value) {
chat = value[0];
}).catchError((error) {
});
final cachedRows = await AppDatabase.loadMessages( final cachedRows = await AppDatabase.loadMessages(
_myId, _myId,
@@ -240,6 +247,10 @@ class _ChatScreenState extends State<ChatScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
// TODO: Локализация
// TODO: Cклонения
String? status = chat?.type == "CHAT" ? "${chat?.participants.length.toString()} участников" : "last seen recently";
return Scaffold( return Scaffold(
backgroundColor: cs.surface, backgroundColor: cs.surface,
appBar: AppBar( appBar: AppBar(
@@ -284,7 +295,7 @@ class _ChatScreenState extends State<ChatScreen>
), ),
), ),
Text( Text(
'last seen recently', status ?? "",
style: TextStyle( style: TextStyle(
color: cs.onSurfaceVariant, color: cs.onSurfaceVariant,
fontSize: 12, fontSize: 12,
@@ -353,6 +364,7 @@ class _ChatScreenState extends State<ChatScreen>
myId: _myId, myId: _myId,
prevMessage: prevMessage, prevMessage: prevMessage,
nextMessage: nextMessage, nextMessage: nextMessage,
chatType: chat!.type,
); );
}, },
); );
+92 -36
View File
@@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:komet/backend/modules/chats.dart';
import 'package:komet/backend/modules/contacts.dart';
import 'package:komet/main.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../backend/modules/messages.dart'; import '../../backend/modules/messages.dart';
import '../../models/attachment.dart'; import '../../models/attachment.dart';
@@ -37,6 +40,7 @@ class MessageBubble extends StatelessWidget {
final int myId; final int myId;
final CachedMessage? prevMessage; final CachedMessage? prevMessage;
final CachedMessage? nextMessage; final CachedMessage? nextMessage;
final String chatType;
const MessageBubble({ const MessageBubble({
super.key, super.key,
@@ -45,6 +49,7 @@ class MessageBubble extends StatelessWidget {
required this.myId, required this.myId,
this.prevMessage, this.prevMessage,
this.nextMessage, this.nextMessage,
required this.chatType
}); });
bool get isGroupedWithNext { bool get isGroupedWithNext {
@@ -277,6 +282,11 @@ class MessageBubble extends StatelessWidget {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final isDark = cs.brightness == Brightness.dark; final isDark = cs.brightness == Brightness.dark;
// TODO: Нормальное кеширование контактов
final ss = messagesModule.searchContactById(message.senderId);
String? senderAvatar = ContactCache.getAvatar(message.senderId);
String? displaySender = ContactCache.get(message.senderId);
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: isMe ? 60 : 12, left: isMe ? 60 : 12,
@@ -285,22 +295,52 @@ class MessageBubble extends StatelessWidget {
bottom: bottomMargin, bottom: bottomMargin,
), ),
child: Align( child: Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Row(
child: Container( mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
constraints: BoxConstraints( spacing: 8.0,
maxWidth: MediaQuery.of(context).size.width * 0.75, children: [
), if (senderAvatar != null && senderAvatar.isNotEmpty && !isMe && chatType != "DIALOG"
decoration: BoxDecoration( && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
color: isMe CircleAvatar(
? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF)) radius: 15,
: (isDark backgroundImage: NetworkImage(senderAvatar),
? cs.surfaceContainerHighest backgroundColor: cs.primaryContainer,
: const Color(0xFFE9E9EB)), )
borderRadius: _borderRadius, else if (displaySender != null && !isMe && chatType != "DIALOG"
), && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
padding: padding, CircleAvatar(
child: _buildContent(context), radius: 15,
), backgroundColor: cs.primaryContainer,
child: Text(
displaySender!.isNotEmpty
? displaySender[0].toUpperCase()
: '?',
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
),
)
// Заглушка для паддинга
else
CircleAvatar(
radius: 15,
backgroundColor: Color(0x00000000)
),
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
decoration: BoxDecoration(
color: isMe
? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF))
: (isDark
? cs.surfaceContainerHighest
: const Color(0xFFE9E9EB)),
borderRadius: _borderRadius,
),
padding: padding,
child: _buildContent(context),
),
],
)
), ),
); );
} }
@@ -327,30 +367,46 @@ class MessageBubble extends StatelessWidget {
final forwarded = _getForwardedAttachment(); final forwarded = _getForwardedAttachment();
final isForwarded = forwarded != null; final isForwarded = forwarded != null;
return Row( // TODO: Нормальное кеширование контактов
mainAxisSize: MainAxisSize.min, final ss = messagesModule.searchContactById(message.senderId);
crossAxisAlignment: CrossAxisAlignment.end, String? displaySender = ContactCache.get(message.senderId);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Flexible( if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId)
child: isForwarded Text(
? _buildForwardedInlineText(context, forwarded, textColor) displaySender ?? "",
: Text( textAlign: TextAlign.left,
message.text ?? '', // TODO: Получение цветов по хешу ника
style: TextStyle(color: textColor, fontSize: 16, height: 1.3), style: TextStyle(color: cs.onPrimaryContainer)
),
), ),
const SizedBox(width: 8), Row(
Padding( mainAxisSize: MainAxisSize.min,
padding: const EdgeInsets.only(bottom: 2), crossAxisAlignment: CrossAxisAlignment.end,
child: Text( children: [
_formatTime(message.time), Flexible(
style: TextStyle( child: isForwarded
color: textColor.withValues(alpha: 0.7), ? _buildForwardedInlineText(context, forwarded, textColor)
fontSize: 10, : Text(
message.text ?? '',
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
),
), ),
), const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
_formatTime(message.time),
style: TextStyle(
color: textColor.withValues(alpha: 0.7),
fontSize: 10,
),
),
),
if (isMe) ...[const SizedBox(width: 4), _buildStatusIcon(context)],
],
), ),
if (isMe) ...[const SizedBox(width: 4), _buildStatusIcon(context)],
], ],
); );
} }