feat: users in chats list, avatars and nicknames, users count in chats
This commit is contained in:
+143
-106
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user