mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
fix: Align contact sorting labels #0
This commit is contained in:
@@ -57,9 +57,7 @@ class Channel {
|
|||||||
} else {
|
} else {
|
||||||
// Normal channel: require explicit secret
|
// Normal channel: require explicit secret
|
||||||
if (explicitSecret == null || explicitSecret.length != 16) {
|
if (explicitSecret == null || explicitSecret.length != 16) {
|
||||||
throw ArgumentError(
|
throw ArgumentError('Normal channels require a 16-byte secret');
|
||||||
'Normal channels require a 16-byte secret',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Channel(
|
return Channel(
|
||||||
index: index,
|
index: index,
|
||||||
@@ -78,6 +76,18 @@ class Channel {
|
|||||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool isHashChannelName(String channelName) {
|
||||||
|
return channelName.trim().startsWith('#');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String pskBase64ForHashChannelName(String channelName) {
|
||||||
|
final normalized = channelName.trim();
|
||||||
|
if (!isHashChannelName(normalized)) {
|
||||||
|
throw ArgumentError('Only #channels can export derived psk_base64');
|
||||||
|
}
|
||||||
|
return base64.encode(_generateHashChannelSecret(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
/// Create the default public channel (channel 0)
|
/// Create the default public channel (channel 0)
|
||||||
/// Uses the well-known pre-shared key from MeshCore
|
/// Uses the well-known pre-shared key from MeshCore
|
||||||
factory Channel.publicChannel() {
|
factory Channel.publicChannel() {
|
||||||
@@ -85,8 +95,22 @@ class Channel {
|
|||||||
index: 0,
|
index: 0,
|
||||||
name: 'Public Channel',
|
name: 'Public Channel',
|
||||||
secret: Uint8List.fromList([
|
secret: Uint8List.fromList([
|
||||||
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
0x8b,
|
||||||
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
|
0x33,
|
||||||
|
0x87,
|
||||||
|
0xe9,
|
||||||
|
0xc5,
|
||||||
|
0xcd,
|
||||||
|
0xea,
|
||||||
|
0x6a,
|
||||||
|
0xc9,
|
||||||
|
0xe5,
|
||||||
|
0xed,
|
||||||
|
0xba,
|
||||||
|
0xa1,
|
||||||
|
0x15,
|
||||||
|
0xcd,
|
||||||
|
0x72,
|
||||||
]),
|
]),
|
||||||
flags: null,
|
flags: null,
|
||||||
);
|
);
|
||||||
@@ -95,6 +119,9 @@ class Channel {
|
|||||||
/// Check if this is a hash-based channel (name starts with '#')
|
/// Check if this is a hash-based channel (name starts with '#')
|
||||||
bool get isHashChannel => name.startsWith('#');
|
bool get isHashChannel => name.startsWith('#');
|
||||||
|
|
||||||
|
/// Base64-encoded PSK for sharing with firmware CLI and related tooling.
|
||||||
|
String get pskBase64 => base64.encode(secret);
|
||||||
|
|
||||||
/// Display name for the channel
|
/// Display name for the channel
|
||||||
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
||||||
String get displayName {
|
String get displayName {
|
||||||
@@ -131,12 +158,7 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy with modified fields
|
/// Create a copy with modified fields
|
||||||
Channel copyWith({
|
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
|
||||||
int? index,
|
|
||||||
String? name,
|
|
||||||
Uint8List? secret,
|
|
||||||
int? flags,
|
|
||||||
}) {
|
|
||||||
return Channel(
|
return Channel(
|
||||||
index: index ?? this.index,
|
index: index ?? this.index,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
|
|||||||
@@ -107,6 +107,15 @@ class _PendingRepeaterOwnerRequest {
|
|||||||
const _PendingRepeaterOwnerRequest({required this.publicKey});
|
const _PendingRepeaterOwnerRequest({required this.publicKey});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ContactsTabSection {
|
||||||
|
favourites,
|
||||||
|
teamMembers,
|
||||||
|
repeaters,
|
||||||
|
sensors,
|
||||||
|
rooms,
|
||||||
|
channels,
|
||||||
|
}
|
||||||
|
|
||||||
/// Main App Provider - coordinates all other providers
|
/// Main App Provider - coordinates all other providers
|
||||||
class AppProvider with ChangeNotifier {
|
class AppProvider with ChangeNotifier {
|
||||||
static const int _maxDirectPayloadHops = 3;
|
static const int _maxDirectPayloadHops = 3;
|
||||||
@@ -162,6 +171,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
bool get isContactsEnabled => _isContactsEnabled;
|
bool get isContactsEnabled => _isContactsEnabled;
|
||||||
bool _isSensorsEnabled = true;
|
bool _isSensorsEnabled = true;
|
||||||
bool get isSensorsEnabled => _isSensorsEnabled;
|
bool get isSensorsEnabled => _isSensorsEnabled;
|
||||||
|
final Map<ContactsTabSection, bool> _contactsSectionVisibility = {
|
||||||
|
for (final section in ContactsTabSection.values) section: true,
|
||||||
|
};
|
||||||
|
|
||||||
bool _isVoiceSilenceTrimmingEnabled = true;
|
bool _isVoiceSilenceTrimmingEnabled = true;
|
||||||
bool get isVoiceSilenceTrimmingEnabled => _isVoiceSilenceTrimmingEnabled;
|
bool get isVoiceSilenceTrimmingEnabled => _isVoiceSilenceTrimmingEnabled;
|
||||||
@@ -238,6 +250,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
_initializeLocationTracking();
|
_initializeLocationTracking();
|
||||||
_loadMapEnabled();
|
_loadMapEnabled();
|
||||||
_loadContactsEnabled();
|
_loadContactsEnabled();
|
||||||
|
_loadContactsSectionVisibility();
|
||||||
_loadSensorsEnabled();
|
_loadSensorsEnabled();
|
||||||
_loadVoiceSilenceTrimmingEnabled();
|
_loadVoiceSilenceTrimmingEnabled();
|
||||||
_loadVoiceBandPassFilterEnabled();
|
_loadVoiceBandPassFilterEnabled();
|
||||||
@@ -259,6 +272,27 @@ class AppProvider with ChangeNotifier {
|
|||||||
return ProfileStorageScope.scopedKey(baseKey);
|
return ProfileStorageScope.scopedKey(baseKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isContactsSectionEnabled(ContactsTabSection section) {
|
||||||
|
return _contactsSectionVisibility[section] ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _contactsSectionVisibilityKey(ContactsTabSection section) {
|
||||||
|
switch (section) {
|
||||||
|
case ContactsTabSection.favourites:
|
||||||
|
return 'contacts_section_favourites_enabled';
|
||||||
|
case ContactsTabSection.teamMembers:
|
||||||
|
return 'contacts_section_team_members_enabled';
|
||||||
|
case ContactsTabSection.repeaters:
|
||||||
|
return 'contacts_section_repeaters_enabled';
|
||||||
|
case ContactsTabSection.sensors:
|
||||||
|
return 'contacts_section_sensors_enabled';
|
||||||
|
case ContactsTabSection.rooms:
|
||||||
|
return 'contacts_section_rooms_enabled';
|
||||||
|
case ContactsTabSection.channels:
|
||||||
|
return 'contacts_section_channels_enabled';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _startPacketCapturePersistence() {
|
void _startPacketCapturePersistence() {
|
||||||
_packetCaptureFlushTimer?.cancel();
|
_packetCaptureFlushTimer?.cancel();
|
||||||
_packetCaptureFlushTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
_packetCaptureFlushTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
@@ -565,6 +599,37 @@ class AppProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContactsSectionVisibility() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
for (final section in ContactsTabSection.values) {
|
||||||
|
_contactsSectionVisibility[section] =
|
||||||
|
prefs.getBool(_scopedKey(_contactsSectionVisibilityKey(section))) ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading contacts section visibility settings: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setContactsSectionEnabled(
|
||||||
|
ContactsTabSection section,
|
||||||
|
bool enabled,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
_contactsSectionVisibility[section] = enabled;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(
|
||||||
|
_scopedKey(_contactsSectionVisibilityKey(section)),
|
||||||
|
enabled,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving contacts section visibility setting: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Load sensors enabled setting from shared preferences
|
/// Load sensors enabled setting from shared preferences
|
||||||
Future<void> _loadSensorsEnabled() async {
|
Future<void> _loadSensorsEnabled() async {
|
||||||
try {
|
try {
|
||||||
@@ -933,9 +998,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
// we also import into firmware so subsequent getContact calls work.
|
// we also import into firmware so subsequent getContact calls work.
|
||||||
// Matches the official app which calls cmdGetAdvertPath for all adverts.
|
// Matches the official app which calls cmdGetAdvertPath for all adverts.
|
||||||
if (source == ContactReceiveSource.advert) {
|
if (source == ContactReceiveSource.advert) {
|
||||||
unawaited(
|
unawaited(connectionProvider.importReceivedAdvert(contact.publicKey));
|
||||||
connectionProvider.importReceivedAdvert(contact.publicKey),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final isNewPendingAdvert = contactsProvider
|
final isNewPendingAdvert = contactsProvider
|
||||||
.addOrUpdatePendingAdvertContact(
|
.addOrUpdatePendingAdvertContact(
|
||||||
@@ -1352,9 +1415,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (_handlePendingRepeaterOwnerResponse(tag, responseData)) {
|
if (_handlePendingRepeaterOwnerResponse(tag, responseData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debugPrint(
|
debugPrint('📊 [AppProvider] Binary response (0x8C tag=$tag) received');
|
||||||
'📊 [AppProvider] Binary response (0x8C tag=$tag) received',
|
|
||||||
);
|
|
||||||
// Binary responses carry Cayenne LPP telemetry data.
|
// Binary responses carry Cayenne LPP telemetry data.
|
||||||
// The data starts with a channel byte — valid LPP always has at least
|
// The data starts with a channel byte — valid LPP always has at least
|
||||||
// 3 bytes (channel + type + value). Skip clearly non-telemetry payloads.
|
// 3 bytes (channel + type + value). Skip clearly non-telemetry payloads.
|
||||||
@@ -2159,9 +2220,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
// already added to pending adverts and showed notification.
|
// already added to pending adverts and showed notification.
|
||||||
// Nothing else to do — the callback pipeline handles everything.
|
// Nothing else to do — the callback pipeline handles everything.
|
||||||
if (wasKnown) {
|
if (wasKnown) {
|
||||||
debugPrint(
|
debugPrint(' [pushAdvert] Existing contact refreshed');
|
||||||
' [pushAdvert] Existing contact refreshed',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2721,6 +2780,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
await Future.wait([
|
await Future.wait([
|
||||||
_loadMapEnabled(),
|
_loadMapEnabled(),
|
||||||
_loadContactsEnabled(),
|
_loadContactsEnabled(),
|
||||||
|
_loadContactsSectionVisibility(),
|
||||||
_loadSensorsEnabled(),
|
_loadSensorsEnabled(),
|
||||||
_loadVoiceSilenceTrimmingEnabled(),
|
_loadVoiceSilenceTrimmingEnabled(),
|
||||||
_loadVoiceBandPassFilterEnabled(),
|
_loadVoiceBandPassFilterEnabled(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import '../services/voice_recorder_service.dart';
|
|||||||
import '../services/voice_codec_service.dart';
|
import '../services/voice_codec_service.dart';
|
||||||
import '../utils/toast_logger.dart';
|
import '../utils/toast_logger.dart';
|
||||||
import '../utils/key_comparison.dart';
|
import '../utils/key_comparison.dart';
|
||||||
|
import '../utils/contact_sorting.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
import '../utils/tictactoe_message_parser.dart';
|
import '../utils/tictactoe_message_parser.dart';
|
||||||
@@ -299,27 +300,27 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
final normalizedQuery = query.trim().toLowerCase();
|
final normalizedQuery = query.trim().toLowerCase();
|
||||||
final contacts =
|
final contacts =
|
||||||
contactsProvider.contacts
|
contactsProvider.chatContacts.where((contact) {
|
||||||
.where((contact) => contact.type == ContactType.chat)
|
if (normalizedQuery.isEmpty) return true;
|
||||||
.where((contact) {
|
return contact.displayName.toLowerCase().contains(normalizedQuery);
|
||||||
if (normalizedQuery.isEmpty) return true;
|
}).toList()..sort((a, b) {
|
||||||
return contact.displayName.toLowerCase().contains(
|
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||||
normalizedQuery,
|
if (primary != 0) {
|
||||||
);
|
return primary;
|
||||||
})
|
}
|
||||||
.toList()
|
|
||||||
..sort((a, b) {
|
final aName = a.displayName.toLowerCase();
|
||||||
final aName = a.displayName.toLowerCase();
|
final bName = b.displayName.toLowerCase();
|
||||||
final bName = b.displayName.toLowerCase();
|
final aStarts =
|
||||||
final aStarts =
|
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
|
||||||
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
|
final bStarts =
|
||||||
final bStarts =
|
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
|
||||||
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
|
if (aStarts != bStarts) {
|
||||||
if (aStarts != bStarts) {
|
return aStarts ? -1 : 1;
|
||||||
return aStarts ? -1 : 1;
|
}
|
||||||
}
|
|
||||||
return aName.compareTo(bName);
|
return compareContactsByDisplayName(a, b);
|
||||||
});
|
});
|
||||||
|
|
||||||
return contacts.take(8).toList(growable: false);
|
return contacts.take(8).toList(growable: false);
|
||||||
}
|
}
|
||||||
@@ -408,16 +409,33 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
|
||||||
// Filter contacts by type
|
final contacts = List<Contact>.from(contactsProvider.chatContacts)
|
||||||
final contacts = contactsProvider.contacts
|
..sort((a, b) {
|
||||||
.where((c) => c.type == ContactType.chat)
|
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||||
.toList();
|
if (primary != 0) {
|
||||||
final rooms = contactsProvider.contacts
|
return primary;
|
||||||
.where((c) => c.type == ContactType.room)
|
}
|
||||||
.toList();
|
|
||||||
final channels = contactsProvider.contacts
|
return compareContactsByDisplayName(a, b);
|
||||||
.where((c) => c.type == ContactType.channel)
|
});
|
||||||
.toList();
|
final rooms = List<Contact>.from(contactsProvider.rooms)
|
||||||
|
..sort((a, b) {
|
||||||
|
final primary = compareContactsByLastSeen(a, b);
|
||||||
|
if (primary != 0) {
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareContactsByDisplayName(a, b);
|
||||||
|
});
|
||||||
|
final channels = List<Contact>.from(contactsProvider.channels)
|
||||||
|
..sort((a, b) {
|
||||||
|
final primary = compareContactsByLastSeen(a, b);
|
||||||
|
if (primary != 0) {
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareContactsByDisplayName(a, b);
|
||||||
|
});
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -534,7 +552,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}).firstOrNull;
|
}).firstOrNull;
|
||||||
|
|
||||||
if (recipient == null) {
|
if (recipient == null) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,13 +584,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
} else {
|
} else {
|
||||||
final senderPrefix = message.senderPublicKeyPrefix;
|
final senderPrefix = message.senderPublicKeyPrefix;
|
||||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
if (senderPrefix == null || senderPrefix.length < 6) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplySenderMissing);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.cannotReplySenderMissing,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
||||||
if (recipient == null) {
|
if (recipient == null) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,9 +190,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_isDeveloperModeEnabled = false;
|
_isDeveloperModeEnabled = false;
|
||||||
_versionTapCount = 0;
|
_versionTapCount = 0;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
context,
|
SnackBar(
|
||||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeDisabled)));
|
content: Text(AppLocalizations.of(context)!.developerModeDisabled),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,9 +206,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_isDeveloperModeEnabled = true;
|
_isDeveloperModeEnabled = true;
|
||||||
_versionTapCount = 0;
|
_versionTapCount = 0;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
context,
|
SnackBar(
|
||||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeEnabled)));
|
content: Text(AppLocalizations.of(context)!.developerModeEnabled),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +563,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.updateCheckIsOnlyAvailableOnAndroid),
|
content: Text(
|
||||||
|
AppLocalizations.of(context)!.updateCheckIsOnlyAvailableOnAndroid,
|
||||||
|
),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -584,7 +590,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (!updateInfo.isAvailable) {
|
if (!updateInfo.isAvailable) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.youAreRunningTheLatestVersion),
|
content: Text(
|
||||||
|
AppLocalizations.of(context)!.youAreRunningTheLatestVersion,
|
||||||
|
),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -594,7 +602,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (updateInfo.downloadUrl == null) {
|
if (updateInfo.downloadUrl == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.updateAvailableButDownloadUrlNotFound),
|
content: Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.updateAvailableButDownloadUrlNotFound,
|
||||||
|
),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1178,6 +1190,100 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
_buildSectionHeader(AppLocalizations.of(context)!.contacts),
|
||||||
|
Consumer<AppProvider>(
|
||||||
|
builder: (context, appProvider, child) => _buildSettingsCard([
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.star_outline_rounded),
|
||||||
|
title: Text(AppLocalizations.of(context)!.favourites),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Show the favourites section in the contacts tab',
|
||||||
|
),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.favourites,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.favourites,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.people_alt_outlined),
|
||||||
|
title: Text(AppLocalizations.of(context)!.teamMembers),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Show direct team contacts in the contacts tab',
|
||||||
|
),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.teamMembers,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.teamMembers,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.router_outlined),
|
||||||
|
title: Text(AppLocalizations.of(context)!.repeaters),
|
||||||
|
subtitle: const Text('Show repeater nodes in the contacts tab'),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.repeaters,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.repeaters,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.sensors_outlined),
|
||||||
|
title: Text(AppLocalizations.of(context)!.sensors),
|
||||||
|
subtitle: const Text('Show sensor nodes in the contacts tab'),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.sensors,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.sensors,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.meeting_room_outlined),
|
||||||
|
title: Text(AppLocalizations.of(context)!.rooms),
|
||||||
|
subtitle: const Text('Show rooms in the contacts tab'),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.rooms,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.rooms,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.broadcast_on_personal_outlined),
|
||||||
|
title: Text(AppLocalizations.of(context)!.channels),
|
||||||
|
subtitle: const Text('Show channels in the contacts tab'),
|
||||||
|
value: appProvider.isContactsSectionEnabled(
|
||||||
|
ContactsTabSection.channels,
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.setContactsSectionEnabled(
|
||||||
|
ContactsTabSection.channels,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
|
||||||
_buildSectionHeader('Messaging'),
|
_buildSectionHeader('Messaging'),
|
||||||
_buildSettingsCard([
|
_buildSettingsCard([
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -1205,7 +1311,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Consumer<AppProvider>(
|
Consumer<AppProvider>(
|
||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.route),
|
secondary: Icon(Icons.route),
|
||||||
title: Text(AppLocalizations.of(context)!.nearestRepeaterFallback),
|
title: Text(
|
||||||
|
AppLocalizations.of(context)!.nearestRepeaterFallback,
|
||||||
|
),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'After normal retries fail, try one final resend through the nearest repeater',
|
'After normal retries fail, try one final resend through the nearest repeater',
|
||||||
),
|
),
|
||||||
@@ -1234,7 +1342,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'Clear Messages',
|
'Clear Messages',
|
||||||
style: TextStyle(color: Colors.red),
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.deleteAllStoredMessageHistory),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(context)!.deleteAllStoredMessageHistory,
|
||||||
|
),
|
||||||
onTap: _clearMessages,
|
onTap: _clearMessages,
|
||||||
),
|
),
|
||||||
Consumer<AppProvider>(
|
Consumer<AppProvider>(
|
||||||
@@ -1344,7 +1454,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
builder: (context, drawingProvider, child) => SwitchListTile(
|
builder: (context, drawingProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.fmd_good_outlined),
|
secondary: Icon(Icons.fmd_good_outlined),
|
||||||
title: Text(AppLocalizations.of(context)!.showSarMarkersLabel),
|
title: Text(AppLocalizations.of(context)!.showSarMarkersLabel),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.displaySarMarkersOnTheMainMap),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(context)!.displaySarMarkersOnTheMainMap,
|
||||||
|
),
|
||||||
value: drawingProvider.showSarMarkers,
|
value: drawingProvider.showSarMarkers,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
drawingProvider.toggleSarMarkers();
|
drawingProvider.toggleSarMarkers();
|
||||||
@@ -1354,7 +1466,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Consumer<MapProvider>(
|
Consumer<MapProvider>(
|
||||||
builder: (context, mapProvider, child) => SwitchListTile(
|
builder: (context, mapProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.timeline),
|
secondary: Icon(Icons.timeline),
|
||||||
title: Text(AppLocalizations.of(context)!.showAllContactTrailsLabel),
|
title: Text(
|
||||||
|
AppLocalizations.of(context)!.showAllContactTrailsLabel,
|
||||||
|
),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Display location trails for all contacts that have history',
|
'Display location trails for all contacts that have history',
|
||||||
),
|
),
|
||||||
@@ -1420,7 +1534,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.compress),
|
secondary: Icon(Icons.compress),
|
||||||
title: Text(AppLocalizations.of(context)!.voiceCompressor),
|
title: Text(AppLocalizations.of(context)!.voiceCompressor),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.balancesQuietAndLoudSpeechLevels),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.balancesQuietAndLoudSpeechLevels,
|
||||||
|
),
|
||||||
value: appProvider.isVoiceCompressorEnabled,
|
value: appProvider.isVoiceCompressorEnabled,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await appProvider.toggleVoiceCompressorEnabled(value);
|
await appProvider.toggleVoiceCompressorEnabled(value);
|
||||||
@@ -1431,7 +1549,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.speed),
|
secondary: Icon(Icons.speed),
|
||||||
title: Text(AppLocalizations.of(context)!.voiceLimiter),
|
title: Text(AppLocalizations.of(context)!.voiceLimiter),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.preventsClippingPeaksBeforeEncoding),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.preventsClippingPeaksBeforeEncoding,
|
||||||
|
),
|
||||||
value: appProvider.isVoiceLimiterEnabled,
|
value: appProvider.isVoiceLimiterEnabled,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await appProvider.toggleVoiceLimiterEnabled(value);
|
await appProvider.toggleVoiceLimiterEnabled(value);
|
||||||
@@ -1442,7 +1564,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.auto_fix_high),
|
secondary: Icon(Icons.auto_fix_high),
|
||||||
title: Text(AppLocalizations.of(context)!.micAutoGain),
|
title: Text(AppLocalizations.of(context)!.micAutoGain),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.letsTheRecorderAdjustInputLevel),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(context)!.letsTheRecorderAdjustInputLevel,
|
||||||
|
),
|
||||||
value: appProvider.isVoiceAutoGainEnabled,
|
value: appProvider.isVoiceAutoGainEnabled,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await appProvider.toggleVoiceAutoGainEnabled(value);
|
await appProvider.toggleVoiceAutoGainEnabled(value);
|
||||||
@@ -1478,7 +1602,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Consumer<AppProvider>(
|
Consumer<AppProvider>(
|
||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: Icon(Icons.content_cut),
|
secondary: Icon(Icons.content_cut),
|
||||||
title: Text(AppLocalizations.of(context)!.trimSilenceInVoiceMessages),
|
title: Text(
|
||||||
|
AppLocalizations.of(context)!.trimSilenceInVoiceMessages,
|
||||||
|
),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Removes long silent parts before sending voice',
|
'Removes long silent parts before sending voice',
|
||||||
),
|
),
|
||||||
@@ -1648,7 +1774,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.timer),
|
leading: Icon(Icons.timer),
|
||||||
title: Text(AppLocalizations.of(context)!.activeuseUpdateInterval),
|
title: Text(
|
||||||
|
AppLocalizations.of(context)!.activeuseUpdateInterval,
|
||||||
|
),
|
||||||
subtitle: Text('$_fastLocationActiveCadenceSeconds s'),
|
subtitle: Text('$_fastLocationActiveCadenceSeconds s'),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: _editFastLocationActiveCadence,
|
onTap: _editFastLocationActiveCadence,
|
||||||
|
|||||||
24
lib/utils/contact_sorting.dart
Normal file
24
lib/utils/contact_sorting.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
int compareContactsByLastSeen(Contact a, Contact b) {
|
||||||
|
return b.lastSeenTime.compareTo(a.lastSeenTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareContactsByFavouriteThenLastSeen(Contact a, Contact b) {
|
||||||
|
if (a.isFavourite != b.isFavourite) {
|
||||||
|
return a.isFavourite ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareContactsByLastSeen(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareContactsByDisplayName(Contact a, Contact b) {
|
||||||
|
final nameCompare = a.displayName.toLowerCase().compareTo(
|
||||||
|
b.displayName.toLowerCase(),
|
||||||
|
);
|
||||||
|
if (nameCompare != 0) {
|
||||||
|
return nameCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.publicKeyHex.compareTo(b.publicKeyHex);
|
||||||
|
}
|
||||||
@@ -54,6 +54,14 @@ class ContactTile extends StatelessWidget {
|
|||||||
return l10n.daysAgo(diff.inDays);
|
return l10n.daysAgo(diff.inDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _threeBytePrefix() {
|
||||||
|
final bytes = contact.publicKey.take(3);
|
||||||
|
return bytes
|
||||||
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join()
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isChannel = contact.type == ContactType.channel;
|
final isChannel = contact.type == ContactType.channel;
|
||||||
@@ -205,46 +213,73 @@ class ContactTile extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
SizedBox(
|
||||||
clipBehavior: Clip.none,
|
width: 58,
|
||||||
children: [
|
child: Column(
|
||||||
ContactAvatar(contact: contact, radius: 24),
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (contact.isNew)
|
children: [
|
||||||
Positioned(
|
Stack(
|
||||||
top: -2,
|
clipBehavior: Clip.none,
|
||||||
right: -2,
|
children: [
|
||||||
child: Container(
|
ContactAvatar(contact: contact, radius: 24),
|
||||||
width: 14,
|
if (contact.isNew)
|
||||||
height: 14,
|
Positioned(
|
||||||
decoration: BoxDecoration(
|
top: -2,
|
||||||
color: Colors.blue,
|
right: -2,
|
||||||
shape: BoxShape.circle,
|
child: Container(
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
width: 14,
|
||||||
),
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (contact.type == ContactType.room &&
|
||||||
|
roomLoginState != null)
|
||||||
|
Positioned(
|
||||||
|
bottom: -2,
|
||||||
|
right: -2,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getRoomStatusColor(roomLoginState),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_getRoomStatusIcon(roomLoginState),
|
||||||
|
size: 11,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_threeBytePrefix(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (contact.type == ContactType.room &&
|
],
|
||||||
roomLoginState != null)
|
),
|
||||||
Positioned(
|
|
||||||
bottom: -2,
|
|
||||||
right: -2,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getRoomStatusColor(roomLoginState),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
_getRoomStatusIcon(roomLoginState),
|
|
||||||
size: 11,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
35
test/models/channel_test.dart
Normal file
35
test/models/channel_test.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/channel.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('hash channels expose deterministic psk_base64', () {
|
||||||
|
final channel = Channel.create(index: 3, name: '#ops');
|
||||||
|
|
||||||
|
expect(channel.isHashChannel, isTrue);
|
||||||
|
expect(channel.pskBase64, 'O2RN43fDLHh5NgWiWqkVvw==');
|
||||||
|
expect(
|
||||||
|
Channel.pskBase64ForHashChannelName('#ops'),
|
||||||
|
'O2RN43fDLHh5NgWiWqkVvw==',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normal channels reject derived hashtag psk export helper', () {
|
||||||
|
expect(
|
||||||
|
() => Channel.pskBase64ForHashChannelName('ops'),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normal channels still expose their stored psk_base64', () {
|
||||||
|
final channel = Channel.create(
|
||||||
|
index: 4,
|
||||||
|
name: 'ops',
|
||||||
|
explicitSecret: Uint8List.fromList(List<int>.generate(16, (i) => i)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(channel.isHashChannel, isFalse);
|
||||||
|
expect(channel.pskBase64, 'AAECAwQFBgcICQoLDA0ODw==');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/channel.dart';
|
||||||
import 'package:meshcore_sar_app/models/contact.dart';
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
import 'package:meshcore_sar_app/models/contact_group.dart';
|
import 'package:meshcore_sar_app/models/contact_group.dart';
|
||||||
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
||||||
@@ -15,8 +17,28 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
String? clipboardText;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
clipboardText = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
clipboardText =
|
||||||
|
(call.arguments as Map<dynamic, dynamic>)['text'] as String?;
|
||||||
|
return null;
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return <String, dynamic>{'text': clipboardText};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
Contact buildChannel({required String name, required int channelIndex}) {
|
Contact buildChannel({required String name, required int channelIndex}) {
|
||||||
@@ -123,6 +145,7 @@ void main() {
|
|||||||
await tester.tap(find.text('Ops'));
|
await tester.tap(find.text('Ops'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Export psk_base64'), findsNothing);
|
||||||
expect(find.text('Delete Channel'), findsOneWidget);
|
expect(find.text('Delete Channel'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Delete Channel'));
|
await tester.tap(find.text('Delete Channel'));
|
||||||
@@ -137,6 +160,26 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('hash channel activity card exports psk_base64', (tester) async {
|
||||||
|
await pumpContactsTab(
|
||||||
|
tester,
|
||||||
|
contacts: [buildChannel(name: '#ops', channelIndex: 3)],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('#ops'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('#ops'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Export psk_base64'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Export psk_base64'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(clipboardText, Channel.pskBase64ForHashChannelName('#ops'));
|
||||||
|
expect(find.text('psk_base64 copied to clipboard'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('repeaters show Others group when multiple groups exist', (
|
testWidgets('repeaters show Others group when multiple groups exist', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -218,7 +261,7 @@ void main() {
|
|||||||
contacts: [buildSensor(seed: 60, name: 'WX Station')],
|
contacts: [buildSensor(seed: 60, name: 'WX Station')],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('Sensors'), findsOneWidget);
|
expect(find.text('Sensors'), findsWidgets);
|
||||||
expect(find.text('WX Station'), findsOneWidget);
|
expect(find.text('WX Station'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ void main() {
|
|||||||
required String name,
|
required String name,
|
||||||
required ContactType type,
|
required ContactType type,
|
||||||
int secondByte = 0,
|
int secondByte = 0,
|
||||||
|
int flags = 0,
|
||||||
|
int lastAdvert = 0,
|
||||||
}) {
|
}) {
|
||||||
final publicKey = Uint8List(32);
|
final publicKey = Uint8List(32);
|
||||||
publicKey[1] = secondByte;
|
publicKey[1] = secondByte;
|
||||||
@@ -18,11 +20,11 @@ void main() {
|
|||||||
return Contact(
|
return Contact(
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
type: type,
|
type: type,
|
||||||
flags: 0,
|
flags: flags,
|
||||||
outPathLen: 0,
|
outPathLen: 0,
|
||||||
outPath: Uint8List(0),
|
outPath: Uint8List(0),
|
||||||
advName: name,
|
advName: name,
|
||||||
lastAdvert: 0,
|
lastAdvert: lastAdvert,
|
||||||
advLat: 0,
|
advLat: 0,
|
||||||
advLon: 0,
|
advLon: 0,
|
||||||
lastMod: 0,
|
lastMod: 0,
|
||||||
@@ -98,4 +100,54 @@ void main() {
|
|||||||
expect(find.text('Show all'), findsNothing);
|
expect(find.text('Show all'), findsNothing);
|
||||||
expect(find.text('John Smith'), findsOneWidget);
|
expect(find.text('John Smith'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('sorts contacts with favourites first, then last seen', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final recentNonFavourite = buildContact(
|
||||||
|
name: 'Alpha',
|
||||||
|
type: ContactType.chat,
|
||||||
|
secondByte: 1,
|
||||||
|
lastAdvert: 300,
|
||||||
|
);
|
||||||
|
final olderFavourite = buildContact(
|
||||||
|
name: 'Bravo',
|
||||||
|
type: ContactType.chat,
|
||||||
|
secondByte: 2,
|
||||||
|
flags: 0x01,
|
||||||
|
lastAdvert: 100,
|
||||||
|
);
|
||||||
|
final newerFavourite = buildContact(
|
||||||
|
name: 'Charlie',
|
||||||
|
type: ContactType.chat,
|
||||||
|
secondByte: 3,
|
||||||
|
flags: 0x01,
|
||||||
|
lastAdvert: 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: RecipientSelectorSheet(
|
||||||
|
contacts: [recentNonFavourite, olderFavourite, newerFavourite],
|
||||||
|
rooms: const [],
|
||||||
|
channels: const [],
|
||||||
|
unreadCount: 0,
|
||||||
|
unreadCountsByPublicKey: const {},
|
||||||
|
showAllOption: false,
|
||||||
|
onSelect: (selectedRecipient, draftMessage) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final charlieY = tester.getTopLeft(find.text('Charlie')).dy;
|
||||||
|
final bravoY = tester.getTopLeft(find.text('Bravo')).dy;
|
||||||
|
final alphaY = tester.getTopLeft(find.text('Alpha')).dy;
|
||||||
|
|
||||||
|
expect(charlieY, lessThan(bravoY));
|
||||||
|
expect(bravoY, lessThan(alphaY));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user