Merge remote-tracking branch 'origin/feature/FullStack' into fix/tab-switch-freeze
# Conflicts: # lib/frontend/screens/chats/chat_list_screen.dart # lib/frontend/screens/chats/chat_screen.dart # lib/frontend/widgets/message_bubble.dart
This commit is contained in:
+9
-1
@@ -125,4 +125,12 @@ app.*.symbols
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
|
||||
# AI / Agents
|
||||
agents.md
|
||||
.claude/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
@@ -237,6 +237,11 @@ class Api {
|
||||
_dispatcher.registerHandler(opcode, handler);
|
||||
}
|
||||
|
||||
/// Снимает обработчик пушей с указанного опкода.
|
||||
void unregisterPushHandler(int opcode) {
|
||||
_dispatcher.unregisterHandler(opcode);
|
||||
}
|
||||
|
||||
/// Стрим всех входящих пушей от сервера.
|
||||
Stream<Packet> get pushStream => _dispatcher.pushStream;
|
||||
|
||||
@@ -248,6 +253,7 @@ class Api {
|
||||
_connection.dispose();
|
||||
_stateController.close();
|
||||
_sessionExpiredController.close();
|
||||
_handshakeSuccessController.close();
|
||||
}
|
||||
|
||||
// Внутрянка
|
||||
|
||||
@@ -25,7 +25,7 @@ class ChatFolder {
|
||||
|
||||
factory ChatFolder.fromJson(Map<String, dynamic> json) {
|
||||
return ChatFolder(
|
||||
id: json['id'].toString(),
|
||||
id: json['id']?.toString() ?? '',
|
||||
title: json['title']?.toString() ?? '',
|
||||
emoji: json['emoji']?.toString(),
|
||||
include: (json['include'] as List<dynamic>?)
|
||||
|
||||
@@ -27,6 +27,7 @@ class PrivacyConfig {
|
||||
final String chatsInvite;
|
||||
final bool pushNewContacts;
|
||||
final bool unsafeFiles;
|
||||
final String phoneNumberPrivacy;
|
||||
final String inactiveTtl;
|
||||
final bool showReadMark;
|
||||
final bool altKeyboard;
|
||||
@@ -48,6 +49,7 @@ class PrivacyConfig {
|
||||
required this.chatsInvite,
|
||||
required this.pushNewContacts,
|
||||
required this.unsafeFiles,
|
||||
required this.phoneNumberPrivacy,
|
||||
required this.inactiveTtl,
|
||||
required this.showReadMark,
|
||||
required this.altKeyboard,
|
||||
@@ -71,6 +73,7 @@ class PrivacyConfig {
|
||||
chatsInvite: map['CHATS_INVITE']?.toString() ?? 'CONTACTS',
|
||||
pushNewContacts: map['PUSH_NEW_CONTACTS'] ?? false,
|
||||
unsafeFiles: map['UNSAFE_FILES'] ?? true,
|
||||
phoneNumberPrivacy: map['PHONE_NUMBER_PRIVACY']?.toString() ?? 'ALL',
|
||||
inactiveTtl: map['INACTIVE_TTL']?.toString() ?? '6M',
|
||||
showReadMark: map['SHOW_READ_MARK'] ?? true,
|
||||
altKeyboard: map['ALT_KEYBOARD'] ?? false,
|
||||
@@ -94,6 +97,7 @@ class PrivacyConfig {
|
||||
'CHATS_INVITE': chatsInvite,
|
||||
'PUSH_NEW_CONTACTS': pushNewContacts,
|
||||
'UNSAFE_FILES': unsafeFiles,
|
||||
'PHONE_NUMBER_PRIVACY': phoneNumberPrivacy,
|
||||
'INACTIVE_TTL': inactiveTtl,
|
||||
'SHOW_READ_MARK': showReadMark,
|
||||
'ALT_KEYBOARD': altKeyboard,
|
||||
@@ -125,6 +129,7 @@ class PrivacyConfig {
|
||||
chatsInvite: 'CONTACTS',
|
||||
pushNewContacts: false,
|
||||
unsafeFiles: true,
|
||||
phoneNumberPrivacy: 'ALL',
|
||||
inactiveTtl: '6M',
|
||||
showReadMark: true,
|
||||
altKeyboard: false,
|
||||
@@ -289,7 +294,7 @@ class LoginSyncParams {
|
||||
draftsSync: int.tryParse(values[SyncKey.draftsSync] ?? '') ?? 0,
|
||||
bannersSync: int.tryParse(values[SyncKey.bannersSync] ?? '') ?? 0,
|
||||
presenceSync: int.tryParse(values[SyncKey.presenceSync] ?? '') ?? -1,
|
||||
lastLogin: int.parse(lastLogin),
|
||||
lastLogin: int.tryParse(lastLogin) ?? 0,
|
||||
configHash: values[SyncKey.configHash],
|
||||
chatCacheFingerprint: values[SyncKey.chatCacheFingerprint],
|
||||
);
|
||||
@@ -646,19 +651,25 @@ class AccountModule {
|
||||
|
||||
Future<ProfileData> _processProfileUpdate(Packet packet) async {
|
||||
_api.registerPushHandler(Opcode.notifProfile, (p) {});
|
||||
await for (final push in _api.pushStream.where(
|
||||
(p) => p.opcode == Opcode.notifProfile,
|
||||
)) {
|
||||
final payload = push.payload;
|
||||
if (payload is Map) {
|
||||
final profile = payload['profile'];
|
||||
if (profile is Map) {
|
||||
final contact = profile['contact'];
|
||||
if (contact is Map) {
|
||||
return ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
try {
|
||||
await for (final push in _api.pushStream
|
||||
.where((p) => p.opcode == Opcode.notifProfile)
|
||||
.timeout(const Duration(seconds: 15))) {
|
||||
final payload = push.payload;
|
||||
if (payload is Map) {
|
||||
final profile = payload['profile'];
|
||||
if (profile is Map) {
|
||||
final contact = profile['contact'];
|
||||
if (contact is Map) {
|
||||
return ProfileData.fromServerMap(contact.cast<dynamic, dynamic>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} on TimeoutException {
|
||||
throw Exception('Таймаут ожидания обновления профиля');
|
||||
} finally {
|
||||
_api.unregisterPushHandler(Opcode.notifProfile);
|
||||
}
|
||||
throw Exception('Не удалось получить обновлённый профиль');
|
||||
}
|
||||
@@ -940,6 +951,12 @@ class AccountModule {
|
||||
logger.w('Папки чатов: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
await _saveLoginInfo(data, profile.id);
|
||||
} catch (e) {
|
||||
logger.w('Info: $e');
|
||||
}
|
||||
|
||||
return LoginResult(
|
||||
profile: profile,
|
||||
updatedToken: updatedToken,
|
||||
@@ -974,6 +991,115 @@ class AccountModule {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveLoginInfo(
|
||||
Map<dynamic, dynamic> data,
|
||||
int accountId,
|
||||
) async {
|
||||
final contact = data['profile']?['contact'] as Map?;
|
||||
final videoChatHistory = data['videoChatHistory'];
|
||||
final chats = data['chats'] as List?;
|
||||
final config = data['config'] as Map?;
|
||||
final serverConfig = config?['server'] as Map?;
|
||||
final userConfig = config?['user'] as Map?;
|
||||
final yMap = serverConfig?['y-map'] as Map?;
|
||||
final whiteListLinks = serverConfig?['white-list-links'] as List?;
|
||||
final fileUploadUnsupported = serverConfig?['file-upload-unsupported-types'] as List?;
|
||||
final time = data['time'] as int?;
|
||||
|
||||
final info = {
|
||||
'registrationTime': contact?['registrationTime'],
|
||||
'country': contact?['country'],
|
||||
'videoChatHistory': videoChatHistory,
|
||||
'updateTime': contact?['updateTime'],
|
||||
'id': contact?['id'],
|
||||
'chatMarker': chats != null && chats.isNotEmpty
|
||||
? _extractChatMarker(chats.cast<Map>())
|
||||
: null,
|
||||
'time': time,
|
||||
'server': serverConfig != null
|
||||
? _extractServerInfo(serverConfig, yMap, whiteListLinks, fileUploadUnsupported)
|
||||
: null,
|
||||
'user': userConfig != null ? _extractUserConfig(userConfig) : null,
|
||||
};
|
||||
|
||||
await AppDatabase.saveLoginInfo(accountId, jsonEncode(info));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractChatMarker(List<Map> chats) {
|
||||
int? latestTime;
|
||||
for (final chat in chats) {
|
||||
final lastEventTime = chat['lastEventTime'] as int?;
|
||||
if (lastEventTime != null && (latestTime == null || lastEventTime > latestTime)) {
|
||||
latestTime = lastEventTime;
|
||||
}
|
||||
}
|
||||
return {'chatMarker': latestTime};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractServerInfo(
|
||||
Map serverConfig,
|
||||
Map? yMap,
|
||||
List? whiteListLinks,
|
||||
List? fileUploadUnsupported,
|
||||
) {
|
||||
return {
|
||||
'account-removal-enabled': serverConfig['account-removal-enabled'],
|
||||
'image-size': serverConfig['image-size'],
|
||||
'gce': serverConfig['gce'],
|
||||
'gcce': serverConfig['gcce'],
|
||||
'max-msg-length': serverConfig['max-msg-length'],
|
||||
'quotes-enabled': serverConfig['quotes-enabled'],
|
||||
'calls-endpoint': serverConfig['calls-endpoint'],
|
||||
'send-location-enabled': serverConfig['send-location-enabled'],
|
||||
'lgce': serverConfig['lgce'],
|
||||
'wud': serverConfig['wud'],
|
||||
'video-msg-enabled': serverConfig['video-msg-enabled'],
|
||||
'grse': serverConfig['grse'],
|
||||
'edit-timeout': serverConfig['edit-timeout'],
|
||||
'image-quality': serverConfig['image-quality'],
|
||||
'unsafe-files-alert': serverConfig['unsafe-files-alert'],
|
||||
'account-nickname-enabled': serverConfig['account-nickname-enabled'],
|
||||
'mentions_entity_names_limit': serverConfig['mentions_entity_names_limit'],
|
||||
'reactions-enabled': serverConfig['reactions-enabled'],
|
||||
'y-map': yMap != null ? {
|
||||
'tile': yMap['tile'],
|
||||
'geocoder': yMap['geocoder'],
|
||||
'static': yMap['static'],
|
||||
} : null,
|
||||
'white-list-links': whiteListLinks,
|
||||
'file-upload-unsupported-types': fileUploadUnsupported,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractUserConfig(Map userConfig) {
|
||||
return {
|
||||
'CHATS_PUSH_NOTIFICATION': userConfig['CHATS_PUSH_NOTIFICATION'],
|
||||
'PUSH_DETAILS': userConfig['PUSH_DETAILS'],
|
||||
'PUSH_SOUND': userConfig['PUSH_SOUND'],
|
||||
'PHONE_NUMBER_PRIVACY': userConfig['PHONE_NUMBER_PRIVACY'],
|
||||
'INACTIVE_TTL': userConfig['INACTIVE_TTL'],
|
||||
'SHOW_READ_MARK': userConfig['SHOW_READ_MARK'],
|
||||
'AUDIO_TRANSCRIPTION_ENABLED': userConfig['AUDIO_TRANSCRIPTION_ENABLED'],
|
||||
'SEARCH_BY_PHONE': userConfig['SEARCH_BY_PHONE'],
|
||||
'INCOMING_CALL': userConfig['INCOMING_CALL'],
|
||||
'DOUBLE_TAP_REACTION_DISABLED': userConfig['DOUBLE_TAP_REACTION_DISABLED'],
|
||||
'SAFE_MODE_NO_PIN': userConfig['SAFE_MODE_NO_PIN'],
|
||||
'CHATS_PUSH_SOUND': userConfig['CHATS_PUSH_SOUND'],
|
||||
'DOUBLE_TAP_REACTION_VALUE': userConfig['DOUBLE_TAP_REACTION_VALUE'],
|
||||
'FAMILY_PROTECTION': userConfig['FAMILY_PROTECTION'],
|
||||
'HIDDEN': userConfig['HIDDEN'],
|
||||
'CHATS_INVITE': userConfig['CHATS_INVITE'],
|
||||
'PUSH_NEW_CONTACTS': userConfig['PUSH_NEW_CONTACTS'],
|
||||
'UNSAFE_FILES': userConfig['UNSAFE_FILES'],
|
||||
'DONT_DISTURB_UNTIL': userConfig['DONT_DISTURB_UNTIL'],
|
||||
'ALT_KEYBOARD': userConfig['ALT_KEYBOARD'],
|
||||
'CONTENT_LEVEL_ACCESS': userConfig['CONTENT_LEVEL_ACCESS'],
|
||||
'STICKERS_SUGGEST': userConfig['STICKERS_SUGGEST'],
|
||||
'SAFE_MODE': userConfig['SAFE_MODE'],
|
||||
'M_CALL_PUSH_NOTIFICATION': userConfig['M_CALL_PUSH_NOTIFICATION'],
|
||||
};
|
||||
}
|
||||
|
||||
Future<RequestCodeResult> _requestCodeInternal(
|
||||
String phone,
|
||||
AuthRequestType type,
|
||||
|
||||
@@ -92,8 +92,8 @@ class CallsModule {
|
||||
msg['id']?.toString() ??
|
||||
DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
final name = contact?.firstName != null
|
||||
? '${contact!.firstName} ${contact.lastName ?? ''}'.trim()
|
||||
final name = (contact != null && contact.firstName.isNotEmpty)
|
||||
? '${contact.firstName} ${contact.lastName ?? ''}'.trim()
|
||||
: 'Неизвестный';
|
||||
|
||||
extractedCalls.add(
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/storage/app_database.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../api.dart';
|
||||
|
||||
Map<int, int> _parseParticipants(dynamic raw) {
|
||||
try {
|
||||
final decoded = raw is String ? jsonDecode(raw) : raw;
|
||||
if (decoded is Map) {
|
||||
return decoded.map((k, v) => MapEntry(
|
||||
k is int ? k : int.parse(k.toString()),
|
||||
v is int ? v : int.tryParse(v.toString()) ?? 0,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e('Failed to parse participants: $e');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
class CachedChat {
|
||||
final int id;
|
||||
@@ -59,8 +76,7 @@ class CachedChat {
|
||||
dontDisturbUntil: row['dont_disturb_until'] as int,
|
||||
isOnline: (row['is_online'] as int) == 1,
|
||||
seenTime: row['seen_time'] as int,
|
||||
// watafuc
|
||||
participants: Map<String, int>.from(jsonDecode(row['participants'])).map((k, v) => MapEntry(int.parse(k), v))
|
||||
participants: _parseParticipants(row['participants'])
|
||||
);
|
||||
|
||||
Map<String, dynamic> toDbRow() => {
|
||||
@@ -242,7 +258,7 @@ class ChatsModule {
|
||||
isOnline = (presence['status'] as int?) == 1;
|
||||
}
|
||||
}
|
||||
Map<int, int> participants = Map<int, int>.from(chat['participants']);
|
||||
Map<int, int> participants = _parseParticipants(chat['participants']);
|
||||
|
||||
return CachedChat(
|
||||
id: id,
|
||||
@@ -282,12 +298,32 @@ class ChatsModule {
|
||||
static String? _nameFromContact(Map<dynamic, dynamic> contact) {
|
||||
final names = contact['names'];
|
||||
if (names is! List || names.isEmpty) return null;
|
||||
final name =
|
||||
names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.first,
|
||||
)
|
||||
as Map;
|
||||
final nameRaw = names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.firstWhere((n) => n is Map, orElse: () => null),
|
||||
);
|
||||
if (nameRaw is! Map) return null;
|
||||
final name = nameRaw;
|
||||
return name['name'] as String?;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> getChatInfo(Api api, int chatId) async {
|
||||
final packet = await api.sendRequest(Opcode.chatInfo, {
|
||||
'chatIds': [chatId],
|
||||
});
|
||||
if (packet.isError) return null;
|
||||
final payload = packet.payload as Map?;
|
||||
final chats = payload?['chats'] as List?;
|
||||
if (chats == null || chats.isEmpty) return null;
|
||||
return Map<String, dynamic>.from(chats.first as Map);
|
||||
}
|
||||
|
||||
static Future<dynamic> searchById(Api api, int userId) async {
|
||||
final packet = await api.sendRequest(Opcode.publicSearch, {
|
||||
'query': userId.toString(),
|
||||
'from': 0,
|
||||
'count': 10,
|
||||
});
|
||||
return packet.payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@ class ContactsModule {
|
||||
|
||||
final names = contact['names'];
|
||||
if (names is List && names.isNotEmpty) {
|
||||
final name =
|
||||
names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.first,
|
||||
)
|
||||
as Map;
|
||||
final nameRaw = names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.firstWhere((n) => n is Map, orElse: () => null),
|
||||
);
|
||||
if (nameRaw is! Map) return null;
|
||||
final name = nameRaw;
|
||||
firstName = (name['firstName'] as String?) ?? '';
|
||||
lastName = name['lastName'] as String?;
|
||||
}
|
||||
|
||||
@@ -75,13 +75,13 @@ class CachedMessage {
|
||||
}
|
||||
|
||||
return CachedMessage(
|
||||
id: row['id'] as String,
|
||||
accountId: row['account_id'] as int,
|
||||
chatId: row['chat_id'] as int,
|
||||
senderId: row['sender_id'] as int,
|
||||
text: row['text'] as String?,
|
||||
time: row['time'] as int,
|
||||
status: row['status'] as String?,
|
||||
id: row['id']?.toString() ?? '',
|
||||
accountId: row['account_id'] is int ? row['account_id'] as int : int.tryParse(row['account_id']?.toString() ?? '') ?? 0,
|
||||
chatId: row['chat_id'] is int ? row['chat_id'] as int : int.tryParse(row['chat_id']?.toString() ?? '') ?? 0,
|
||||
senderId: row['sender_id'] is int ? row['sender_id'] as int : int.tryParse(row['sender_id']?.toString() ?? '') ?? 0,
|
||||
text: row['text']?.toString(),
|
||||
time: row['time'] is int ? row['time'] as int : int.tryParse(row['time']?.toString() ?? '') ?? 0,
|
||||
status: row['status']?.toString(),
|
||||
payload: payload,
|
||||
attachments: attachments,
|
||||
);
|
||||
@@ -155,7 +155,9 @@ class MessagesModule {
|
||||
}
|
||||
|
||||
if (rows.isNotEmpty) {
|
||||
AppDatabase.saveMessages(rows).ignore();
|
||||
AppDatabase.saveMessages(rows).catchError((e) {
|
||||
debugPrint('saveMessages error: $e');
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -209,15 +211,22 @@ class MessagesModule {
|
||||
id: id,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: (m['sender'] as int?) ?? 0,
|
||||
text: m['text'] as String?,
|
||||
time: (m['time'] as int?) ?? 0,
|
||||
status: m['status'] as String?,
|
||||
senderId: _parseIntField(m['sender']),
|
||||
text: m['text']?.toString(),
|
||||
time: _parseIntField(m['time']),
|
||||
status: m['status']?.toString(),
|
||||
payload: Map<String, dynamic>.from(m.cast()),
|
||||
attachments: attachments,
|
||||
);
|
||||
}
|
||||
|
||||
int _parseIntField(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return int.tryParse(value.toString()) ?? 0;
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
int accountId,
|
||||
int chatId,
|
||||
@@ -250,9 +259,8 @@ class MessagesModule {
|
||||
if (data is! Map) return null;
|
||||
|
||||
final content = data['content'];
|
||||
if (content is String) {
|
||||
return Uri.parse(content).host.isNotEmpty ? null : null;
|
||||
}
|
||||
if (content is Uint8List) return content;
|
||||
if (content is List<int>) return Uint8List.fromList(content);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -288,9 +296,8 @@ class MessagesModule {
|
||||
if (data is! Map) return null;
|
||||
|
||||
final content = data['content'];
|
||||
if (content is String) {
|
||||
return Uri.parse(content).host.isNotEmpty ? null : null;
|
||||
}
|
||||
if (content is Uint8List) return content;
|
||||
if (content is List<int>) return Uint8List.fromList(content);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -326,9 +333,8 @@ class MessagesModule {
|
||||
if (data is! Map) return null;
|
||||
|
||||
final content = data['content'];
|
||||
if (content is String) {
|
||||
return Uri.parse(content).host.isNotEmpty ? null : null;
|
||||
}
|
||||
if (content is Uint8List) return content;
|
||||
if (content is List<int>) return Uint8List.fromList(content);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -356,6 +362,8 @@ class MessagesModule {
|
||||
final cached = ContactCache.get(contactId);
|
||||
if (cached != null) return cached;
|
||||
|
||||
if (_api.state != SessionState.online) return null;
|
||||
|
||||
try {
|
||||
final response = await _api.sendRequest(Opcode.contactInfo, {
|
||||
'contactIds': [contactId],
|
||||
|
||||
@@ -190,6 +190,7 @@ Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) {
|
||||
|
||||
if (pos >= src.length) break;
|
||||
|
||||
if (pos + 1 >= src.length) throw StateError('LZ4: unexpected end of input');
|
||||
final offset = src[pos] | (src[pos + 1] << 8);
|
||||
pos += 2;
|
||||
if (offset == 0) throw StateError('LZ4: offset = 0');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:komet/core/utils/logger.dart';
|
||||
@@ -72,10 +73,15 @@ class ProfileData {
|
||||
final profileOptionsStr = row['profile_options'] as String?;
|
||||
List<int>? profileOptions;
|
||||
if (profileOptionsStr != null && profileOptionsStr.isNotEmpty) {
|
||||
profileOptions = profileOptionsStr
|
||||
.split(',')
|
||||
.map((e) => int.parse(e.trim()))
|
||||
.toList();
|
||||
try {
|
||||
profileOptions = profileOptionsStr
|
||||
.split(',')
|
||||
.where((e) => e.trim().isNotEmpty)
|
||||
.map((e) => int.parse(e.trim()))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
profileOptions = null;
|
||||
}
|
||||
}
|
||||
return ProfileData(
|
||||
id: row['id'] as int,
|
||||
@@ -118,6 +124,7 @@ abstract class SyncKey {
|
||||
static const configHash = 'config_hash';
|
||||
static const chatCacheFingerprint = 'chat_cache_fingerprint';
|
||||
static const serverTime = 'server_time';
|
||||
static const loginInfo = 'login_info';
|
||||
}
|
||||
|
||||
class AppDatabase {
|
||||
@@ -130,8 +137,20 @@ class AppDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
static Completer<Database>? _initCompleter;
|
||||
|
||||
static Future<Database> get _instance async {
|
||||
_db ??= await _open();
|
||||
if (_db != null) return _db!;
|
||||
if (_initCompleter != null) return _initCompleter!.future;
|
||||
_initCompleter = Completer<Database>();
|
||||
try {
|
||||
_db = await _open();
|
||||
_initCompleter!.complete(_db!);
|
||||
} catch (e) {
|
||||
_initCompleter!.completeError(e);
|
||||
_initCompleter = null;
|
||||
rethrow;
|
||||
}
|
||||
return _db!;
|
||||
}
|
||||
|
||||
@@ -374,6 +393,19 @@ class AppDatabase {
|
||||
return rows.first['value'] as String;
|
||||
}
|
||||
|
||||
static Future<void> saveLoginInfo(int accountId, String jsonInfo) async {
|
||||
final db = await _instance;
|
||||
await db.insert('sync_state', {
|
||||
'account_id': accountId,
|
||||
'key': SyncKey.loginInfo,
|
||||
'value': jsonInfo,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
static Future<String?> getLoginInfo(int accountId) async {
|
||||
return getSyncValue(accountId, SyncKey.loginInfo);
|
||||
}
|
||||
|
||||
static Future<void> close() async {
|
||||
await _db?.close();
|
||||
_db = null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SpoofingService {
|
||||
static const String hardcodedAppVersion = '26.8.1';
|
||||
static const String hardcodedAppVersion = '26.15.3';
|
||||
static const int hardcodedBuildNumber = 6606;
|
||||
|
||||
static Future<Map<String, dynamic>?> getSpoofedSessionData() async {
|
||||
|
||||
@@ -33,7 +33,7 @@ class TokenStorage {
|
||||
static Future<String?> readActiveToken() async {
|
||||
final id = await getActiveAccountId();
|
||||
if (id == null) return null;
|
||||
return readToken(id);
|
||||
return await readToken(id);
|
||||
}
|
||||
|
||||
static Future<void> deleteAccount(int accountId) async {
|
||||
|
||||
@@ -99,7 +99,9 @@ class Connection {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
logger.w('Ошибка при закрытии сокета: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_setState(SocketState.disconnected);
|
||||
|
||||
@@ -60,8 +60,8 @@ class ProxyConnector {
|
||||
if (!useAuth) {
|
||||
throw SocketException('SOCKS5: прокси требует аутентификацию');
|
||||
}
|
||||
final usernameBytes = utf8.encode(settings.username!);
|
||||
final passwordBytes = utf8.encode(settings.password!);
|
||||
final usernameBytes = utf8.encode(settings.username ?? '');
|
||||
final passwordBytes = utf8.encode(settings.password ?? '');
|
||||
final authPacket = BytesBuilder()
|
||||
..addByte(0x01)
|
||||
..addByte(usernameBytes.length)
|
||||
@@ -175,7 +175,10 @@ class ProxyConnector {
|
||||
final responseStr = utf8.decode(headerBytes, allowMalformed: true);
|
||||
final statusLine = responseStr.split('\r\n').first;
|
||||
final parts = statusLine.split(' ');
|
||||
final statusCode = parts.length >= 2 ? int.tryParse(parts[1]) ?? 0 : 0;
|
||||
if (parts.length < 2) {
|
||||
throw SocketException('HTTP CONNECT: некорректный ответ: $statusLine');
|
||||
}
|
||||
final statusCode = int.tryParse(parts[1]) ?? 0;
|
||||
if (statusCode != 200) {
|
||||
throw SocketException(
|
||||
'HTTP CONNECT: прокси вернул статус $statusCode',
|
||||
@@ -201,10 +204,17 @@ class ProxyConnector {
|
||||
RawSocket proxySocket,
|
||||
_RawSocketIO io,
|
||||
) async {
|
||||
final server = await RawServerSocket.bind(
|
||||
InternetAddress.loopbackIPv4,
|
||||
0,
|
||||
);
|
||||
RawServerSocket? server;
|
||||
try {
|
||||
server = await RawServerSocket.bind(
|
||||
InternetAddress.loopbackIPv4,
|
||||
0,
|
||||
);
|
||||
} catch (e) {
|
||||
io.dispose();
|
||||
proxySocket.close();
|
||||
rethrow;
|
||||
}
|
||||
final clientSide = await RawSocket.connect(
|
||||
InternetAddress.loopbackIPv4,
|
||||
server.port,
|
||||
|
||||
@@ -8,7 +8,7 @@ class PacketSender {
|
||||
int get currentSeq => _seq;
|
||||
|
||||
int _nextSeq() {
|
||||
_seq = (_seq + 1) % 256;
|
||||
_seq = (_seq + 1) % 65536;
|
||||
return _seq;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,14 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
late AnimationController _shakeController;
|
||||
late Animation<double> _shakeAnimation;
|
||||
|
||||
bool _keyboardScheduled = false;
|
||||
Animation<double>? _routeAnimation;
|
||||
AnimationStatusListener? _routeAnimationListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
|
||||
_shakeController = AnimationController(
|
||||
vsync: this,
|
||||
@@ -56,8 +57,17 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
]).animate(CurvedAnimation(parent: _shakeController, curve: Curves.linear));
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_scheduleKeyboardOpen();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_routeAnimationListener != null) {
|
||||
_routeAnimation?.removeStatusListener(_routeAnimationListener!);
|
||||
}
|
||||
_timer?.cancel();
|
||||
_errorTimer?.cancel();
|
||||
_shakeController.dispose();
|
||||
@@ -66,6 +76,36 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scheduleKeyboardOpen() {
|
||||
if (_keyboardScheduled) return;
|
||||
_keyboardScheduled = true;
|
||||
|
||||
final animation = ModalRoute.of(context)?.animation;
|
||||
if (animation == null || animation.status == AnimationStatus.completed) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _openKeyboard();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_routeAnimation = animation;
|
||||
_routeAnimationListener = (status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
animation.removeStatusListener(_routeAnimationListener!);
|
||||
_routeAnimationListener = null;
|
||||
if (mounted) _openKeyboard();
|
||||
}
|
||||
};
|
||||
animation.addStatusListener(_routeAnimationListener!);
|
||||
}
|
||||
|
||||
void _openKeyboard() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
SystemChannels.textInput.invokeMethod<void>('TextInput.show');
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
_timerSeconds = 30;
|
||||
@@ -215,7 +255,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => _focusNode.requestFocus(),
|
||||
onTap: _openKeyboard,
|
||||
child: FittedBox(
|
||||
child: Row(
|
||||
children: List.generate(6, (index) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -53,7 +55,7 @@ class _ServerSettingsSheetState extends State<ServerSettingsSheet> {
|
||||
await prefs.setString(ServerConfig.prefHostKey, host);
|
||||
await prefs.setInt(ServerConfig.prefPortKey, port);
|
||||
await api.disconnect();
|
||||
api.connect();
|
||||
unawaited(api.connect());
|
||||
final online = await api.stateStream
|
||||
.firstWhere((s) =>
|
||||
s == SessionState.online || s == SessionState.disconnected)
|
||||
|
||||
@@ -31,7 +31,13 @@ class _CallsTabState extends State<CallsTab> {
|
||||
}
|
||||
|
||||
final callsModule = CallsModule(api);
|
||||
final calls = await callsModule.fetchHistory(p.id, p.id);
|
||||
List<CallLogEntry> calls;
|
||||
try {
|
||||
calls = await callsModule.fetchHistory(p.id, p.id);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<CallLogEntry> grouped = [];
|
||||
for (final call in calls) {
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/protocol/opcode_map.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart' as main;
|
||||
import '../../widgets/custom_notification.dart';
|
||||
|
||||
class ChatInfoScreen extends StatefulWidget {
|
||||
final int chatId;
|
||||
final String name;
|
||||
final String imageUrl;
|
||||
final String chatType;
|
||||
|
||||
const ChatInfoScreen({
|
||||
super.key,
|
||||
required this.chatId,
|
||||
required this.name,
|
||||
required this.imageUrl,
|
||||
required this.chatType,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatInfoScreen> createState() => _ChatInfoScreenState();
|
||||
}
|
||||
|
||||
class _ChatInfoScreenState extends State<ChatInfoScreen>
|
||||
with TickerProviderStateMixin {
|
||||
bool _isLoading = true;
|
||||
Map<String, dynamic>? _chatData;
|
||||
late AnimationController _shimmerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shimmerController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
)..repeat();
|
||||
_loadChatData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shimmerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadChatData() async {
|
||||
try {
|
||||
final packet = await main.api.sendRequest(Opcode.chatInfo, {
|
||||
'chatIds': [widget.chatId],
|
||||
});
|
||||
|
||||
final payload = packet.payload as Map?;
|
||||
if (payload == null) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final errorField = payload['error'];
|
||||
if (errorField != null) {
|
||||
String errorMsg = 'Error';
|
||||
if (errorField is Map) {
|
||||
errorMsg = errorField['localizedMessage'] ?? errorField['message'] ?? errorField.toString();
|
||||
} else if (errorField is String) {
|
||||
errorMsg = errorField;
|
||||
}
|
||||
if (mounted) showCustomNotification(context, errorMsg);
|
||||
setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final chats = payload['chats'] as List?;
|
||||
if (chats != null && chats.isNotEmpty) {
|
||||
_chatData = Map<String, dynamic>.from(chats.first as Map);
|
||||
} else if (chats != null && chats.isEmpty) {
|
||||
if (mounted) showCustomNotification(context, 'No data found');
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Error: $e');
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
l10n?.chatInfoTitle ?? 'Info',
|
||||
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? _buildShimmer(cs)
|
||||
: _chatData == null
|
||||
? Center(
|
||||
child: Text(
|
||||
'No data',
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
)
|
||||
: _buildContent(cs, l10n),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShimmer(ColorScheme cs) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...List.generate(
|
||||
10,
|
||||
(_) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme cs, AppLocalizations? l10n) {
|
||||
final chat = _chatData!;
|
||||
final type = chat['type'] as String? ?? '';
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: cs.primaryContainer,
|
||||
),
|
||||
child: widget.imageUrl.isNotEmpty
|
||||
? ClipOval(
|
||||
child: Image.network(widget.imageUrl, fit: BoxFit.cover),
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
widget.name.isNotEmpty
|
||||
? widget.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: cs.onPrimaryContainer,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text(
|
||||
widget.name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (type == 'CHANNEL') ...[
|
||||
_buildSectionTitle('Channel', cs),
|
||||
_buildRow(
|
||||
l10n?.chatInfoSubscribers ?? 'subscribers:',
|
||||
(chat['participantsCount'] as int?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
if ((chat['link'] as String?)?.isNotEmpty ?? false)
|
||||
_buildRow(
|
||||
l10n?.chatInfoLink ?? 'link:',
|
||||
chat['link'] as String,
|
||||
cs,
|
||||
),
|
||||
_buildRow(
|
||||
l10n?.chatInfoOfficial ?? 'official:',
|
||||
(chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
_buildRow(
|
||||
l10n?.chatInfoComments ?? 'comments:',
|
||||
(chat['options']?['COMMENTS'] as bool?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
_buildRow(
|
||||
l10n?.chatInfoAplus ?? 'approved by Roskomnadzor:',
|
||||
(chat['options']?['A_PLUS_CHANNEL'] as bool?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
_buildRow(
|
||||
l10n?.chatInfoSignAdmin ?? 'admin signature:',
|
||||
(chat['options']?['SIGN_ADMIN'] as bool?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
if ((chat['modified'] as int?) != null)
|
||||
_buildRow(
|
||||
l10n?.chatInfoLastChanged ?? 'last changed:',
|
||||
_formatTs(chat['modified'] as int),
|
||||
cs,
|
||||
),
|
||||
if ((chat['created'] as int?) != null)
|
||||
_buildRow(
|
||||
l10n?.chatInfoCreated ?? 'created:',
|
||||
_formatTs(chat['created'] as int),
|
||||
cs,
|
||||
),
|
||||
],
|
||||
|
||||
if (type == 'CHAT') ...[
|
||||
_buildSectionTitle('Chat', cs),
|
||||
_buildRow(
|
||||
l10n?.chatInfoMembers ?? 'members:',
|
||||
(chat['participantsCount'] as int?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
if ((chat['hasBots'] as bool?) ?? false)
|
||||
_buildRow(
|
||||
l10n?.chatInfoHasBots ?? 'has bots:',
|
||||
'true',
|
||||
cs,
|
||||
),
|
||||
if ((chat['blockedParticipantsCount'] as int?) != null &&
|
||||
chat['blockedParticipantsCount'] > 0)
|
||||
_buildRow(
|
||||
l10n?.chatInfoBlockedCount ?? 'blocked in group:',
|
||||
(chat['blockedParticipantsCount'] as int).toString(),
|
||||
cs,
|
||||
),
|
||||
_buildRow(
|
||||
l10n?.chatInfoOfficialStatus ?? 'official status:',
|
||||
(chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-',
|
||||
cs,
|
||||
),
|
||||
if ((chat['modified'] as int?) != null)
|
||||
_buildRow(
|
||||
l10n?.chatInfoLastChanged ?? 'last changed:',
|
||||
_formatTs(chat['modified'] as int),
|
||||
cs,
|
||||
),
|
||||
if ((chat['joinTime'] as int?) != null && chat['joinTime'] != 1)
|
||||
_buildRow(
|
||||
l10n?.chatInfoJoined ?? 'joined:',
|
||||
_formatTs(chat['joinTime'] as int),
|
||||
cs,
|
||||
),
|
||||
if ((chat['created'] as int?) != null)
|
||||
_buildRow(
|
||||
l10n?.chatInfoGroupCreated ?? 'group created:',
|
||||
_formatTs(chat['created'] as int),
|
||||
cs,
|
||||
),
|
||||
if ((chat['owner'] as int?) != null)
|
||||
_buildRow(
|
||||
l10n?.chatInfoGroupOwner ?? 'group owner:',
|
||||
(chat['owner'] as int).toString(),
|
||||
cs,
|
||||
),
|
||||
],
|
||||
|
||||
if (type == 'DIALOG') ...[
|
||||
if ((chat['created'] as int?) != null &&
|
||||
chat['created'] != 0 &&
|
||||
chat['created'] != 1)
|
||||
_buildRow(
|
||||
l10n?.chatInfoDialogStarted ?? 'dialog started:',
|
||||
_formatTs(chat['created'] as int),
|
||||
cs,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(String label, String value, ColorScheme cs) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTs(int ts) {
|
||||
if (ts < 1000000000000) return ts.toString();
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
@@ -665,6 +665,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
..removeListener(_onStoriesRevealTick)
|
||||
..removeStatusListener(_onStoriesRevealStatus)
|
||||
..dispose();
|
||||
_shimmerController.dispose();
|
||||
_folderPageController.dispose();
|
||||
while (_folderChatScrollControllers.isNotEmpty) {
|
||||
final c = _folderChatScrollControllers.removeLast();
|
||||
@@ -1070,6 +1071,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
chatType: "DIALOG",
|
||||
);
|
||||
} else {
|
||||
final name = chat.lastMsgSenderId != null
|
||||
@@ -1101,6 +1103,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
chatType: chat.type,
|
||||
);
|
||||
}
|
||||
}, childCount: _isInitialLoading ? 10 : chats.length),
|
||||
@@ -1282,6 +1285,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
duration: _navDragging
|
||||
@@ -1714,6 +1718,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
bool isRead = false,
|
||||
int unreadCount = 0,
|
||||
bool isMuted = false,
|
||||
String chatType = "CHAT",
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isSelected = _selectedChats.contains(id);
|
||||
@@ -1723,16 +1728,17 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
if (_isSelectionMode) {
|
||||
_toggleSelection(id);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
chatId: int.parse(id),
|
||||
name: name,
|
||||
imageUrl: imageUrl,
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
chatId: int.parse(id),
|
||||
name: name,
|
||||
imageUrl: imageUrl,
|
||||
chatType: chatType,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _toggleSelection(id),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/frontend/screens/chats/chat_info_screen.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../backend/api.dart';
|
||||
@@ -15,12 +16,14 @@ class ChatScreen extends StatefulWidget {
|
||||
final int chatId;
|
||||
final String name;
|
||||
final String imageUrl;
|
||||
final String chatType;
|
||||
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.chatId,
|
||||
required this.name,
|
||||
required this.imageUrl,
|
||||
required this.chatType,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -255,71 +258,84 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
String? status = chat?.type == "CHAT" ? "${chat?.participants.length.toString()} участников" : "last seen recently";
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surfaceContainerHigh,
|
||||
foregroundColor: cs.onSurface,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back, weight: 400),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
if (widget.imageUrl.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundImage: CachedNetworkImageProvider(widget.imageUrl),
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Outfit',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
status ?? "",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(kToolbarHeight),
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatInfoScreen(
|
||||
chatId: widget.chatId,
|
||||
name: widget.name,
|
||||
imageUrl: widget.imageUrl,
|
||||
chatType: widget.chatType)
|
||||
)
|
||||
),
|
||||
child: AppBar(
|
||||
backgroundColor: cs.surfaceContainerHigh,
|
||||
foregroundColor: cs.onSurface,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back, weight: 400),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.call, weight: 400),
|
||||
onPressed: () {},
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
if (widget.imageUrl.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundImage: CachedNetworkImageProvider(widget.imageUrl),
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Outfit',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
status ?? "",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.call, weight: 400),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert, weight: 400),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert, weight: 400),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -366,7 +382,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
myId: _myId,
|
||||
prevMessage: prevMessage,
|
||||
nextMessage: nextMessage,
|
||||
chatType: chat!.type,
|
||||
chatType: chat?.type ?? 'CHAT',
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -470,6 +486,43 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
Widget _buildInputArea(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final mutedIcon = cs.onSurfaceVariant.withValues(alpha: 0.85);
|
||||
|
||||
if (widget.chatType == "CHANNEL") {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.alphaBlend(
|
||||
cs.surfaceContainerHighest.withValues(alpha: 0.92),
|
||||
cs.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(
|
||||
color: cs.outlineVariant.withValues(alpha: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Отключить уведомления',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
|
||||
@@ -1,11 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../backend/modules/chats.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class DebugMenuScreen extends StatelessWidget {
|
||||
class DebugMenuScreen extends StatefulWidget {
|
||||
const DebugMenuScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DebugMenuScreen> createState() => _DebugMenuScreenState();
|
||||
}
|
||||
|
||||
class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
final _idController = TextEditingController();
|
||||
String? _searchResult;
|
||||
bool _isSearching = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_idController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _search() async {
|
||||
final id = int.tryParse(_idController.text);
|
||||
if (id == null) return;
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_searchResult = null;
|
||||
});
|
||||
try {
|
||||
final result = await ChatsModule.searchById(api, id);
|
||||
logger.i('searchById result: $result');
|
||||
if (!mounted) return;
|
||||
if (result is Map && result.containsKey('error')) {
|
||||
final errorMsg = result['localizedMessage'] ?? result['message'] ?? result['error'] ?? 'Error';
|
||||
setState(() => _searchResult = 'Error: $errorMsg');
|
||||
} else if (result is Map) {
|
||||
setState(() => _searchResult = result.toString());
|
||||
} else {
|
||||
setState(() => _searchResult = result?.toString() ?? 'null');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _searchResult = 'Exception: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSearching = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -115,10 +159,92 @@ class DebugMenuScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Поиск по ID (opcode 60)',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _idController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Введите user ID',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _search(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FilledButton(
|
||||
onPressed: _isSearching ? null : _search,
|
||||
child: _isSearching
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Symbols.search, size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_searchResult != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
_searchResult!,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 120)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,8 +271,9 @@ class _DevicesScreenState extends State<DevicesScreen>
|
||||
setState(() => _loadingIps.add(id));
|
||||
}
|
||||
|
||||
HttpClient? client;
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 5);
|
||||
final request = await client.getUrl(
|
||||
Uri.parse(
|
||||
@@ -296,6 +297,8 @@ class _DevicesScreenState extends State<DevicesScreen>
|
||||
setState(() => _loadingIps.remove(id));
|
||||
showCustomNotification(context, 'Ошибка IP: $e');
|
||||
}
|
||||
} finally {
|
||||
client?.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
|
||||
class InfoScreen extends StatefulWidget {
|
||||
const InfoScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InfoScreen> createState() => _InfoScreenState();
|
||||
}
|
||||
|
||||
class _InfoScreenState extends State<InfoScreen> {
|
||||
bool _isLoading = true;
|
||||
Map<String, dynamic>? _info;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
try {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
final jsonStr = await AppDatabase.getLoginInfo(accountId);
|
||||
if (jsonStr != null) {
|
||||
setState(() => _info = jsonDecode(jsonStr) as Map<String, dynamic>);
|
||||
}
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Error: $e');
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
l10n?.infoTitle ?? 'Info',
|
||||
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _info == null
|
||||
? Center(
|
||||
child: Text(
|
||||
'No data',
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
)
|
||||
: _buildContent(cs, l10n!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme cs, AppLocalizations l10n) {
|
||||
final info = _info!;
|
||||
final server = info['server'] as Map<String, dynamic>?;
|
||||
final user = info['user'] as Map<String, dynamic>?;
|
||||
final yMap = server?['y-map'] as Map<String, dynamic>?;
|
||||
|
||||
final accountKeys = <String, String>{
|
||||
'registrationTime': l10n.infoRegistrationTime,
|
||||
'country': l10n.infoCountry,
|
||||
'videoChatHistory': l10n.infoVideoChatHistory,
|
||||
'updateTime': l10n.infoUpdateTime,
|
||||
'id': l10n.infoId,
|
||||
'chatMarker': l10n.infoChatMarker,
|
||||
};
|
||||
|
||||
final serverKeys = <String, String>{
|
||||
'account-removal-enabled': l10n.infoAccountRemovalEnabled,
|
||||
'image-size': l10n.infoImageSize,
|
||||
'gce': l10n.infoGce,
|
||||
'gcce': l10n.infoGcce,
|
||||
'max-msg-length': l10n.infoMaxMsgLength,
|
||||
'quotes-enabled': l10n.infoQuotesEnabled,
|
||||
'calls-endpoint': l10n.infoCallsEndpoint,
|
||||
'send-location-enabled': l10n.infoSendLocationEnabled,
|
||||
'lgce': l10n.infoLgce,
|
||||
'wud': l10n.infoWud,
|
||||
'video-msg-enabled': l10n.infoVideoMsgEnabled,
|
||||
'grse': l10n.infoGrse,
|
||||
'edit-timeout': l10n.infoEditTimeout,
|
||||
'image-quality': l10n.infoImageQuality,
|
||||
'unsafe-files-alert': l10n.infoUnsafeFilesAlert,
|
||||
'account-nickname-enabled': l10n.infoAccountNicknameEnabled,
|
||||
'mentions_entity_names_limit': l10n.infoMentionsEntityNamesLimit,
|
||||
'reactions-enabled': l10n.infoReactionsEnabled,
|
||||
};
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSectionTitle(l10n.infoAccountSection, cs),
|
||||
...accountKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(info[e.key]), cs)),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionTitle(l10n.infoServerSection, cs),
|
||||
...serverKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(server?[e.key]), cs)),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionTitle(l10n.infoYMapSection, cs),
|
||||
_buildRow('tile', l10n.infoTile, yMap?['tile']?.toString() ?? '-', cs),
|
||||
_buildRow('geocoder', l10n.infoGeocoder, yMap?['geocoder']?.toString() ?? '-', cs),
|
||||
_buildRow('static', l10n.infoStatic, yMap?['static']?.toString() ?? '-', cs),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionTitle(l10n.infoFileUploadTypes, cs),
|
||||
_buildListRow(server?['file-upload-unsupported-types'] as List?, cs),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionTitle(l10n.infoWhiteListLinks, cs),
|
||||
_buildListRow(server?['white-list-links'] as List?, cs),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionTitle(l10n.infoUserSection, cs),
|
||||
if (user != null)
|
||||
...user.entries
|
||||
.where((e) => e.value != null)
|
||||
.map((e) => _buildRow(e.key, e.key, e.value.toString(), cs)),
|
||||
|
||||
const SizedBox(height: 120),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(String key, String label, String value, ColorScheme cs) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListRow(List? items, ColorScheme cs) {
|
||||
if (items == null || items.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: items
|
||||
.map(
|
||||
(item) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
item.toString(),
|
||||
style: TextStyle(fontSize: 13, color: cs.onSurface),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatValue(dynamic value) {
|
||||
if (value == null) return '-';
|
||||
if (value is Map && value.containsKey('chatMarker')) {
|
||||
final ts = value['chatMarker'] as int?;
|
||||
if (ts != null) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
if (value is int && value > 1000000000000) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
if (value is int && value > 86400) {
|
||||
final weeks = value ~/ 604800;
|
||||
final days = (value % 604800) ~/ 86400;
|
||||
if (weeks > 0) {
|
||||
return '$weeks ${_w(weeks)} ${days > 0 ? '$days ${_d(days)}' : ''}'.trim();
|
||||
}
|
||||
final h = value ~/ 3600;
|
||||
final m = (value % 3600) ~/ 60;
|
||||
if (h > 0) return '${h}h ${m}m';
|
||||
return '${m}m';
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
String _w(int n) {
|
||||
final m = n % 10;
|
||||
if (m == 1 && n != 11) return 'нед';
|
||||
if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'нед';
|
||||
return 'нед';
|
||||
}
|
||||
|
||||
String _d(int n) {
|
||||
final m = n % 10;
|
||||
if (m == 1 && n != 11) return 'дн';
|
||||
if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'дн';
|
||||
return 'дн';
|
||||
}
|
||||
}
|
||||
@@ -222,6 +222,7 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
case 'CONTACTS':
|
||||
return 'Мои контакты';
|
||||
case 'NONE':
|
||||
case 'NOBODY':
|
||||
return 'Никто';
|
||||
default:
|
||||
return value;
|
||||
@@ -481,9 +482,31 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
icon: Icons.visibility_off_outlined,
|
||||
label: 'Видеть статус «в сети»',
|
||||
value: _privacyConfig?.hidden == true ? 'Никто' : 'Мои контакты',
|
||||
isLast: true,
|
||||
isLast: false,
|
||||
onTap: () => _showHiddenStatusSheet(context, cs),
|
||||
),
|
||||
_buildOptionRow(
|
||||
cs,
|
||||
icon: Symbols.contact_page,
|
||||
label: 'Видеть мой номер',
|
||||
value: _getPrivacyLabel(
|
||||
_privacyConfig?.phoneNumberPrivacy ?? 'ALL',
|
||||
),
|
||||
isLast: true,
|
||||
onTap: () => _showOptionSheet(
|
||||
context,
|
||||
cs,
|
||||
title: 'Видеть мой номер',
|
||||
currentValue: _privacyConfig?.phoneNumberPrivacy ?? 'ALL',
|
||||
options: const [
|
||||
('ALL', 'Все'),
|
||||
('CONTACTS', 'Мои контакты'),
|
||||
('NOBODY', 'Никто'),
|
||||
],
|
||||
onSelect: (value) =>
|
||||
_updateSetting('PHONE_NUMBER_PRIVACY', value),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../../l10n/app_localizations.dart';
|
||||
import '../auth/proxy_settings_sheet.dart';
|
||||
import 'debug_menu_screen.dart';
|
||||
import 'devices_screen.dart';
|
||||
import 'info_screen.dart';
|
||||
import 'security_screen.dart';
|
||||
import 'spoof_screen.dart';
|
||||
|
||||
@@ -97,15 +98,27 @@ class _SettingsTabState extends State<SettingsTab> {
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: _buildSection(
|
||||
child: _buildSection(
|
||||
context,
|
||||
cs,
|
||||
items: const [
|
||||
_SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'),
|
||||
_SettingsItem(
|
||||
items: [
|
||||
const _SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'),
|
||||
const _SettingsItem(
|
||||
icon: Symbols.language,
|
||||
label: 'Войти в Сферум',
|
||||
),
|
||||
_SettingsItem(
|
||||
icon: Symbols.info,
|
||||
label: 'Info',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const InfoScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -290,66 +290,70 @@ class MessageBubble extends StatelessWidget {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
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(
|
||||
padding: EdgeInsets.only(
|
||||
left: isMe ? 60 : 12,
|
||||
right: isMe ? 12 : 60,
|
||||
top: topMargin,
|
||||
bottom: bottomMargin,
|
||||
),
|
||||
child: Align(
|
||||
child: Row(
|
||||
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (senderAvatar != null && senderAvatar.isNotEmpty && !isMe && chatType != "DIALOG"
|
||||
&& nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundImage: CachedNetworkImageProvider(senderAvatar),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
else if (displaySender != null && !isMe && chatType != "DIALOG"
|
||||
&& nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
displaySender!.isNotEmpty
|
||||
? displaySender[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
|
||||
return GestureDetector(
|
||||
// TODO: действия с сообщением
|
||||
onTap: () => print("test"),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: isMe ? 12 : 12,
|
||||
right: isMe ? 12 : 12,
|
||||
top: topMargin,
|
||||
bottom: bottomMargin,
|
||||
),
|
||||
child: Align(
|
||||
child: Row(
|
||||
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
spacing: 8,
|
||||
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!isMe && chatType == "CHAT" && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
|
||||
...(senderAvatar != null && senderAvatar.isNotEmpty)
|
||||
? [
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundImage: CachedNetworkImageProvider(senderAvatar),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
]
|
||||
: [
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
displaySender != null && displaySender.isNotEmpty
|
||||
? displaySender[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
|
||||
),
|
||||
)
|
||||
]
|
||||
else if (!isMe && chatType != "CHAT")
|
||||
SizedBox(width: 0)
|
||||
else if (!isMe)
|
||||
CircleAvatar(radius: 15, backgroundColor: Color(0x00000000)),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
)
|
||||
// Заглушка для паддинга
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: Color(0x00000000)
|
||||
decoration: BoxDecoration(
|
||||
color: isMe
|
||||
? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF))
|
||||
: (isDark
|
||||
? cs.surfaceContainerHighest
|
||||
: const Color(0xFFE9E9EB)),
|
||||
borderRadius: _borderRadius,
|
||||
),
|
||||
padding: padding,
|
||||
child: _buildContent(context),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,18 +387,15 @@ class MessageBubble extends StatelessWidget {
|
||||
final forwarded = _getForwardedAttachment();
|
||||
final isForwarded = forwarded != null && !isForwardedContact;
|
||||
|
||||
// TODO: Нормальное кеширование контактов
|
||||
final ss = messagesModule.searchContactById(message.senderId);
|
||||
String? displaySender = ContactCache.get(message.senderId);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId)
|
||||
if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId && chatType == "CHAT")
|
||||
Text(
|
||||
displaySender ?? "",
|
||||
textAlign: TextAlign.left,
|
||||
// TODO: Получение цветов по хешу ника
|
||||
style: TextStyle(color: cs.onPrimaryContainer)
|
||||
),
|
||||
Row(
|
||||
|
||||
+56
-1
@@ -104,5 +104,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"profileMenuSpoof": "Spoofing"
|
||||
"profileMenuSpoof": "Spoofing",
|
||||
"infoTitle": "Info",
|
||||
"infoAccountSection": "Account",
|
||||
"infoServerSection": "Server",
|
||||
"infoUserSection": "User",
|
||||
"infoYMapSection": "Y-Map",
|
||||
"infoFileUploadTypes": "file-upload-unsupported-types",
|
||||
"infoWhiteListLinks": "white-list-links",
|
||||
"infoRegistrationTime": "registrationTime",
|
||||
"infoCountry": "country",
|
||||
"infoVideoChatHistory": "videoChatHistory",
|
||||
"infoUpdateTime": "updateTime",
|
||||
"infoId": "id",
|
||||
"infoChatMarker": "chatMarker",
|
||||
"infoAccountRemovalEnabled": "account-removal-enabled",
|
||||
"infoImageSize": "image-size",
|
||||
"infoGce": "gce",
|
||||
"infoGcce": "gcce",
|
||||
"infoMaxMsgLength": "max-msg-length",
|
||||
"infoQuotesEnabled": "quotes-enabled",
|
||||
"infoCallsEndpoint": "calls-endpoint",
|
||||
"infoSendLocationEnabled": "send-location-enabled",
|
||||
"infoLgce": "lgce",
|
||||
"infoWud": "wud",
|
||||
"infoVideoMsgEnabled": "video-msg-enabled",
|
||||
"infoGrse": "grse",
|
||||
"infoEditTimeout": "edit-timeout",
|
||||
"infoImageQuality": "image-quality",
|
||||
"infoUnsafeFilesAlert": "unsafe-files-alert",
|
||||
"infoAccountNicknameEnabled": "account-nickname-enabled",
|
||||
"infoMentionsEntityNamesLimit": "mentions_entity_names_limit",
|
||||
"infoReactionsEnabled": "reactions-enabled",
|
||||
"infoTile": "tile",
|
||||
"infoGeocoder": "geocoder",
|
||||
"infoStatic": "static",
|
||||
"chatInfoSubscribers": "subscribers:",
|
||||
"chatInfoInvitedBy": "invited by:",
|
||||
"chatInfoLink": "link:",
|
||||
"chatInfoOfficial": "official:",
|
||||
"chatInfoComments": "comments:",
|
||||
"chatInfoAplus": "approved by Roskomnadzor:",
|
||||
"chatInfoSignAdmin": "admin signature:",
|
||||
"chatInfoLastChanged": "last changed:",
|
||||
"chatInfoJoinTime": "joined:",
|
||||
"chatInfoCreated": "created:",
|
||||
"chatInfoTitle": "Info",
|
||||
"chatInfoMembers": "members:",
|
||||
"chatInfoLastSeen": "last seen recently",
|
||||
"chatInfoHasBots": "has bots:",
|
||||
"chatInfoBlockedCount": "blocked in group:",
|
||||
"chatInfoOfficialStatus": "official status:",
|
||||
"chatInfoLastChanged": "last changed:",
|
||||
"chatInfoJoined": "joined:",
|
||||
"chatInfoGroupCreated": "group created:",
|
||||
"chatInfoGroupOwner": "group owner:",
|
||||
"chatInfoDialogStarted": "dialog started:"
|
||||
}
|
||||
|
||||
@@ -631,6 +631,330 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Spoofing'**
|
||||
String get profileMenuSpoof;
|
||||
|
||||
/// No description provided for @infoTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Info'**
|
||||
String get infoTitle;
|
||||
|
||||
/// No description provided for @infoAccountSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account'**
|
||||
String get infoAccountSection;
|
||||
|
||||
/// No description provided for @infoServerSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server'**
|
||||
String get infoServerSection;
|
||||
|
||||
/// No description provided for @infoUserSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'User'**
|
||||
String get infoUserSection;
|
||||
|
||||
/// No description provided for @infoYMapSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Y-Map'**
|
||||
String get infoYMapSection;
|
||||
|
||||
/// No description provided for @infoFileUploadTypes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'file-upload-unsupported-types'**
|
||||
String get infoFileUploadTypes;
|
||||
|
||||
/// No description provided for @infoWhiteListLinks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'white-list-links'**
|
||||
String get infoWhiteListLinks;
|
||||
|
||||
/// No description provided for @infoRegistrationTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'registrationTime'**
|
||||
String get infoRegistrationTime;
|
||||
|
||||
/// No description provided for @infoCountry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'country'**
|
||||
String get infoCountry;
|
||||
|
||||
/// No description provided for @infoVideoChatHistory.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'videoChatHistory'**
|
||||
String get infoVideoChatHistory;
|
||||
|
||||
/// No description provided for @infoUpdateTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'updateTime'**
|
||||
String get infoUpdateTime;
|
||||
|
||||
/// No description provided for @infoId.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'id'**
|
||||
String get infoId;
|
||||
|
||||
/// No description provided for @infoChatMarker.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'chatMarker'**
|
||||
String get infoChatMarker;
|
||||
|
||||
/// No description provided for @infoAccountRemovalEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'account-removal-enabled'**
|
||||
String get infoAccountRemovalEnabled;
|
||||
|
||||
/// No description provided for @infoImageSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'image-size'**
|
||||
String get infoImageSize;
|
||||
|
||||
/// No description provided for @infoGce.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'gce'**
|
||||
String get infoGce;
|
||||
|
||||
/// No description provided for @infoGcce.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'gcce'**
|
||||
String get infoGcce;
|
||||
|
||||
/// No description provided for @infoMaxMsgLength.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'max-msg-length'**
|
||||
String get infoMaxMsgLength;
|
||||
|
||||
/// No description provided for @infoQuotesEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'quotes-enabled'**
|
||||
String get infoQuotesEnabled;
|
||||
|
||||
/// No description provided for @infoCallsEndpoint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'calls-endpoint'**
|
||||
String get infoCallsEndpoint;
|
||||
|
||||
/// No description provided for @infoSendLocationEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'send-location-enabled'**
|
||||
String get infoSendLocationEnabled;
|
||||
|
||||
/// No description provided for @infoLgce.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'lgce'**
|
||||
String get infoLgce;
|
||||
|
||||
/// No description provided for @infoWud.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'wud'**
|
||||
String get infoWud;
|
||||
|
||||
/// No description provided for @infoVideoMsgEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'video-msg-enabled'**
|
||||
String get infoVideoMsgEnabled;
|
||||
|
||||
/// No description provided for @infoGrse.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'grse'**
|
||||
String get infoGrse;
|
||||
|
||||
/// No description provided for @infoEditTimeout.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'edit-timeout'**
|
||||
String get infoEditTimeout;
|
||||
|
||||
/// No description provided for @infoImageQuality.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'image-quality'**
|
||||
String get infoImageQuality;
|
||||
|
||||
/// No description provided for @infoUnsafeFilesAlert.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'unsafe-files-alert'**
|
||||
String get infoUnsafeFilesAlert;
|
||||
|
||||
/// No description provided for @infoAccountNicknameEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'account-nickname-enabled'**
|
||||
String get infoAccountNicknameEnabled;
|
||||
|
||||
/// No description provided for @infoMentionsEntityNamesLimit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'mentions_entity_names_limit'**
|
||||
String get infoMentionsEntityNamesLimit;
|
||||
|
||||
/// No description provided for @infoReactionsEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'reactions-enabled'**
|
||||
String get infoReactionsEnabled;
|
||||
|
||||
/// No description provided for @infoTile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'tile'**
|
||||
String get infoTile;
|
||||
|
||||
/// No description provided for @infoGeocoder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'geocoder'**
|
||||
String get infoGeocoder;
|
||||
|
||||
/// No description provided for @infoStatic.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'static'**
|
||||
String get infoStatic;
|
||||
|
||||
/// No description provided for @chatInfoSubscribers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'subscribers:'**
|
||||
String get chatInfoSubscribers;
|
||||
|
||||
/// No description provided for @chatInfoInvitedBy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'invited by:'**
|
||||
String get chatInfoInvitedBy;
|
||||
|
||||
/// No description provided for @chatInfoLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'link:'**
|
||||
String get chatInfoLink;
|
||||
|
||||
/// No description provided for @chatInfoOfficial.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'official:'**
|
||||
String get chatInfoOfficial;
|
||||
|
||||
/// No description provided for @chatInfoComments.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'comments:'**
|
||||
String get chatInfoComments;
|
||||
|
||||
/// No description provided for @chatInfoAplus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'approved by Roskomnadzor:'**
|
||||
String get chatInfoAplus;
|
||||
|
||||
/// No description provided for @chatInfoSignAdmin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'admin signature:'**
|
||||
String get chatInfoSignAdmin;
|
||||
|
||||
/// No description provided for @chatInfoLastChanged.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'last changed:'**
|
||||
String get chatInfoLastChanged;
|
||||
|
||||
/// No description provided for @chatInfoJoinTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'joined:'**
|
||||
String get chatInfoJoinTime;
|
||||
|
||||
/// No description provided for @chatInfoCreated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'created:'**
|
||||
String get chatInfoCreated;
|
||||
|
||||
/// No description provided for @chatInfoTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Info'**
|
||||
String get chatInfoTitle;
|
||||
|
||||
/// No description provided for @chatInfoMembers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'members:'**
|
||||
String get chatInfoMembers;
|
||||
|
||||
/// No description provided for @chatInfoLastSeen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'last seen recently'**
|
||||
String get chatInfoLastSeen;
|
||||
|
||||
/// No description provided for @chatInfoHasBots.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'has bots:'**
|
||||
String get chatInfoHasBots;
|
||||
|
||||
/// No description provided for @chatInfoBlockedCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'blocked in group:'**
|
||||
String get chatInfoBlockedCount;
|
||||
|
||||
/// No description provided for @chatInfoOfficialStatus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'official status:'**
|
||||
String get chatInfoOfficialStatus;
|
||||
|
||||
/// No description provided for @chatInfoJoined.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'joined:'**
|
||||
String get chatInfoJoined;
|
||||
|
||||
/// No description provided for @chatInfoGroupCreated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'group created:'**
|
||||
String get chatInfoGroupCreated;
|
||||
|
||||
/// No description provided for @chatInfoGroupOwner.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'group owner:'**
|
||||
String get chatInfoGroupOwner;
|
||||
|
||||
/// No description provided for @chatInfoDialogStarted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'dialog started:'**
|
||||
String get chatInfoDialogStarted;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -289,4 +289,166 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get profileMenuSpoof => 'Spoofing';
|
||||
|
||||
@override
|
||||
String get infoTitle => 'Info';
|
||||
|
||||
@override
|
||||
String get infoAccountSection => 'Account';
|
||||
|
||||
@override
|
||||
String get infoServerSection => 'Server';
|
||||
|
||||
@override
|
||||
String get infoUserSection => 'User';
|
||||
|
||||
@override
|
||||
String get infoYMapSection => 'Y-Map';
|
||||
|
||||
@override
|
||||
String get infoFileUploadTypes => 'file-upload-unsupported-types';
|
||||
|
||||
@override
|
||||
String get infoWhiteListLinks => 'white-list-links';
|
||||
|
||||
@override
|
||||
String get infoRegistrationTime => 'registrationTime';
|
||||
|
||||
@override
|
||||
String get infoCountry => 'country';
|
||||
|
||||
@override
|
||||
String get infoVideoChatHistory => 'videoChatHistory';
|
||||
|
||||
@override
|
||||
String get infoUpdateTime => 'updateTime';
|
||||
|
||||
@override
|
||||
String get infoId => 'id';
|
||||
|
||||
@override
|
||||
String get infoChatMarker => 'chatMarker';
|
||||
|
||||
@override
|
||||
String get infoAccountRemovalEnabled => 'account-removal-enabled';
|
||||
|
||||
@override
|
||||
String get infoImageSize => 'image-size';
|
||||
|
||||
@override
|
||||
String get infoGce => 'gce';
|
||||
|
||||
@override
|
||||
String get infoGcce => 'gcce';
|
||||
|
||||
@override
|
||||
String get infoMaxMsgLength => 'max-msg-length';
|
||||
|
||||
@override
|
||||
String get infoQuotesEnabled => 'quotes-enabled';
|
||||
|
||||
@override
|
||||
String get infoCallsEndpoint => 'calls-endpoint';
|
||||
|
||||
@override
|
||||
String get infoSendLocationEnabled => 'send-location-enabled';
|
||||
|
||||
@override
|
||||
String get infoLgce => 'lgce';
|
||||
|
||||
@override
|
||||
String get infoWud => 'wud';
|
||||
|
||||
@override
|
||||
String get infoVideoMsgEnabled => 'video-msg-enabled';
|
||||
|
||||
@override
|
||||
String get infoGrse => 'grse';
|
||||
|
||||
@override
|
||||
String get infoEditTimeout => 'edit-timeout';
|
||||
|
||||
@override
|
||||
String get infoImageQuality => 'image-quality';
|
||||
|
||||
@override
|
||||
String get infoUnsafeFilesAlert => 'unsafe-files-alert';
|
||||
|
||||
@override
|
||||
String get infoAccountNicknameEnabled => 'account-nickname-enabled';
|
||||
|
||||
@override
|
||||
String get infoMentionsEntityNamesLimit => 'mentions_entity_names_limit';
|
||||
|
||||
@override
|
||||
String get infoReactionsEnabled => 'reactions-enabled';
|
||||
|
||||
@override
|
||||
String get infoTile => 'tile';
|
||||
|
||||
@override
|
||||
String get infoGeocoder => 'geocoder';
|
||||
|
||||
@override
|
||||
String get infoStatic => 'static';
|
||||
|
||||
@override
|
||||
String get chatInfoSubscribers => 'subscribers:';
|
||||
|
||||
@override
|
||||
String get chatInfoInvitedBy => 'invited by:';
|
||||
|
||||
@override
|
||||
String get chatInfoLink => 'link:';
|
||||
|
||||
@override
|
||||
String get chatInfoOfficial => 'official:';
|
||||
|
||||
@override
|
||||
String get chatInfoComments => 'comments:';
|
||||
|
||||
@override
|
||||
String get chatInfoAplus => 'approved by Roskomnadzor:';
|
||||
|
||||
@override
|
||||
String get chatInfoSignAdmin => 'admin signature:';
|
||||
|
||||
@override
|
||||
String get chatInfoLastChanged => 'last changed:';
|
||||
|
||||
@override
|
||||
String get chatInfoJoinTime => 'joined:';
|
||||
|
||||
@override
|
||||
String get chatInfoCreated => 'created:';
|
||||
|
||||
@override
|
||||
String get chatInfoTitle => 'Info';
|
||||
|
||||
@override
|
||||
String get chatInfoMembers => 'members:';
|
||||
|
||||
@override
|
||||
String get chatInfoLastSeen => 'last seen recently';
|
||||
|
||||
@override
|
||||
String get chatInfoHasBots => 'has bots:';
|
||||
|
||||
@override
|
||||
String get chatInfoBlockedCount => 'blocked in group:';
|
||||
|
||||
@override
|
||||
String get chatInfoOfficialStatus => 'official status:';
|
||||
|
||||
@override
|
||||
String get chatInfoJoined => 'joined:';
|
||||
|
||||
@override
|
||||
String get chatInfoGroupCreated => 'group created:';
|
||||
|
||||
@override
|
||||
String get chatInfoGroupOwner => 'group owner:';
|
||||
|
||||
@override
|
||||
String get chatInfoDialogStarted => 'dialog started:';
|
||||
}
|
||||
|
||||
@@ -291,4 +291,166 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get profileMenuSpoof => 'Подмена данных';
|
||||
|
||||
@override
|
||||
String get infoTitle => 'Info';
|
||||
|
||||
@override
|
||||
String get infoAccountSection => 'Аккаунт';
|
||||
|
||||
@override
|
||||
String get infoServerSection => 'Сервер';
|
||||
|
||||
@override
|
||||
String get infoUserSection => 'Пользователь';
|
||||
|
||||
@override
|
||||
String get infoYMapSection => 'Y-Map';
|
||||
|
||||
@override
|
||||
String get infoFileUploadTypes => 'запрещённые типы файлов';
|
||||
|
||||
@override
|
||||
String get infoWhiteListLinks => 'безопасные ссылки';
|
||||
|
||||
@override
|
||||
String get infoRegistrationTime => 'Дата регистрации:';
|
||||
|
||||
@override
|
||||
String get infoCountry => 'Регион аккаунта:';
|
||||
|
||||
@override
|
||||
String get infoVideoChatHistory => 'videoChatHistory';
|
||||
|
||||
@override
|
||||
String get infoUpdateTime => 'Последнее обновление аватарки:';
|
||||
|
||||
@override
|
||||
String get infoId => 'id аккаунта:';
|
||||
|
||||
@override
|
||||
String get infoChatMarker => 'chatMarker';
|
||||
|
||||
@override
|
||||
String get infoAccountRemovalEnabled => 'Мгновенное удаление аккаунта:';
|
||||
|
||||
@override
|
||||
String get infoImageSize => 'image-size';
|
||||
|
||||
@override
|
||||
String get infoGce => 'gce';
|
||||
|
||||
@override
|
||||
String get infoGcce => 'gcce';
|
||||
|
||||
@override
|
||||
String get infoMaxMsgLength => 'макс. длина сообщения:';
|
||||
|
||||
@override
|
||||
String get infoQuotesEnabled => 'quotes-enabled';
|
||||
|
||||
@override
|
||||
String get infoCallsEndpoint => 'calls-endpoint';
|
||||
|
||||
@override
|
||||
String get infoSendLocationEnabled => 'отправка гео.:';
|
||||
|
||||
@override
|
||||
String get infoLgce => 'lgce';
|
||||
|
||||
@override
|
||||
String get infoWud => 'wud';
|
||||
|
||||
@override
|
||||
String get infoVideoMsgEnabled => 'Кружки:';
|
||||
|
||||
@override
|
||||
String get infoGrse => 'grse';
|
||||
|
||||
@override
|
||||
String get infoEditTimeout => 'Можно редактировать сообщение в течении:';
|
||||
|
||||
@override
|
||||
String get infoImageQuality => 'image-quality';
|
||||
|
||||
@override
|
||||
String get infoUnsafeFilesAlert => 'unsafe-files-alert';
|
||||
|
||||
@override
|
||||
String get infoAccountNicknameEnabled => 'account-nickname-enabled';
|
||||
|
||||
@override
|
||||
String get infoMentionsEntityNamesLimit => 'макс. кол-во упоминаний:';
|
||||
|
||||
@override
|
||||
String get infoReactionsEnabled => 'reactions-enabled';
|
||||
|
||||
@override
|
||||
String get infoTile => 'tile';
|
||||
|
||||
@override
|
||||
String get infoGeocoder => 'geocoder';
|
||||
|
||||
@override
|
||||
String get infoStatic => 'static';
|
||||
|
||||
@override
|
||||
String get chatInfoSubscribers => 'подписчиков:';
|
||||
|
||||
@override
|
||||
String get chatInfoInvitedBy => 'Приглашён от:';
|
||||
|
||||
@override
|
||||
String get chatInfoLink => 'ссылка:';
|
||||
|
||||
@override
|
||||
String get chatInfoOfficial => 'оффициальный:';
|
||||
|
||||
@override
|
||||
String get chatInfoComments => 'комментарии:';
|
||||
|
||||
@override
|
||||
String get chatInfoAplus => 'подтверждён Роскомнадзором:';
|
||||
|
||||
@override
|
||||
String get chatInfoSignAdmin => 'Подпись админов:';
|
||||
|
||||
@override
|
||||
String get chatInfoLastChanged => 'последнее изменение:';
|
||||
|
||||
@override
|
||||
String get chatInfoJoinTime => 'заход в канал:';
|
||||
|
||||
@override
|
||||
String get chatInfoCreated => 'канал создан:';
|
||||
|
||||
@override
|
||||
String get chatInfoTitle => 'Информация';
|
||||
|
||||
@override
|
||||
String get chatInfoMembers => 'участников:';
|
||||
|
||||
@override
|
||||
String get chatInfoLastSeen => 'был(а) недавно';
|
||||
|
||||
@override
|
||||
String get chatInfoHasBots => 'Есть боты:';
|
||||
|
||||
@override
|
||||
String get chatInfoBlockedCount => 'в ЧС группы:';
|
||||
|
||||
@override
|
||||
String get chatInfoOfficialStatus => 'Официальный статус:';
|
||||
|
||||
@override
|
||||
String get chatInfoJoined => 'Зашли в:';
|
||||
|
||||
@override
|
||||
String get chatInfoGroupCreated => 'Группа создана в:';
|
||||
|
||||
@override
|
||||
String get chatInfoGroupOwner => 'Создатель группы:';
|
||||
|
||||
@override
|
||||
String get chatInfoDialogStarted => 'ЛС начат в:';
|
||||
}
|
||||
|
||||
+56
-1
@@ -104,5 +104,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"profileMenuSpoof": "Подмена данных"
|
||||
"profileMenuSpoof": "Подмена данных",
|
||||
"infoTitle": "Info",
|
||||
"infoAccountSection": "Аккаунт",
|
||||
"infoServerSection": "Сервер",
|
||||
"infoUserSection": "Пользователь",
|
||||
"infoYMapSection": "Y-Map",
|
||||
"infoFileUploadTypes": "запрещённые типы файлов",
|
||||
"infoWhiteListLinks": "безопасные ссылки",
|
||||
"infoRegistrationTime": "Дата регистрации:",
|
||||
"infoCountry": "Регион аккаунта:",
|
||||
"infoVideoChatHistory": "videoChatHistory",
|
||||
"infoUpdateTime": "Последнее обновление аватарки:",
|
||||
"infoId": "id аккаунта:",
|
||||
"infoChatMarker": "chatMarker",
|
||||
"infoAccountRemovalEnabled": "Мгновенное удаление аккаунта:",
|
||||
"infoImageSize": "image-size",
|
||||
"infoGce": "gce",
|
||||
"infoGcce": "gcce",
|
||||
"infoMaxMsgLength": "макс. длина сообщения:",
|
||||
"infoQuotesEnabled": "quotes-enabled",
|
||||
"infoCallsEndpoint": "calls-endpoint",
|
||||
"infoSendLocationEnabled": "отправка гео.:",
|
||||
"infoLgce": "lgce",
|
||||
"infoWud": "wud",
|
||||
"infoVideoMsgEnabled": "Кружки:",
|
||||
"infoGrse": "grse",
|
||||
"infoEditTimeout": "Можно редактировать сообщение в течении:",
|
||||
"infoImageQuality": "image-quality",
|
||||
"infoUnsafeFilesAlert": "unsafe-files-alert",
|
||||
"infoAccountNicknameEnabled": "account-nickname-enabled",
|
||||
"infoMentionsEntityNamesLimit": "макс. кол-во упоминаний:",
|
||||
"infoReactionsEnabled": "reactions-enabled",
|
||||
"infoTile": "tile",
|
||||
"infoGeocoder": "geocoder",
|
||||
"infoStatic": "static",
|
||||
"chatInfoSubscribers": "подписчиков:",
|
||||
"chatInfoInvitedBy": "Приглашён от:",
|
||||
"chatInfoLink": "ссылка:",
|
||||
"chatInfoOfficial": "оффициальный:",
|
||||
"chatInfoComments": "комментарии:",
|
||||
"chatInfoAplus": "подтверждён Роскомнадзором:",
|
||||
"chatInfoSignAdmin": "Подпись админов:",
|
||||
"chatInfoLastChanged": "последнее изменение:",
|
||||
"chatInfoJoinTime": "заход в канал:",
|
||||
"chatInfoCreated": "канал создан:",
|
||||
"chatInfoTitle": "Информация",
|
||||
"chatInfoMembers": "участников:",
|
||||
"chatInfoLastSeen": "был(а) недавно",
|
||||
"chatInfoHasBots": "Есть боты:",
|
||||
"chatInfoBlockedCount": "в ЧС группы:",
|
||||
"chatInfoOfficialStatus": "Официальный статус:",
|
||||
"chatInfoLastChanged": "последнее изменение:",
|
||||
"chatInfoJoined": "Зашли в:",
|
||||
"chatInfoGroupCreated": "Группа создана в:",
|
||||
"chatInfoGroupOwner": "Создатель группы:",
|
||||
"chatInfoDialogStarted": "ЛС начат в:"
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -71,6 +73,7 @@ class KometAppState extends State<KometApp> {
|
||||
|
||||
late Locale _locale;
|
||||
bool _isLoggingOut = false;
|
||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
widget.initialFpsOverlay,
|
||||
);
|
||||
@@ -83,15 +86,16 @@ class KometAppState extends State<KometApp> {
|
||||
api.setReconnectCallback(() async {
|
||||
try {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null &&
|
||||
await TokenStorage.readToken(accountId) != null) {
|
||||
if (accountId != null) {
|
||||
final token = await TokenStorage.readToken(accountId);
|
||||
await accountModule.login(accountId: accountId, token: token);
|
||||
if (token != null) {
|
||||
await accountModule.login(accountId: accountId, token: token);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
api.sessionExpiredStream.listen((SessionExpiredException e) async {
|
||||
_sessionExpiredSub = api.sessionExpiredStream.listen((SessionExpiredException e) async {
|
||||
if (_isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
|
||||
@@ -118,6 +122,7 @@ class KometAppState extends State<KometApp> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionExpiredSub?.cancel();
|
||||
fpsOverlayEnabled.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
+22
-18
@@ -136,7 +136,8 @@ class VideoAttachment extends MessageAttachment {
|
||||
} else if (previewRaw is List) {
|
||||
try {
|
||||
final bytes = List<int>.from(previewRaw);
|
||||
previewStr = String.fromCharCodes(bytes);
|
||||
final base64 = String.fromCharCodes(bytes);
|
||||
previewStr = 'data:image/webp;base64,$base64';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -192,7 +193,8 @@ class AudioAttachment extends MessageAttachment {
|
||||
} else if (previewRaw is List) {
|
||||
try {
|
||||
final bytes = List<int>.from(previewRaw);
|
||||
previewStr = String.fromCharCodes(bytes);
|
||||
final base64 = String.fromCharCodes(bytes);
|
||||
previewStr = 'data:image/webp;base64,$base64';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -244,7 +246,8 @@ class FileAttachment extends MessageAttachment {
|
||||
} else if (previewRaw is List) {
|
||||
try {
|
||||
final bytes = List<int>.from(previewRaw);
|
||||
previewStr = String.fromCharCodes(bytes);
|
||||
final base64 = String.fromCharCodes(bytes);
|
||||
previewStr = 'data:image/webp;base64,$base64';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -294,7 +297,8 @@ class StickerAttachment extends MessageAttachment {
|
||||
} else if (previewRaw is List) {
|
||||
try {
|
||||
final bytes = List<int>.from(previewRaw);
|
||||
previewStr = String.fromCharCodes(bytes);
|
||||
final base64 = String.fromCharCodes(bytes);
|
||||
previewStr = 'data:image/webp;base64,$base64';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -344,15 +348,15 @@ class ContactAttachment extends MessageAttachment {
|
||||
|
||||
factory ContactAttachment.fromMap(Map<String, dynamic> map) {
|
||||
return ContactAttachment(
|
||||
previewData: map['previewData'] as String?,
|
||||
baseUrl: map['baseUrl'] as String?,
|
||||
userId: map['userId'] as String?,
|
||||
firstName: map['firstName'] as String?,
|
||||
lastName: map['lastName'] as String?,
|
||||
phoneNumber: map['phoneNumber'] as String?,
|
||||
photoUrl: map['photoUrl'] as String?,
|
||||
contactId: map['contactId'] as int?,
|
||||
name: map['name'] as String?,
|
||||
previewData: map['previewData']?.toString(),
|
||||
baseUrl: map['baseUrl']?.toString(),
|
||||
userId: map['userId']?.toString(),
|
||||
firstName: map['firstName']?.toString(),
|
||||
lastName: map['lastName']?.toString(),
|
||||
phoneNumber: map['phoneNumber']?.toString(),
|
||||
photoUrl: map['photoUrl']?.toString(),
|
||||
contactId: map['contactId'] is int ? map['contactId'] as int : int.tryParse(map['contactId']?.toString() ?? ''),
|
||||
name: map['name']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -426,11 +430,11 @@ class ControlAttachment extends MessageAttachment {
|
||||
|
||||
factory ControlAttachment.fromMap(Map<String, dynamic> map) {
|
||||
return ControlAttachment(
|
||||
previewData: map['previewData'] as String?,
|
||||
baseUrl: map['baseUrl'] as String?,
|
||||
event: map['event'] as String?,
|
||||
title: map['title'] as String?,
|
||||
userIds: (map['userIds'] as List?)?.cast<int>(),
|
||||
previewData: map['previewData']?.toString(),
|
||||
baseUrl: map['baseUrl']?.toString(),
|
||||
event: map['event']?.toString(),
|
||||
title: map['title']?.toString(),
|
||||
userIds: (map['userIds'] as List?)?.map((e) => e is int ? e : int.tryParse(e?.toString() ?? '') ?? 0).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user