|
|
|
@ -27,13 +27,15 @@ import 'map_screen.dart';
|
|
|
|
import 'repeater_hub_screen.dart';
|
|
|
|
import 'repeater_hub_screen.dart';
|
|
|
|
import 'settings_screen.dart';
|
|
|
|
import 'settings_screen.dart';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enum RoomLoginDestination {
|
|
|
|
|
|
|
|
chat,
|
|
|
|
|
|
|
|
management,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ContactsScreen extends StatefulWidget {
|
|
|
|
class ContactsScreen extends StatefulWidget {
|
|
|
|
final bool hideBackButton;
|
|
|
|
final bool hideBackButton;
|
|
|
|
|
|
|
|
|
|
|
|
const ContactsScreen({
|
|
|
|
const ContactsScreen({super.key, this.hideBackButton = false});
|
|
|
|
super.key,
|
|
|
|
|
|
|
|
this.hideBackButton = false,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
State<ContactsScreen> createState() => _ContactsScreenState();
|
|
|
|
State<ContactsScreen> createState() => _ContactsScreenState();
|
|
|
|
@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
top: false,
|
|
|
|
top: false,
|
|
|
|
child: QuickSwitchBar(
|
|
|
|
child: QuickSwitchBar(
|
|
|
|
selectedIndex: 0,
|
|
|
|
selectedIndex: 0,
|
|
|
|
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
|
|
|
|
onDestinationSelected: (index) =>
|
|
|
|
|
|
|
|
_handleQuickSwitch(index, context),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -168,8 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final filteredAndSorted = _filterAndSortContacts(contacts, connector);
|
|
|
|
final filteredAndSorted = _filterAndSortContacts(contacts, connector);
|
|
|
|
final filteredGroups =
|
|
|
|
final filteredGroups = _showUnreadOnly
|
|
|
|
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts);
|
|
|
|
? const <ContactGroup>[]
|
|
|
|
|
|
|
|
: _filterAndSortGroups(_groups, contacts);
|
|
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
children: [
|
|
|
|
@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
|
|
|
|
|
|
horizontal: 16,
|
|
|
|
|
|
|
|
vertical: 12,
|
|
|
|
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
onChanged: (value) {
|
|
|
|
onChanged: (value) {
|
|
|
|
_searchDebounce?.cancel();
|
|
|
|
_searchDebounce?.cancel();
|
|
|
|
@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
final group = filteredGroups[index];
|
|
|
|
final group = filteredGroups[index];
|
|
|
|
return _buildGroupTile(context, group, contacts);
|
|
|
|
return _buildGroupTile(context, group, contacts);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final contact = filteredAndSorted[index - filteredGroups.length];
|
|
|
|
final contact =
|
|
|
|
final unreadCount = connector.getUnreadCountForContact(contact);
|
|
|
|
filteredAndSorted[index - filteredGroups.length];
|
|
|
|
|
|
|
|
final unreadCount = connector.getUnreadCountForContact(
|
|
|
|
|
|
|
|
contact,
|
|
|
|
|
|
|
|
);
|
|
|
|
return _ContactTile(
|
|
|
|
return _ContactTile(
|
|
|
|
contact: contact,
|
|
|
|
contact: contact,
|
|
|
|
lastSeen: _resolveLastSeen(contact),
|
|
|
|
lastSeen: _resolveLastSeen(contact),
|
|
|
|
unreadCount: unreadCount,
|
|
|
|
unreadCount: unreadCount,
|
|
|
|
onTap: () => _openChat(context, contact),
|
|
|
|
onTap: () => _openChat(context, contact),
|
|
|
|
onLongPress: () => _showContactOptions(context, connector, contact),
|
|
|
|
onLongPress: () =>
|
|
|
|
|
|
|
|
_showContactOptions(context, connector, contact),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -255,35 +266,48 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<ContactGroup> _filterAndSortGroups(List<ContactGroup> groups, List<Contact> contacts) {
|
|
|
|
List<ContactGroup> _filterAndSortGroups(
|
|
|
|
|
|
|
|
List<ContactGroup> groups,
|
|
|
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
|
|
|
) {
|
|
|
|
final query = _searchQuery.trim().toLowerCase();
|
|
|
|
final query = _searchQuery.trim().toLowerCase();
|
|
|
|
final contactsByKey = <String, Contact>{};
|
|
|
|
final contactsByKey = <String, Contact>{};
|
|
|
|
for (final contact in contacts) {
|
|
|
|
for (final contact in contacts) {
|
|
|
|
contactsByKey[contact.publicKeyHex] = contact;
|
|
|
|
contactsByKey[contact.publicKeyHex] = contact;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final filtered = groups.where((group) {
|
|
|
|
final filtered = groups
|
|
|
|
if (query.isEmpty) return true;
|
|
|
|
.where((group) {
|
|
|
|
if (group.name.toLowerCase().contains(query)) return true;
|
|
|
|
if (query.isEmpty) return true;
|
|
|
|
for (final key in group.memberKeys) {
|
|
|
|
if (group.name.toLowerCase().contains(query)) return true;
|
|
|
|
final contact = contactsByKey[key];
|
|
|
|
for (final key in group.memberKeys) {
|
|
|
|
if (contact != null && matchesContactQuery(contact, query)) return true;
|
|
|
|
final contact = contactsByKey[key];
|
|
|
|
}
|
|
|
|
if (contact != null && matchesContactQuery(contact, query)) {
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}).where((group) {
|
|
|
|
}
|
|
|
|
if (_typeFilter == ContactTypeFilter.all) return true;
|
|
|
|
}
|
|
|
|
for (final key in group.memberKeys) {
|
|
|
|
return false;
|
|
|
|
final contact = contactsByKey[key];
|
|
|
|
})
|
|
|
|
if (contact != null && _matchesTypeFilter(contact)) return true;
|
|
|
|
.where((group) {
|
|
|
|
}
|
|
|
|
if (_typeFilter == ContactTypeFilter.all) return true;
|
|
|
|
return false;
|
|
|
|
for (final key in group.memberKeys) {
|
|
|
|
}).toList();
|
|
|
|
final contact = contactsByKey[key];
|
|
|
|
|
|
|
|
if (contact != null && _matchesTypeFilter(contact)) return true;
|
|
|
|
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
filtered.sort(
|
|
|
|
|
|
|
|
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
|
|
|
|
|
|
|
);
|
|
|
|
return filtered;
|
|
|
|
return filtered;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) {
|
|
|
|
List<Contact> _filterAndSortContacts(
|
|
|
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
|
|
|
MeshCoreConnector connector,
|
|
|
|
|
|
|
|
) {
|
|
|
|
var filtered = contacts.where((contact) {
|
|
|
|
var filtered = contacts.where((contact) {
|
|
|
|
if (_searchQuery.isEmpty) return true;
|
|
|
|
if (_searchQuery.isEmpty) return true;
|
|
|
|
return matchesContactQuery(contact, _searchQuery);
|
|
|
|
return matchesContactQuery(contact, _searchQuery);
|
|
|
|
@ -301,19 +325,27 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
|
|
|
|
|
|
|
|
switch (_sortOption) {
|
|
|
|
switch (_sortOption) {
|
|
|
|
case ContactSortOption.lastSeen:
|
|
|
|
case ContactSortOption.lastSeen:
|
|
|
|
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)));
|
|
|
|
filtered.sort(
|
|
|
|
|
|
|
|
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
|
|
|
|
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
case ContactSortOption.recentMessages:
|
|
|
|
case ContactSortOption.recentMessages:
|
|
|
|
filtered.sort((a, b) {
|
|
|
|
filtered.sort((a, b) {
|
|
|
|
final aMessages = connector.getMessages(a);
|
|
|
|
final aMessages = connector.getMessages(a);
|
|
|
|
final bMessages = connector.getMessages(b);
|
|
|
|
final bMessages = connector.getMessages(b);
|
|
|
|
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
|
|
|
|
final aLastMsg = aMessages.isEmpty
|
|
|
|
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
|
|
|
|
? DateTime(1970)
|
|
|
|
|
|
|
|
: aMessages.last.timestamp;
|
|
|
|
|
|
|
|
final bLastMsg = bMessages.isEmpty
|
|
|
|
|
|
|
|
? DateTime(1970)
|
|
|
|
|
|
|
|
: bMessages.last.timestamp;
|
|
|
|
return bLastMsg.compareTo(aLastMsg);
|
|
|
|
return bLastMsg.compareTo(aLastMsg);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
case ContactSortOption.name:
|
|
|
|
case ContactSortOption.name:
|
|
|
|
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
|
|
filtered.sort(
|
|
|
|
|
|
|
|
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
|
|
|
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -340,7 +372,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
: contact.lastSeen;
|
|
|
|
: contact.lastSeen;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildGroupTile(BuildContext context, ContactGroup group, List<Contact> contacts) {
|
|
|
|
Widget _buildGroupTile(
|
|
|
|
|
|
|
|
BuildContext context,
|
|
|
|
|
|
|
|
ContactGroup group,
|
|
|
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
|
|
|
) {
|
|
|
|
final memberContacts = _resolveGroupContacts(group, contacts);
|
|
|
|
final memberContacts = _resolveGroupContacts(group, contacts);
|
|
|
|
final subtitle = _formatGroupMembers(context, memberContacts);
|
|
|
|
final subtitle = _formatGroupMembers(context, memberContacts);
|
|
|
|
return ListTile(
|
|
|
|
return ListTile(
|
|
|
|
@ -359,7 +395,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<Contact> _resolveGroupContacts(ContactGroup group, List<Contact> contacts) {
|
|
|
|
List<Contact> _resolveGroupContacts(
|
|
|
|
|
|
|
|
ContactGroup group,
|
|
|
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
|
|
|
) {
|
|
|
|
final byKey = <String, Contact>{};
|
|
|
|
final byKey = <String, Contact>{};
|
|
|
|
for (final contact in contacts) {
|
|
|
|
for (final contact in contacts) {
|
|
|
|
byKey[contact.publicKeyHex] = contact;
|
|
|
|
byKey[contact.publicKeyHex] = contact;
|
|
|
|
@ -371,7 +410,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
resolved.add(contact);
|
|
|
|
resolved.add(contact);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
|
|
resolved.sort(
|
|
|
|
|
|
|
|
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
|
|
|
|
|
|
|
);
|
|
|
|
return resolved;
|
|
|
|
return resolved;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -387,7 +428,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
if (contact.type == advTypeRepeater) {
|
|
|
|
if (contact.type == advTypeRepeater) {
|
|
|
|
_showRepeaterLogin(context, contact);
|
|
|
|
_showRepeaterLogin(context, contact);
|
|
|
|
} else if (contact.type == advTypeRoom) {
|
|
|
|
} else if (contact.type == advTypeRoom) {
|
|
|
|
_showRoomLogin(context, contact);
|
|
|
|
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
|
|
|
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
|
|
|
Navigator.push(
|
|
|
|
Navigator.push(
|
|
|
|
@ -403,17 +444,13 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
case 1:
|
|
|
|
case 1:
|
|
|
|
Navigator.pushReplacement(
|
|
|
|
Navigator.pushReplacement(
|
|
|
|
context,
|
|
|
|
context,
|
|
|
|
buildQuickSwitchRoute(
|
|
|
|
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
|
|
|
const ChannelsScreen(hideBackButton: true),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
case 2:
|
|
|
|
Navigator.pushReplacement(
|
|
|
|
Navigator.pushReplacement(
|
|
|
|
context,
|
|
|
|
context,
|
|
|
|
buildQuickSwitchRoute(
|
|
|
|
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
|
|
|
const MapScreen(hideBackButton: true),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -429,10 +466,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
Navigator.push(
|
|
|
|
Navigator.push(
|
|
|
|
context,
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (context) => RepeaterHubScreen(
|
|
|
|
builder: (context) =>
|
|
|
|
repeater: repeater,
|
|
|
|
RepeaterHubScreen(repeater: repeater, password: password),
|
|
|
|
password: password,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -440,18 +475,23 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _showRoomLogin(BuildContext context, Contact room) {
|
|
|
|
void _showRoomLogin(
|
|
|
|
|
|
|
|
BuildContext context,
|
|
|
|
|
|
|
|
Contact room,
|
|
|
|
|
|
|
|
RoomLoginDestination destination,
|
|
|
|
|
|
|
|
) {
|
|
|
|
showDialog(
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
context: context,
|
|
|
|
builder: (context) => RoomLoginDialog(
|
|
|
|
builder: (context) => RoomLoginDialog(
|
|
|
|
room: room,
|
|
|
|
room: room,
|
|
|
|
onLogin: (password) {
|
|
|
|
onLogin: (password) {
|
|
|
|
// Navigate to chat screen after successful login
|
|
|
|
|
|
|
|
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
|
|
|
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
|
|
|
Navigator.push(
|
|
|
|
Navigator.push(
|
|
|
|
context,
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (context) => ChatScreen(contact: room),
|
|
|
|
builder: (context) => destination == RoomLoginDestination.management
|
|
|
|
|
|
|
|
? RepeaterHubScreen(repeater: room, password: password)
|
|
|
|
|
|
|
|
: ChatScreen(contact: room),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -459,7 +499,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _showGroupOptions(BuildContext context, ContactGroup group, List<Contact> contacts) {
|
|
|
|
void _showGroupOptions(
|
|
|
|
|
|
|
|
BuildContext context,
|
|
|
|
|
|
|
|
ContactGroup group,
|
|
|
|
|
|
|
|
List<Contact> contacts,
|
|
|
|
|
|
|
|
) {
|
|
|
|
final members = _resolveGroupContacts(group, contacts);
|
|
|
|
final members = _resolveGroupContacts(group, contacts);
|
|
|
|
showModalBottomSheet(
|
|
|
|
showModalBottomSheet(
|
|
|
|
context: context,
|
|
|
|
context: context,
|
|
|
|
@ -478,7 +522,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
ListTile(
|
|
|
|
ListTile(
|
|
|
|
leading: const Icon(Icons.delete, color: Colors.red),
|
|
|
|
leading: const Icon(Icons.delete, color: Colors.red),
|
|
|
|
title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)),
|
|
|
|
title: Text(
|
|
|
|
|
|
|
|
context.l10n.contacts_deleteGroup,
|
|
|
|
|
|
|
|
style: const TextStyle(color: Colors.red),
|
|
|
|
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
onTap: () {
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
_confirmDeleteGroup(context, group);
|
|
|
|
_confirmDeleteGroup(context, group);
|
|
|
|
@ -522,7 +569,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
});
|
|
|
|
});
|
|
|
|
await _saveGroups();
|
|
|
|
await _saveGroups();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
|
|
|
|
child: Text(
|
|
|
|
|
|
|
|
context.l10n.common_delete,
|
|
|
|
|
|
|
|
style: const TextStyle(color: Colors.red),
|
|
|
|
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -548,10 +598,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
final filteredContacts = filterQuery.isEmpty
|
|
|
|
final filteredContacts = filterQuery.isEmpty
|
|
|
|
? sortedContacts
|
|
|
|
? sortedContacts
|
|
|
|
: sortedContacts
|
|
|
|
: sortedContacts
|
|
|
|
.where((contact) => matchesContactQuery(contact, filterQuery))
|
|
|
|
.where(
|
|
|
|
.toList();
|
|
|
|
(contact) => matchesContactQuery(contact, filterQuery),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.toList();
|
|
|
|
return AlertDialog(
|
|
|
|
return AlertDialog(
|
|
|
|
title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup),
|
|
|
|
title: Text(
|
|
|
|
|
|
|
|
isEditing
|
|
|
|
|
|
|
|
? context.l10n.contacts_editGroup
|
|
|
|
|
|
|
|
: context.l10n.contacts_newGroup,
|
|
|
|
|
|
|
|
),
|
|
|
|
content: SizedBox(
|
|
|
|
content: SizedBox(
|
|
|
|
width: double.maxFinite,
|
|
|
|
width: double.maxFinite,
|
|
|
|
child: Column(
|
|
|
|
child: Column(
|
|
|
|
@ -582,12 +638,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
SizedBox(
|
|
|
|
SizedBox(
|
|
|
|
height: 240,
|
|
|
|
height: 240,
|
|
|
|
child: filteredContacts.isEmpty
|
|
|
|
child: filteredContacts.isEmpty
|
|
|
|
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter))
|
|
|
|
? Center(
|
|
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
|
|
context.l10n.contacts_noContactsMatchFilter,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
: ListView.builder(
|
|
|
|
: ListView.builder(
|
|
|
|
itemCount: filteredContacts.length,
|
|
|
|
itemCount: filteredContacts.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final contact = filteredContacts[index];
|
|
|
|
final contact = filteredContacts[index];
|
|
|
|
final isSelected = selectedKeys.contains(contact.publicKeyHex);
|
|
|
|
final isSelected = selectedKeys.contains(
|
|
|
|
|
|
|
|
contact.publicKeyHex,
|
|
|
|
|
|
|
|
);
|
|
|
|
return CheckboxListTile(
|
|
|
|
return CheckboxListTile(
|
|
|
|
value: isSelected,
|
|
|
|
value: isSelected,
|
|
|
|
title: Text(contact.name),
|
|
|
|
title: Text(contact.name),
|
|
|
|
@ -618,7 +680,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
final name = nameController.text.trim();
|
|
|
|
final name = nameController.text.trim();
|
|
|
|
if (name.isEmpty) {
|
|
|
|
if (name.isEmpty) {
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)),
|
|
|
|
SnackBar(
|
|
|
|
|
|
|
|
content: Text(context.l10n.contacts_groupNameRequired),
|
|
|
|
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -628,13 +692,19 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
});
|
|
|
|
});
|
|
|
|
if (exists) {
|
|
|
|
if (exists) {
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))),
|
|
|
|
SnackBar(
|
|
|
|
|
|
|
|
content: Text(
|
|
|
|
|
|
|
|
context.l10n.contacts_groupAlreadyExists(name),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setState(() {
|
|
|
|
setState(() {
|
|
|
|
if (isEditing) {
|
|
|
|
if (isEditing) {
|
|
|
|
final index = _groups.indexWhere((g) => g.name == group.name);
|
|
|
|
final index = _groups.indexWhere(
|
|
|
|
|
|
|
|
(g) => g.name == group.name,
|
|
|
|
|
|
|
|
);
|
|
|
|
if (index != -1) {
|
|
|
|
if (index != -1) {
|
|
|
|
_groups[index] = ContactGroup(
|
|
|
|
_groups[index] = ContactGroup(
|
|
|
|
name: name,
|
|
|
|
name: name,
|
|
|
|
@ -642,7 +712,12 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList()));
|
|
|
|
_groups.add(
|
|
|
|
|
|
|
|
ContactGroup(
|
|
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
|
|
memberKeys: selectedKeys.toList(),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
await _saveGroups();
|
|
|
|
await _saveGroups();
|
|
|
|
@ -650,7 +725,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
Navigator.pop(dialogContext);
|
|
|
|
Navigator.pop(dialogContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create),
|
|
|
|
child: Text(
|
|
|
|
|
|
|
|
isEditing
|
|
|
|
|
|
|
|
? context.l10n.common_save
|
|
|
|
|
|
|
|
: context.l10n.common_create,
|
|
|
|
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@ -682,16 +761,24 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
_showRepeaterLogin(context, contact);
|
|
|
|
_showRepeaterLogin(context, contact);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
else if (isRoom)
|
|
|
|
else if (isRoom) ...[
|
|
|
|
ListTile(
|
|
|
|
ListTile(
|
|
|
|
leading: const Icon(Icons.room, color: Colors.blue),
|
|
|
|
leading: const Icon(Icons.room, color: Colors.blue),
|
|
|
|
title: Text(context.l10n.contacts_roomLogin),
|
|
|
|
title: Text(context.l10n.contacts_roomLogin),
|
|
|
|
onTap: () {
|
|
|
|
onTap: () {
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
_showRoomLogin(context, contact);
|
|
|
|
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
),
|
|
|
|
else
|
|
|
|
ListTile(
|
|
|
|
|
|
|
|
leading: const Icon(Icons.room_preferences, color: Colors.orange),
|
|
|
|
|
|
|
|
title: Text(context.l10n.room_management),
|
|
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
|
|
|
|
_showRoomLogin(context, contact, RoomLoginDestination.management);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
] else
|
|
|
|
ListTile(
|
|
|
|
ListTile(
|
|
|
|
leading: const Icon(Icons.chat),
|
|
|
|
leading: const Icon(Icons.chat),
|
|
|
|
title: Text(context.l10n.contacts_openChat),
|
|
|
|
title: Text(context.l10n.contacts_openChat),
|
|
|
|
@ -702,7 +789,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
ListTile(
|
|
|
|
ListTile(
|
|
|
|
leading: const Icon(Icons.delete, color: Colors.red),
|
|
|
|
leading: const Icon(Icons.delete, color: Colors.red),
|
|
|
|
title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)),
|
|
|
|
title: Text(
|
|
|
|
|
|
|
|
context.l10n.contacts_deleteContact,
|
|
|
|
|
|
|
|
style: const TextStyle(color: Colors.red),
|
|
|
|
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
onTap: () {
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
Navigator.pop(sheetContext);
|
|
|
|
_confirmDelete(context, connector, contact);
|
|
|
|
_confirmDelete(context, connector, contact);
|
|
|
|
@ -734,7 +824,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|
|
|
Navigator.pop(dialogContext);
|
|
|
|
Navigator.pop(dialogContext);
|
|
|
|
connector.removeContact(contact);
|
|
|
|
connector.removeContact(contact);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
|
|
|
|
child: Text(
|
|
|
|
|
|
|
|
context.l10n.common_delete,
|
|
|
|
|
|
|
|
style: const TextStyle(color: Colors.red),
|
|
|
|
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -759,14 +852,17 @@ class _ContactTile extends StatelessWidget {
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final shotPublicKey = "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
|
|
|
|
final shotPublicKey =
|
|
|
|
|
|
|
|
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
|
|
|
|
return ListTile(
|
|
|
|
return ListTile(
|
|
|
|
leading: CircleAvatar(
|
|
|
|
leading: CircleAvatar(
|
|
|
|
backgroundColor: _getTypeColor(contact.type),
|
|
|
|
backgroundColor: _getTypeColor(contact.type),
|
|
|
|
child: _buildContactAvatar(contact),
|
|
|
|
child: _buildContactAvatar(contact),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
title: Text(contact.name),
|
|
|
|
title: Text(contact.name),
|
|
|
|
subtitle: Text('${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey'),
|
|
|
|
subtitle: Text(
|
|
|
|
|
|
|
|
'${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey',
|
|
|
|
|
|
|
|
),
|
|
|
|
trailing: Column(
|
|
|
|
trailing: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
@ -791,10 +887,7 @@ class _ContactTile extends StatelessWidget {
|
|
|
|
Widget _buildContactAvatar(Contact contact) {
|
|
|
|
Widget _buildContactAvatar(Contact contact) {
|
|
|
|
final emoji = firstEmoji(contact.name);
|
|
|
|
final emoji = firstEmoji(contact.name);
|
|
|
|
if (emoji != null) {
|
|
|
|
if (emoji != null) {
|
|
|
|
return Text(
|
|
|
|
return Text(emoji, style: const TextStyle(fontSize: 18));
|
|
|
|
emoji,
|
|
|
|
|
|
|
|
style: const TextStyle(fontSize: 18),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
|
|
|
|
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -833,13 +926,21 @@ class _ContactTile extends StatelessWidget {
|
|
|
|
final now = DateTime.now();
|
|
|
|
final now = DateTime.now();
|
|
|
|
final diff = now.difference(lastSeen);
|
|
|
|
final diff = now.difference(lastSeen);
|
|
|
|
|
|
|
|
|
|
|
|
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow;
|
|
|
|
if (diff.isNegative || diff.inMinutes < 5) {
|
|
|
|
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
|
|
|
return context.l10n.contacts_lastSeenNow;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (diff.inMinutes < 60) {
|
|
|
|
|
|
|
|
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
|
|
|
|
|
|
|
}
|
|
|
|
if (diff.inHours < 24) {
|
|
|
|
if (diff.inHours < 24) {
|
|
|
|
final hours = diff.inHours;
|
|
|
|
final hours = diff.inHours;
|
|
|
|
return hours == 1 ? context.l10n.contacts_lastSeenHourAgo : context.l10n.contacts_lastSeenHoursAgo(hours);
|
|
|
|
return hours == 1
|
|
|
|
|
|
|
|
? context.l10n.contacts_lastSeenHourAgo
|
|
|
|
|
|
|
|
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final days = diff.inDays;
|
|
|
|
final days = diff.inDays;
|
|
|
|
return days == 1 ? context.l10n.contacts_lastSeenDayAgo : context.l10n.contacts_lastSeenDaysAgo(days);
|
|
|
|
return days == 1
|
|
|
|
|
|
|
|
? context.l10n.contacts_lastSeenDayAgo
|
|
|
|
|
|
|
|
: context.l10n.contacts_lastSeenDaysAgo(days);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|