feat: нормальная работа с группами
This commit is contained in:
@@ -13,6 +13,7 @@ import '../../core/storage/app_database.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../../core/utils/text_format.dart';
|
||||
import '../../models/contact_info.dart';
|
||||
import '../api.dart';
|
||||
import 'chat_parsing.dart';
|
||||
import 'chat_preview.dart';
|
||||
@@ -303,6 +304,33 @@ class ChatSearchHit {
|
||||
});
|
||||
}
|
||||
|
||||
class ChatMemberEntry {
|
||||
final int id;
|
||||
final String? name;
|
||||
final String? avatarUrl;
|
||||
final int? seenTime;
|
||||
final int presenceStatus;
|
||||
final bool blocked;
|
||||
|
||||
const ChatMemberEntry({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.avatarUrl,
|
||||
this.seenTime,
|
||||
required this.presenceStatus,
|
||||
this.blocked = false,
|
||||
});
|
||||
|
||||
bool get isOnline => presenceStatus == 1;
|
||||
}
|
||||
|
||||
class ChatMembersPage {
|
||||
final List<ChatMemberEntry> members;
|
||||
final int marker;
|
||||
|
||||
const ChatMembersPage({required this.members, required this.marker});
|
||||
}
|
||||
|
||||
class MessageSearchHit {
|
||||
final int chatId;
|
||||
final String? messageId;
|
||||
@@ -1686,6 +1714,119 @@ class ChatsModule {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addMembers(
|
||||
Api api, {
|
||||
required int chatId,
|
||||
required List<int> userIds,
|
||||
bool showHistory = true,
|
||||
}) async {
|
||||
if (userIds.isEmpty) return false;
|
||||
try {
|
||||
final packet = await api.sendRequest(Opcode.chatMembersUpdate, {
|
||||
'chatId': chatId,
|
||||
'userIds': userIds,
|
||||
'showHistory': showHistory,
|
||||
'operation': 'add',
|
||||
});
|
||||
if (!packet.isOk) {
|
||||
logger.w('addMembers $chatId: ${messageFromErrorPayload(packet.payload)}');
|
||||
return false;
|
||||
}
|
||||
final data = packet.payload;
|
||||
final chat = data is Map ? data['chat'] : null;
|
||||
if (chat is Map) {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null) {
|
||||
await cacheServerChat(chat.cast<dynamic, dynamic>(), accountId);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} on PacketError catch (e) {
|
||||
logger.w('addMembers $chatId: ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
logger.w('addMembers $chatId: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChatMembersPage?> getChatMembers(
|
||||
Api api,
|
||||
int chatId, {
|
||||
int marker = 0,
|
||||
int count = 50,
|
||||
}) async {
|
||||
try {
|
||||
final packet = await api.sendRequest(Opcode.chatMembers, {
|
||||
'type': 'MEMBER',
|
||||
'marker': marker,
|
||||
'chatId': chatId,
|
||||
'count': count,
|
||||
});
|
||||
if (!packet.isOk) return null;
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return null;
|
||||
|
||||
final entries = <ChatMemberEntry>[];
|
||||
final presenceById = <int, Map<String, dynamic>>{};
|
||||
final rawMembers = payload['members'];
|
||||
if (rawMembers is List) {
|
||||
for (final m in rawMembers.whereType<Map>()) {
|
||||
final contact = m['contact'];
|
||||
if (contact is! Map) continue;
|
||||
final id = contact['id'];
|
||||
if (id is! int) continue;
|
||||
|
||||
final info = ContactInfo.fromMap(Map<String, dynamic>.from(contact));
|
||||
final name = info.displayName;
|
||||
if (name != null && name.isNotEmpty) ContactCache.put(id, name);
|
||||
final avatar = info.avatarUrl;
|
||||
if (avatar != null && avatar.isNotEmpty) {
|
||||
ContactCache.putAvatar(id, avatar);
|
||||
}
|
||||
final phone = contact['phone'];
|
||||
if (phone is int && phone > 0) ContactCache.putPhone(id, phone);
|
||||
ContactInfoFetch.putContact(id, contact.cast<dynamic, dynamic>());
|
||||
|
||||
final presence = m['presence'];
|
||||
var status = 0;
|
||||
int? seen;
|
||||
if (presence is Map) {
|
||||
final p = Map<String, dynamic>.from(presence);
|
||||
presenceById[id] = p;
|
||||
status = (p['status'] as int?) ?? 0;
|
||||
final s = p['seen'];
|
||||
seen = s is int ? s : null;
|
||||
}
|
||||
|
||||
entries.add(
|
||||
ChatMemberEntry(
|
||||
id: id,
|
||||
name: name,
|
||||
avatarUrl: avatar,
|
||||
seenTime: seen,
|
||||
presenceStatus: status,
|
||||
blocked: info.isDeleted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (presenceById.isNotEmpty) PresenceFetch.primeAll(presenceById);
|
||||
|
||||
final next = payload['marker'];
|
||||
return ChatMembersPage(
|
||||
members: entries,
|
||||
marker: next is int ? next : marker,
|
||||
);
|
||||
} on PacketError catch (e) {
|
||||
logger.w('getChatMembers $chatId: ${e.message}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
logger.w('getChatMembers $chatId: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<CachedChat>> refreshChats(Api api, List<int> chatIds) async {
|
||||
if (chatIds.isEmpty) return const [];
|
||||
try {
|
||||
|
||||
Vendored
+40
@@ -113,6 +113,46 @@ class ContactInfoFetch {
|
||||
_cache.putValue(id, ContactInfo.fromMap(Map<String, dynamic>.from(contact)));
|
||||
}
|
||||
|
||||
static Future<Map<int, ContactInfo>> getMany(
|
||||
List<int> ids, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
final result = <int, ContactInfo>{};
|
||||
final missing = <int>[];
|
||||
for (final id in ids) {
|
||||
if (!forceRefresh) {
|
||||
final cached = _cache.peek(id);
|
||||
if (cached != null) {
|
||||
result[id] = cached;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
missing.add(id);
|
||||
}
|
||||
if (missing.isEmpty) return result;
|
||||
|
||||
final api = _api;
|
||||
if (api == null || api.state != SessionState.online) return result;
|
||||
try {
|
||||
final resp = await api.sendRequest(Opcode.contactInfo, {
|
||||
'contactIds': missing,
|
||||
});
|
||||
final data = resp.payload;
|
||||
final contacts = data is Map ? data['contacts'] : null;
|
||||
if (contacts is List) {
|
||||
final now = DateTime.now();
|
||||
for (final c in contacts.whereType<Map>()) {
|
||||
final id = c['id'];
|
||||
if (id is! int) continue;
|
||||
final info = ContactInfo.fromMap(Map<String, dynamic>.from(c));
|
||||
_cache.putValue(id, info, at: now);
|
||||
result[id] = info;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<ContactInfo?> _fetch(int id) async {
|
||||
final api = _api;
|
||||
if (api == null || api.state != SessionState.online) return null;
|
||||
|
||||
@@ -883,6 +883,17 @@ class AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<int>> loadContactIds(int accountId) async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query(
|
||||
'contacts',
|
||||
columns: ['id'],
|
||||
where: 'account_id = ?',
|
||||
whereArgs: [accountId],
|
||||
);
|
||||
return [for (final r in rows) r['id'] as int];
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> loadContact(
|
||||
int accountId,
|
||||
int id,
|
||||
|
||||
@@ -20,24 +20,39 @@ import '../../widgets/connection_status.dart';
|
||||
import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/komet_avatar.dart';
|
||||
import '../../widgets/swipe_route.dart';
|
||||
import '../../../backend/modules/chats.dart';
|
||||
import '../contacts/open_contact_profile.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'group_invite_sheets.dart';
|
||||
|
||||
class _MemberInfo {
|
||||
final int id;
|
||||
final String? name;
|
||||
final String? avatarUrl;
|
||||
final bool isAdmin;
|
||||
final bool isOwner;
|
||||
final bool isMe;
|
||||
final String? alias;
|
||||
final int? seenTime;
|
||||
final bool isOnline;
|
||||
final int presenceStatus;
|
||||
final bool blocked;
|
||||
final bool isContact;
|
||||
|
||||
const _MemberInfo({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.avatarUrl,
|
||||
required this.isAdmin,
|
||||
required this.isOwner,
|
||||
required this.isMe,
|
||||
this.alias,
|
||||
this.seenTime,
|
||||
required this.isOnline,
|
||||
required this.presenceStatus,
|
||||
this.blocked = false,
|
||||
this.isContact = false,
|
||||
});
|
||||
|
||||
bool get isOnline => presenceStatus == 1;
|
||||
}
|
||||
|
||||
enum ChatInfoTab { media }
|
||||
@@ -88,8 +103,16 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
int _presenceStatus = 0;
|
||||
bool _isBot = false;
|
||||
|
||||
List<_MemberInfo> _members = [];
|
||||
int _onlineCount = 0;
|
||||
final List<_MemberInfo> _members = [];
|
||||
final List<_MemberInfo> _owners = [];
|
||||
final List<_MemberInfo> _admins = [];
|
||||
final List<_MemberInfo> _contactMembers = [];
|
||||
final List<_MemberInfo> _otherMembers = [];
|
||||
final Set<int> _seenMemberIds = {};
|
||||
Set<int> _contactIds = {};
|
||||
int _memberMarker = 0;
|
||||
bool _membersLoading = false;
|
||||
bool _membersEnd = false;
|
||||
|
||||
int _mediaChatId = 0;
|
||||
String? _anchorMsgId;
|
||||
@@ -97,6 +120,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bodyScrollController.addListener(_onBodyScroll);
|
||||
_load();
|
||||
}
|
||||
|
||||
@@ -207,34 +231,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
setState(() => _isLoading = false);
|
||||
return;
|
||||
} else if (widget.chatType == 'CHAT') {
|
||||
final chatInfo = _chatInfo!;
|
||||
final memberIds = chatInfo.participantIds;
|
||||
|
||||
Map<int, Map<String, dynamic>> presenceMap = {};
|
||||
if (memberIds.isNotEmpty) {
|
||||
presenceMap = await PresenceFetch.getMany(memberIds);
|
||||
}
|
||||
|
||||
_onlineCount = 0;
|
||||
_members = memberIds.map((id) {
|
||||
final pres = presenceMap[id];
|
||||
final online = (pres?['status'] as int?) == 1;
|
||||
if (online) _onlineCount++;
|
||||
return _MemberInfo(
|
||||
id: id,
|
||||
isAdmin: chatInfo.isAdmin(id),
|
||||
isOwner: chatInfo.isOwner(id),
|
||||
isMe: id == _myId,
|
||||
seenTime: pres?['seen'] as int?,
|
||||
isOnline: online,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_members.sort((a, b) {
|
||||
if (a.isMe != b.isMe) return a.isMe ? -1 : 1;
|
||||
if (a.isOnline != b.isOnline) return a.isOnline ? -1 : 1;
|
||||
return (b.seenTime ?? 0).compareTo(a.seenTime ?? 0);
|
||||
});
|
||||
_contactIds = (await AppDatabase.loadContactIds(_myId)).toSet();
|
||||
await _loadLeaders();
|
||||
await _fetchMembersPage(initial: true);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
@@ -253,6 +252,177 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
return _tabs.contains(media) ? media : null;
|
||||
}
|
||||
|
||||
_MemberInfo _memberFrom(ChatMemberEntry e) => _MemberInfo(
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
avatarUrl: e.avatarUrl,
|
||||
isAdmin: _chatInfo?.isAdmin(e.id) ?? false,
|
||||
isOwner: _chatInfo?.isOwner(e.id) ?? false,
|
||||
isMe: e.id == _myId,
|
||||
alias: _chatInfo?.adminAlias(e.id),
|
||||
seenTime: e.seenTime,
|
||||
presenceStatus: e.presenceStatus,
|
||||
blocked: e.blocked,
|
||||
isContact: _contactIds.contains(e.id),
|
||||
);
|
||||
|
||||
Future<void> _loadLeaders() async {
|
||||
final info = _chatInfo;
|
||||
if (info == null) return;
|
||||
|
||||
final owner = info.owner;
|
||||
final leaderIds = <int>[
|
||||
if (owner != null && owner != 0) owner,
|
||||
for (final a in info.adminIds)
|
||||
if (a != owner) a,
|
||||
];
|
||||
if (leaderIds.isEmpty) return;
|
||||
|
||||
final contacts = await ContactInfoFetch.getMany(leaderIds);
|
||||
final presence = await PresenceFetch.getMany(leaderIds);
|
||||
if (!mounted) return;
|
||||
|
||||
for (final id in leaderIds) {
|
||||
if (!_seenMemberIds.add(id)) continue;
|
||||
final c = contacts[id];
|
||||
final pres = presence[id];
|
||||
_addMember(
|
||||
_MemberInfo(
|
||||
id: id,
|
||||
name: c?.displayName ?? ContactCache.get(id),
|
||||
avatarUrl: c?.avatarUrl ?? ContactCache.getAvatar(id),
|
||||
isAdmin: info.isAdmin(id),
|
||||
isOwner: info.isOwner(id),
|
||||
isMe: id == _myId,
|
||||
alias: info.adminAlias(id),
|
||||
seenTime: pres?['seen'] as int?,
|
||||
presenceStatus: (pres?['status'] as int?) ?? 0,
|
||||
blocked: c?.isDeleted ?? false,
|
||||
isContact: _contactIds.contains(id),
|
||||
),
|
||||
);
|
||||
}
|
||||
_rebuildMembers();
|
||||
}
|
||||
|
||||
int _memberRank(_MemberInfo m) {
|
||||
if (m.isOwner) return 0;
|
||||
if (m.isAdmin) return 1;
|
||||
if (m.isContact) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
void _addMember(_MemberInfo m) {
|
||||
switch (_memberRank(m)) {
|
||||
case 0:
|
||||
_owners.add(m);
|
||||
case 1:
|
||||
_admins.add(m);
|
||||
case 2:
|
||||
_contactMembers.add(m);
|
||||
default:
|
||||
_otherMembers.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
void _rebuildMembers() {
|
||||
_members
|
||||
..clear()
|
||||
..addAll(_owners)
|
||||
..addAll(_admins)
|
||||
..addAll(_contactMembers)
|
||||
..addAll(_otherMembers);
|
||||
}
|
||||
|
||||
Future<void> _fetchMembersPage({bool initial = false}) async {
|
||||
if (_membersLoading || _membersEnd) return;
|
||||
_membersLoading = true;
|
||||
if (!initial && mounted) setState(() {});
|
||||
|
||||
final page = await chats.getChatMembers(
|
||||
api,
|
||||
widget.chatId,
|
||||
marker: _memberMarker,
|
||||
);
|
||||
_membersLoading = false;
|
||||
if (!mounted) return;
|
||||
|
||||
if (page == null) {
|
||||
if (!initial) setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
var added = 0;
|
||||
for (final e in page.members) {
|
||||
if (_seenMemberIds.add(e.id)) {
|
||||
_addMember(_memberFrom(e));
|
||||
added++;
|
||||
}
|
||||
}
|
||||
if (added > 0) _rebuildMembers();
|
||||
|
||||
final total = _chatInfo?.participantsCount;
|
||||
if (page.members.isEmpty ||
|
||||
added == 0 ||
|
||||
page.marker == _memberMarker ||
|
||||
(total != null && _members.length >= total)) {
|
||||
_membersEnd = true;
|
||||
}
|
||||
_memberMarker = page.marker;
|
||||
|
||||
if (!initial) setState(() {});
|
||||
}
|
||||
|
||||
void _onBodyScroll() {
|
||||
if (!mounted || widget.chatType != 'CHAT') return;
|
||||
if (_membersLoading || _membersEnd) return;
|
||||
if (_selectedTab != AppLocalizations.of(context)!.chatInfoTabMembers) return;
|
||||
final pos = _bodyScrollController.position;
|
||||
if (pos.pixels >= pos.maxScrollExtent - 400) {
|
||||
_fetchMembersPage();
|
||||
}
|
||||
}
|
||||
|
||||
String? get _inviteLink {
|
||||
final link = _chatInfo?.link;
|
||||
return (link != null && link.isNotEmpty) ? link : null;
|
||||
}
|
||||
|
||||
Future<void> _openAddMembers() async {
|
||||
final exclude = {_myId, ..._members.map((m) => m.id)};
|
||||
final added = await showAddMembersSheet(
|
||||
context,
|
||||
chatId: widget.chatId,
|
||||
excludeIds: exclude,
|
||||
);
|
||||
if (added == true && mounted) await _refreshMembers();
|
||||
}
|
||||
|
||||
void _openInviteLink(String link) {
|
||||
showInviteLinkSheet(
|
||||
context,
|
||||
link: link,
|
||||
title: widget.name,
|
||||
avatarUrl: widget.imageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshMembers() async {
|
||||
_contactMembers.clear();
|
||||
_otherMembers.clear();
|
||||
_seenMemberIds
|
||||
..clear()
|
||||
..addAll(_owners.map((m) => m.id))
|
||||
..addAll(_admins.map((m) => m.id));
|
||||
_memberMarker = 0;
|
||||
_membersEnd = false;
|
||||
_membersLoading = false;
|
||||
_rebuildMembers();
|
||||
if (mounted) setState(() {});
|
||||
await _fetchMembersPage(initial: true);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -326,6 +496,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
|
||||
bool get _isContact => _localContact != null;
|
||||
|
||||
bool get _peerDeleted => _contactData?.isDeleted ?? false;
|
||||
|
||||
String _joinName(String first, String last) => last.trim().isEmpty
|
||||
? first.trim()
|
||||
: '${first.trim()} ${last.trim()}';
|
||||
@@ -420,8 +592,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
);
|
||||
final custom = _customName;
|
||||
final real = _realName;
|
||||
final hasToggle =
|
||||
widget.chatType == 'DIALOG' && real != null && real != custom;
|
||||
final hasToggle = _isContact && real != null && real != custom;
|
||||
|
||||
final nameSwap = AnimatedTextSwap(
|
||||
showAlternate: _showRealName,
|
||||
@@ -463,6 +634,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
switch (widget.chatType) {
|
||||
case 'DIALOG':
|
||||
if (_peerDeleted) return l10n.chatInfoMemberDeleted;
|
||||
if (_isBot) return l10n.contactProfileBot;
|
||||
if (_isOnline) return l10n.contactProfileOnline;
|
||||
if (_presenceStatus == 2 || _presenceStatus == 3) return l10n.contactProfileRecentlyActive;
|
||||
@@ -472,9 +644,6 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
return '';
|
||||
case 'CHAT':
|
||||
final total = _chatInfo?.participantsCount ?? _members.length;
|
||||
if (_onlineCount > 0) {
|
||||
return l10n.chatInfoOnlineOfTotal('$_onlineCount', '$total');
|
||||
}
|
||||
return '$total ${pluralRu(total, 'участник', 'участника', 'участников')}';
|
||||
case 'CHANNEL':
|
||||
final count = _chatInfo?.participantsCount ?? 0;
|
||||
@@ -1026,13 +1195,64 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_memberAction(cs, Icons.person_add, l10n.chatInfoAddMember, () {}),
|
||||
_memberAction(
|
||||
cs,
|
||||
Icons.person_add,
|
||||
l10n.chatInfoAddMember,
|
||||
_openAddMembers,
|
||||
),
|
||||
if (_inviteLink != null) ...[
|
||||
_listDivider(cs),
|
||||
_memberAction(
|
||||
cs,
|
||||
Icons.link,
|
||||
l10n.chatInfoInviteByLink,
|
||||
() => _openInviteLink(_inviteLink!),
|
||||
),
|
||||
],
|
||||
..._members.expand((m) => [_listDivider(cs), _memberTile(cs, m)]),
|
||||
if (_membersLoading || !_membersEnd) ...[
|
||||
_listDivider(cs),
|
||||
_membersFooter(cs),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _membersFooter(ColorScheme cs) {
|
||||
if (_membersLoading) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return InkWell(
|
||||
onTap: () => _fetchMembersPage(),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.expand_more, color: cs.primary, size: 26),
|
||||
const SizedBox(width: 14),
|
||||
Text(
|
||||
l10n.chatInfoShowMore,
|
||||
style: TextStyle(color: cs.primary, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _memberAction(
|
||||
ColorScheme cs,
|
||||
IconData icon,
|
||||
@@ -1065,16 +1285,21 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
Widget _memberTile(ColorScheme cs, _MemberInfo member) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final name =
|
||||
member.name ??
|
||||
ContactCache.get(member.id) ??
|
||||
(member.isMe ? l10n.callParticipantYou : '${member.id}');
|
||||
final avatar = ContactCache.getAvatar(member.id);
|
||||
final avatar = member.avatarUrl ?? ContactCache.getAvatar(member.id);
|
||||
|
||||
final String sublabel;
|
||||
if (member.isMe) {
|
||||
if (member.blocked) {
|
||||
sublabel = l10n.chatInfoMemberDeleted;
|
||||
} else if (member.isMe) {
|
||||
sublabel = l10n.callParticipantYou;
|
||||
} else if (member.isOnline) {
|
||||
} else if (member.presenceStatus == 1) {
|
||||
sublabel = l10n.contactProfileOnline;
|
||||
} else if (member.seenTime != null) {
|
||||
} else if (member.presenceStatus == 2 || member.presenceStatus == 3) {
|
||||
sublabel = l10n.contactProfileRecentlyActive;
|
||||
} else if (member.seenTime != null && member.seenTime! > 0) {
|
||||
sublabel = formatLastSeen(member.seenTime!);
|
||||
} else {
|
||||
sublabel = l10n.contactProfileRecentlyActive;
|
||||
@@ -1084,57 +1309,95 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
? l10n.chatInfoRoleOwner
|
||||
: (member.isAdmin ? l10n.chatInfoRoleAdmin : null);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
(avatar != null && avatar.isNotEmpty)
|
||||
? CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
avatar,
|
||||
maxWidth: 144,
|
||||
maxHeight: 144,
|
||||
),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
: CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
return InkWell(
|
||||
onTap: member.isMe
|
||||
? null
|
||||
: () => openContactDialogProfile(
|
||||
context,
|
||||
contactId: member.id,
|
||||
name: name,
|
||||
avatarUrl: avatar,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
if (member.blocked)
|
||||
_ghostAvatar()
|
||||
else if (avatar != null && avatar.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
avatar,
|
||||
maxWidth: 144,
|
||||
maxHeight: 144,
|
||||
),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
style: TextStyle(color: cs.onPrimaryContainer, fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: cs.onPrimaryContainer,
|
||||
fontSize: 16,
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
Text(
|
||||
sublabel,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sublabel,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (roleLabel != null)
|
||||
Text(
|
||||
roleLabel,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
if (member.alias != null)
|
||||
_memberTag(cs, member.alias!)
|
||||
else if (roleLabel != null)
|
||||
Text(
|
||||
roleLabel,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _ghostAvatar({double radius = 22, double fontSize = 24}) {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: const Color(0xFFD4D4D4),
|
||||
child: Text('👻', style: TextStyle(fontSize: fontSize)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _memberTag(ColorScheme cs, String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1374,6 +1637,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
}
|
||||
|
||||
Widget _avatar() {
|
||||
if (_peerDeleted) return _ghostAvatar(radius: 48, fontSize: 52);
|
||||
final avatar = KometAvatar(
|
||||
name: widget.name,
|
||||
imageUrl: widget.imageUrl,
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/backend/modules/contacts.dart';
|
||||
import 'package:komet/backend/modules/messages.dart' show ContactCache;
|
||||
import 'package:komet/core/storage/app_database.dart';
|
||||
import 'package:komet/core/storage/token_storage.dart';
|
||||
import 'package:komet/frontend/screens/contacts/contact_sheet_common.dart';
|
||||
import 'package:komet/frontend/widgets/custom_notification.dart';
|
||||
import 'package:komet/frontend/widgets/komet_avatar.dart';
|
||||
import 'package:komet/l10n/app_localizations.dart';
|
||||
|
||||
class _Candidate {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? avatarUrl;
|
||||
|
||||
const _Candidate({required this.id, required this.name, this.avatarUrl});
|
||||
}
|
||||
|
||||
Future<bool?> showAddMembersSheet(
|
||||
BuildContext context, {
|
||||
required int chatId,
|
||||
required Set<int> excludeIds,
|
||||
}) {
|
||||
return showBlurredCard<bool>(
|
||||
context,
|
||||
(host) => _AddMembersCard(
|
||||
chatId: chatId,
|
||||
excludeIds: excludeIds,
|
||||
hostContext: host,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _AddMembersCard extends StatefulWidget {
|
||||
final int chatId;
|
||||
final Set<int> excludeIds;
|
||||
final BuildContext hostContext;
|
||||
|
||||
const _AddMembersCard({
|
||||
required this.chatId,
|
||||
required this.excludeIds,
|
||||
required this.hostContext,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AddMembersCard> createState() => _AddMembersCardState();
|
||||
}
|
||||
|
||||
class _AddMembersCardState extends State<_AddMembersCard> {
|
||||
final TextEditingController _searchCtrl = TextEditingController();
|
||||
final Set<int> _selected = {};
|
||||
List<_Candidate> _all = [];
|
||||
String _query = '';
|
||||
bool _loading = true;
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
return;
|
||||
}
|
||||
final byId = <int, _Candidate>{};
|
||||
|
||||
final contacts = await ContactsModule.getContacts(accountId);
|
||||
for (final c in contacts) {
|
||||
if (c.id == accountId || widget.excludeIds.contains(c.id)) continue;
|
||||
final name = [
|
||||
c.firstName,
|
||||
c.lastName,
|
||||
].where((s) => s != null && s.trim().isNotEmpty).join(' ').trim();
|
||||
byId[c.id] = _Candidate(
|
||||
id: c.id,
|
||||
name: name.isEmpty ? '${c.id}' : name,
|
||||
avatarUrl: c.baseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
final dialogs = await AppDatabase.loadDialogChats(accountId);
|
||||
for (final row in dialogs) {
|
||||
final chat = CachedChat.fromDbRow(row);
|
||||
for (final pid in chat.participants.keys) {
|
||||
if (pid == accountId ||
|
||||
widget.excludeIds.contains(pid) ||
|
||||
byId.containsKey(pid)) {
|
||||
continue;
|
||||
}
|
||||
final name = chat.title ?? ContactCache.get(pid) ?? '$pid';
|
||||
byId[pid] = _Candidate(
|
||||
id: pid,
|
||||
name: name,
|
||||
avatarUrl: chat.iconUrl ?? ContactCache.getAvatar(pid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final list = byId.values.toList()
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_all = list;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<_Candidate> get _filtered {
|
||||
final q = _query.trim().toLowerCase();
|
||||
if (q.isEmpty) return _all;
|
||||
return _all.where((c) => c.name.toLowerCase().contains(q)).toList();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_selected.isEmpty || _submitting) return;
|
||||
setState(() => _submitting = true);
|
||||
final ok = await chats.addMembers(
|
||||
api,
|
||||
chatId: widget.chatId,
|
||||
userIds: _selected.toList(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (ok) {
|
||||
Navigator.of(context).pop(true);
|
||||
if (widget.hostContext.mounted) {
|
||||
showCustomNotification(widget.hostContext, l10n.chatInfoMembersAdded);
|
||||
}
|
||||
} else {
|
||||
setState(() => _submitting = false);
|
||||
showCustomNotification(context, l10n.chatInfoAddMembersError);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
final maxListHeight = MediaQuery.sizeOf(context).height * 0.5;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
width: width > 420 ? 380 : double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.chatInfoAddMember,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
constraints: const BoxConstraints(maxWidth: 150),
|
||||
prefixIcon: Icon(
|
||||
Symbols.search,
|
||||
size: 18,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 34,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: l10n.chatInfoMembersSearchHint,
|
||||
hintStyle: TextStyle(
|
||||
color: cs.outline,
|
||||
fontSize: 14,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, thickness: 0.5, color: cs.outlineVariant),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxListHeight),
|
||||
child: _buildList(cs, l10n),
|
||||
),
|
||||
Divider(height: 1, thickness: 0.5, color: cs.outlineVariant),
|
||||
_buildAddButton(cs, l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(ColorScheme cs, AppLocalizations l10n) {
|
||||
if (_loading) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final items = _filtered;
|
||||
if (items.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: Text(
|
||||
l10n.chatInfoAddMembersEmpty,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) => _candidateRow(cs, items[i]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _candidateRow(ColorScheme cs, _Candidate c) {
|
||||
final selected = _selected.contains(c.id);
|
||||
return InkWell(
|
||||
onTap: () => setState(() {
|
||||
if (selected) {
|
||||
_selected.remove(c.id);
|
||||
} else {
|
||||
_selected.add(c.id);
|
||||
}
|
||||
}),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
KometAvatar(name: c.name, imageUrl: c.avatarUrl, size: 42),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
c.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
selected
|
||||
? Symbols.check_circle
|
||||
: Symbols.radio_button_unchecked,
|
||||
fill: selected ? 1 : 0,
|
||||
color: selected ? cs.primary : cs.outline,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddButton(ColorScheme cs, AppLocalizations l10n) {
|
||||
final enabled = _selected.isNotEmpty && !_submitting;
|
||||
return InkWell(
|
||||
onTap: enabled ? _submit : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_submitting)
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
_selected.isEmpty
|
||||
? l10n.chatInfoAddMembersAction
|
||||
: '${l10n.chatInfoAddMembersAction} · ${_selected.length}',
|
||||
style: TextStyle(
|
||||
color: enabled ? cs.primary : cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showInviteLinkSheet(
|
||||
BuildContext context, {
|
||||
required String link,
|
||||
required String title,
|
||||
String? avatarUrl,
|
||||
}) {
|
||||
return showBlurredCard<void>(
|
||||
context,
|
||||
(host) => _InviteLinkCard(
|
||||
link: link,
|
||||
title: title,
|
||||
avatarUrl: avatarUrl,
|
||||
hostContext: host,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _InviteLinkCard extends StatelessWidget {
|
||||
final String link;
|
||||
final String title;
|
||||
final String? avatarUrl;
|
||||
final BuildContext hostContext;
|
||||
|
||||
const _InviteLinkCard({
|
||||
required this.link,
|
||||
required this.title,
|
||||
this.avatarUrl,
|
||||
required this.hostContext,
|
||||
});
|
||||
|
||||
String get _shortLink => link.replaceFirst(RegExp(r'^https?://'), '');
|
||||
|
||||
Future<void> _copy(BuildContext context) async {
|
||||
final message = AppLocalizations.of(context)!.sharedLinkCopied;
|
||||
await Clipboard.setData(ClipboardData(text: link));
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
if (hostContext.mounted) showCustomNotification(hostContext, message);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
width: width > 420 ? 380 : double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.chatInfoInviteLink.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 6, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
KometAvatar(name: title, imageUrl: avatarUrl, size: 40),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_shortLink,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Symbols.content_copy, color: cs.primary),
|
||||
onPressed: () => _copy(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.chatInfoInviteLinkHint,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => _copy(context),
|
||||
icon: const Icon(Symbols.content_copy, size: 20),
|
||||
label: Text(l10n.sharedCopyLink),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -583,6 +583,14 @@
|
||||
"chatInfoAddMember": "Add member",
|
||||
"chatInfoRoleOwner": "owner",
|
||||
"chatInfoRoleAdmin": "Admin",
|
||||
"chatInfoMemberDeleted": "Account deleted",
|
||||
"chatInfoInviteByLink": "Invite via link",
|
||||
"chatInfoInviteLinkHint": "You can invite anyone with this link",
|
||||
"chatInfoAddMembersAction": "Add",
|
||||
"chatInfoMembersSearchHint": "Search",
|
||||
"chatInfoAddMembersEmpty": "No one to add",
|
||||
"chatInfoMembersAdded": "Members added",
|
||||
"chatInfoAddMembersError": "Couldn't add members",
|
||||
"chatInfoNoData": "No data",
|
||||
"chatInfoHideExtra": "Hide",
|
||||
"chatInfoShowMoreExtra": "Details",
|
||||
|
||||
@@ -2696,6 +2696,54 @@ abstract class AppLocalizations {
|
||||
/// **'Admin'**
|
||||
String get chatInfoRoleAdmin;
|
||||
|
||||
/// No description provided for @chatInfoMemberDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account deleted'**
|
||||
String get chatInfoMemberDeleted;
|
||||
|
||||
/// No description provided for @chatInfoInviteByLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invite via link'**
|
||||
String get chatInfoInviteByLink;
|
||||
|
||||
/// No description provided for @chatInfoInviteLinkHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can invite anyone with this link'**
|
||||
String get chatInfoInviteLinkHint;
|
||||
|
||||
/// No description provided for @chatInfoAddMembersAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add'**
|
||||
String get chatInfoAddMembersAction;
|
||||
|
||||
/// No description provided for @chatInfoMembersSearchHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search'**
|
||||
String get chatInfoMembersSearchHint;
|
||||
|
||||
/// No description provided for @chatInfoAddMembersEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No one to add'**
|
||||
String get chatInfoAddMembersEmpty;
|
||||
|
||||
/// No description provided for @chatInfoMembersAdded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Members added'**
|
||||
String get chatInfoMembersAdded;
|
||||
|
||||
/// No description provided for @chatInfoAddMembersError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t add members'**
|
||||
String get chatInfoAddMembersError;
|
||||
|
||||
/// No description provided for @chatInfoNoData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -1385,6 +1385,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chatInfoRoleAdmin => 'Admin';
|
||||
|
||||
@override
|
||||
String get chatInfoMemberDeleted => 'Account deleted';
|
||||
|
||||
@override
|
||||
String get chatInfoInviteByLink => 'Invite via link';
|
||||
|
||||
@override
|
||||
String get chatInfoInviteLinkHint => 'You can invite anyone with this link';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersAction => 'Add';
|
||||
|
||||
@override
|
||||
String get chatInfoMembersSearchHint => 'Search';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersEmpty => 'No one to add';
|
||||
|
||||
@override
|
||||
String get chatInfoMembersAdded => 'Members added';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersError => 'Couldn\'t add members';
|
||||
|
||||
@override
|
||||
String get chatInfoNoData => 'No data';
|
||||
|
||||
|
||||
@@ -1393,6 +1393,31 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chatInfoRoleAdmin => 'Админ';
|
||||
|
||||
@override
|
||||
String get chatInfoMemberDeleted => 'Аккаунт удалён';
|
||||
|
||||
@override
|
||||
String get chatInfoInviteByLink => 'Пригласить по ссылке';
|
||||
|
||||
@override
|
||||
String get chatInfoInviteLinkHint =>
|
||||
'Вы можете пригласить любого человека по этой ссылке';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersAction => 'Добавить';
|
||||
|
||||
@override
|
||||
String get chatInfoMembersSearchHint => 'Поиск';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersEmpty => 'Некого добавить';
|
||||
|
||||
@override
|
||||
String get chatInfoMembersAdded => 'Участники добавлены';
|
||||
|
||||
@override
|
||||
String get chatInfoAddMembersError => 'Не удалось добавить участников';
|
||||
|
||||
@override
|
||||
String get chatInfoNoData => 'Нет данных';
|
||||
|
||||
|
||||
@@ -456,6 +456,14 @@
|
||||
"chatInfoAddMember": "Добавить участника",
|
||||
"chatInfoRoleOwner": "Владелец",
|
||||
"chatInfoRoleAdmin": "Админ",
|
||||
"chatInfoMemberDeleted": "Аккаунт удалён",
|
||||
"chatInfoInviteByLink": "Пригласить по ссылке",
|
||||
"chatInfoInviteLinkHint": "Вы можете пригласить любого человека по этой ссылке",
|
||||
"chatInfoAddMembersAction": "Добавить",
|
||||
"chatInfoMembersSearchHint": "Поиск",
|
||||
"chatInfoAddMembersEmpty": "Некого добавить",
|
||||
"chatInfoMembersAdded": "Участники добавлены",
|
||||
"chatInfoAddMembersError": "Не удалось добавить участников",
|
||||
"chatInfoNoData": "Нет данных",
|
||||
"chatInfoHideExtra": "Скрыть",
|
||||
"chatInfoShowMoreExtra": "Подробнее",
|
||||
|
||||
@@ -23,6 +23,16 @@ class ChatInfo {
|
||||
bool isAdmin(int id) => adminIds.contains(id);
|
||||
bool isOwner(int id) => owner != null && id == owner;
|
||||
|
||||
String? adminAlias(int id) {
|
||||
final source = raw['adminParticipants'];
|
||||
if (source is! Map) return null;
|
||||
final entry = source[id.toString()] ?? source[id];
|
||||
if (entry is! Map) return null;
|
||||
final alias = entry['alias'];
|
||||
if (alias is String && alias.trim().isNotEmpty) return alias.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
int? get participantsCount => raw['participantsCount'] as int?;
|
||||
int? get blockedParticipantsCount => raw['blockedParticipantsCount'] as int?;
|
||||
String? get link => raw['link'] as String?;
|
||||
|
||||
@@ -69,5 +69,10 @@ class ContactInfo {
|
||||
|
||||
bool get isBot => options.contains('BOT');
|
||||
|
||||
bool get isDeleted {
|
||||
final status = raw['accountStatus'];
|
||||
return status is int && status != 0;
|
||||
}
|
||||
|
||||
int? get id => raw['id'] as int?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user