Merge branch 'main' of github.com:MeshEnvy/meshcore-open

This commit is contained in:
Ben Allfree
2026-02-24 22:50:20 -08:00
42 changed files with 1473 additions and 2223 deletions
+1
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.flutter-plugins-dependencies .flutter-plugins-dependencies
.pub-cache/ .pub-cache/
.pub/ .pub/
pubspec.lock
/build/ /build/
/coverage/ /coverage/
+68
View File
@@ -669,6 +669,7 @@ class MeshCoreConnector extends ChangeNotifier {
publicKey: contact.publicKey, publicKey: contact.publicKey,
name: contact.name, name: contact.name,
type: contact.type, type: contact.type,
flags: contact.flags,
pathLength: selection.hopCount >= 0 pathLength: selection.hopCount >= 0
? selection.hopCount ? selection.hopCount
: contact.pathLength, : contact.pathLength,
@@ -1185,11 +1186,78 @@ class MeshCoreConnector extends ChangeNotifier {
customPath, customPath,
pathLen, pathLen,
type: contact.type, type: contact.type,
flags: contact.flags,
name: contact.name, name: contact.name,
), ),
); );
} }
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
if (!isConnected) return;
final latestContact =
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
final updatedFlags = isFavorite
? (latestContact.flags | contactFlagFavorite)
: (latestContact.flags & ~contactFlagFavorite);
await sendFrame(
buildUpdateContactPathFrame(
latestContact.publicKey,
latestContact.path,
latestContact.pathLength,
type: latestContact.type,
flags: updatedFlags,
name: latestContact.name,
),
);
final index = _contacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (index >= 0) {
_contacts[index] = _contacts[index].copyWith(
type: latestContact.type,
name: latestContact.name,
pathLength: latestContact.pathLength,
path: latestContact.path,
flags: updatedFlags,
);
notifyListeners();
unawaited(_persistContacts());
}
}
Future<Contact?> _fetchContactSnapshotFromDevice(
Uint8List pubKey, {
Duration timeout = const Duration(seconds: 3),
}) async {
if (!isConnected) return null;
final expectedKeyHex = pubKeyToHex(pubKey);
final completer = Completer<Contact?>();
void finish(Contact? result) {
if (!completer.isCompleted) {
completer.complete(result);
}
}
final subscription = receivedFrames.listen((frame) {
if (frame.isEmpty || frame[0] != respCodeContact) return;
final parsed = Contact.fromFrame(frame);
if (parsed == null || parsed.publicKeyHex != expectedKeyHex) return;
finish(parsed);
});
final timer = Timer(timeout, () => finish(null));
try {
await getContactByKey(pubKey);
return await completer.future;
} finally {
timer.cancel();
await subscription.cancel();
}
}
/// Set path override for a contact (persists across contact refreshes) /// Set path override for a contact (persists across contact refreshes)
/// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path /// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path
Future<void> setPathOverride( Future<void> setPathOverride(
+1
View File
@@ -290,6 +290,7 @@ int _minPositive(int a, int b) {
const int contactPubKeyOffset = 1; const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33; const int contactTypeOffset = 33;
const int contactFlagsOffset = 34; const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
const int contactPathLenOffset = 35; const int contactPathLenOffset = 35;
const int contactPathOffset = 36; const int contactPathOffset = 36;
const int contactNameOffset = 100; const int contactNameOffset = 100;
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "bg", "@@locale": "bg",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакти", "nav_contacts": "Контакти",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
"listFilter_addToFavorites": "Добави към любими",
"listFilter_favorites": "Любими"
} }
+3
View File
@@ -1343,6 +1343,9 @@
"listFilter_az": "A-Z", "listFilter_az": "A-Z",
"listFilter_filters": "Filtere", "listFilter_filters": "Filtere",
"listFilter_all": "Alle", "listFilter_all": "Alle",
"listFilter_favorites": "Favoriten",
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
"listFilter_users": "Benutzer", "listFilter_users": "Benutzer",
"listFilter_repeaters": "Repeater", "listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver", "listFilter_roomServers": "Raumserver",
+4 -1
View File
@@ -1555,6 +1555,9 @@
"listFilter_az": "A-Z", "listFilter_az": "A-Z",
"listFilter_filters": "Filters", "listFilter_filters": "Filters",
"listFilter_all": "All", "listFilter_all": "All",
"listFilter_favorites": "Favorites",
"listFilter_addToFavorites": "Add to favorites",
"listFilter_removeFromFavorites": "Remove from favorites",
"listFilter_users": "Users", "listFilter_users": "Users",
"listFilter_repeaters": "Repeaters", "listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Room servers", "listFilter_roomServers": "Room servers",
@@ -1779,4 +1782,4 @@
"settings_gpxExportShareSubject": "meshcore-open GPX map data export", "settings_gpxExportShareSubject": "meshcore-open GPX map data export",
"snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_nearByRepeaters": "Nearby Repeaters",
"snrIndicator_lastSeen": "Last seen" "snrIndicator_lastSeen": "Last seen"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "es", "@@locale": "es",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contactos", "nav_contacts": "Contactos",
@@ -1772,5 +1778,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_favorites": "Favoritos",
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
"listFilter_addToFavorites": "Añadir a favoritos"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "fr", "@@locale": "fr",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contacts", "nav_contacts": "Contacts",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_addToFavorites": "Ajouter à mes favoris",
"listFilter_removeFromFavorites": "Supprimer des favoris",
"listFilter_favorites": "Préférences"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "it", "@@locale": "it",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contatti", "nav_contacts": "Contatti",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_addToFavorites": "Aggiungi ai preferiti",
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
"listFilter_favorites": "Preferiti"
} }
+18
View File
@@ -4772,6 +4772,24 @@ abstract class AppLocalizations {
/// **'All'** /// **'All'**
String get listFilter_all; String get listFilter_all;
/// No description provided for @listFilter_favorites.
///
/// In en, this message translates to:
/// **'Favorites'**
String get listFilter_favorites;
/// No description provided for @listFilter_addToFavorites.
///
/// In en, this message translates to:
/// **'Add to favorites'**
String get listFilter_addToFavorites;
/// No description provided for @listFilter_removeFromFavorites.
///
/// In en, this message translates to:
/// **'Remove from favorites'**
String get listFilter_removeFromFavorites;
/// No description provided for @listFilter_users. /// No description provided for @listFilter_users.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+9
View File
@@ -2728,6 +2728,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get listFilter_all => 'Всички'; String get listFilter_all => 'Всички';
@override
String get listFilter_favorites => 'Любими';
@override
String get listFilter_addToFavorites => 'Добави към любими';
@override
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
@override @override
String get listFilter_users => 'Потребители'; String get listFilter_users => 'Потребители';
+9
View File
@@ -2733,6 +2733,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get listFilter_all => 'Alle'; String get listFilter_all => 'Alle';
@override
String get listFilter_favorites => 'Favoriten';
@override
String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen';
@override
String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen';
@override @override
String get listFilter_users => 'Benutzer'; String get listFilter_users => 'Benutzer';
+9
View File
@@ -2686,6 +2686,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get listFilter_all => 'All'; String get listFilter_all => 'All';
@override
String get listFilter_favorites => 'Favorites';
@override
String get listFilter_addToFavorites => 'Add to favorites';
@override
String get listFilter_removeFromFavorites => 'Remove from favorites';
@override @override
String get listFilter_users => 'Users'; String get listFilter_users => 'Users';
+9
View File
@@ -2726,6 +2726,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get listFilter_all => 'Todas'; String get listFilter_all => 'Todas';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Añadir a favoritos';
@override
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
@override @override
String get listFilter_users => 'Usuarios'; String get listFilter_users => 'Usuarios';
+9
View File
@@ -2742,6 +2742,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get listFilter_all => 'Tout'; String get listFilter_all => 'Tout';
@override
String get listFilter_favorites => 'Préférences';
@override
String get listFilter_addToFavorites => 'Ajouter à mes favoris';
@override
String get listFilter_removeFromFavorites => 'Supprimer des favoris';
@override @override
String get listFilter_users => 'Utilisateurs'; String get listFilter_users => 'Utilisateurs';
+9
View File
@@ -2726,6 +2726,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get listFilter_all => 'Tutti'; String get listFilter_all => 'Tutti';
@override
String get listFilter_favorites => 'Preferiti';
@override
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
@override
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
@override @override
String get listFilter_users => 'Utenti'; String get listFilter_users => 'Utenti';
+9
View File
@@ -2717,6 +2717,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get listFilter_all => 'Alles'; String get listFilter_all => 'Alles';
@override
String get listFilter_favorites => 'Favorieten';
@override
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
@override
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
@override @override
String get listFilter_users => 'Gebruikers'; String get listFilter_users => 'Gebruikers';
+9
View File
@@ -2724,6 +2724,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get listFilter_all => 'Wszystko'; String get listFilter_all => 'Wszystko';
@override
String get listFilter_favorites => 'Ulubione';
@override
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
@override
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
@override @override
String get listFilter_users => 'Użytkownicy'; String get listFilter_users => 'Użytkownicy';
+9
View File
@@ -2727,6 +2727,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get listFilter_all => 'Tudo'; String get listFilter_all => 'Tudo';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
@override
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
@override @override
String get listFilter_users => 'Usuários'; String get listFilter_users => 'Usuários';
+9
View File
@@ -2730,6 +2730,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get listFilter_all => 'Все'; String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Избранное';
@override
String get listFilter_addToFavorites => 'Добавить в избранное';
@override
String get listFilter_removeFromFavorites => 'Удалить из избранного';
@override @override
String get listFilter_users => 'Пользователи'; String get listFilter_users => 'Пользователи';
+9
View File
@@ -2712,6 +2712,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get listFilter_all => 'Všetko'; String get listFilter_all => 'Všetko';
@override
String get listFilter_favorites => 'Obľúbené';
@override
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
@override
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
@override @override
String get listFilter_users => 'Používatelia'; String get listFilter_users => 'Používatelia';
+9
View File
@@ -2715,6 +2715,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get listFilter_all => 'Vse'; String get listFilter_all => 'Vse';
@override
String get listFilter_favorites => 'Priljubljene';
@override
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
@override
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
@override @override
String get listFilter_users => 'Uporabniki'; String get listFilter_users => 'Uporabniki';
+9
View File
@@ -2700,6 +2700,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get listFilter_all => 'Alla'; String get listFilter_all => 'Alla';
@override
String get listFilter_favorites => 'Favoriter';
@override
String get listFilter_addToFavorites => 'Lägg till i favoriter';
@override
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
@override @override
String get listFilter_users => 'Användare'; String get listFilter_users => 'Användare';
+9
View File
@@ -2737,6 +2737,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get listFilter_all => 'Все'; String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Улюблені';
@override
String get listFilter_addToFavorites => 'Додати до улюблених';
@override
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
@override @override
String get listFilter_users => 'Користувачі'; String get listFilter_users => 'Користувачі';
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "nl", "@@locale": "nl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contacten", "nav_contacts": "Contacten",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
"listFilter_favorites": "Favorieten",
"listFilter_addToFavorites": "Toevoegen aan favorieten"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pl", "@@locale": "pl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakty", "nav_contacts": "Kontakty",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Usuń z ulubionych",
"listFilter_addToFavorites": "Dodaj do ulubionych",
"listFilter_favorites": "Ulubione"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pt", "@@locale": "pt",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contactos", "nav_contacts": "Contactos",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_addToFavorites": "Adicionar aos favoritos",
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
"listFilter_favorites": "Favoritos"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.", "channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "ru", "@@locale": "ru",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакты", "nav_contacts": "Контакты",
@@ -984,5 +990,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_addToFavorites": "Добавить в избранное",
"listFilter_favorites": "Избранное",
"listFilter_removeFromFavorites": "Удалить из избранного"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sk", "@@locale": "sk",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakty", "nav_contacts": "Kontakty",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Odstrániť z označení",
"listFilter_addToFavorites": "Pridaj do obľúbených",
"listFilter_favorites": "Obľúbené"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sl", "@@locale": "sl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Stiki", "nav_contacts": "Stiki",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_favorites": "Priljubljene",
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
"listFilter_addToFavorites": "Dodaj v priljubljene"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sv", "@@locale": "sv",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakter", "nav_contacts": "Kontakter",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Ta bort från favoriter",
"listFilter_addToFavorites": "Lägg till i favoriter",
"listFilter_favorites": "Favoriter"
} }
+11 -2
View File
@@ -1,6 +1,12 @@
{ {
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "uk", "@@locale": "uk",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакти", "nav_contacts": "Контакти",
@@ -1744,5 +1750,8 @@
"type": "double" "type": "double"
} }
} }
} },
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
"listFilter_addToFavorites": "Додати до улюблених",
"listFilter_favorites": "Улюблені"
} }
+499 -485
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -5,6 +5,7 @@ class Contact {
final Uint8List publicKey; final Uint8List publicKey;
final String name; final String name;
final int type; final int type;
final int flags;
final int pathLength; // -1 = flood, 0+ = direct hops (from device) final int pathLength; // -1 = flood, 0+ = direct hops (from device)
final Uint8List path; // Path bytes from device final Uint8List path; // Path bytes from device
final int? final int?
@@ -19,6 +20,7 @@ class Contact {
required this.publicKey, required this.publicKey,
required this.name, required this.name,
required this.type, required this.type,
this.flags = 0,
required this.pathLength, required this.pathLength,
required this.path, required this.path,
this.pathOverride, this.pathOverride,
@@ -58,11 +60,13 @@ class Contact {
} }
bool get hasLocation => latitude != null && longitude != null; bool get hasLocation => latitude != null && longitude != null;
bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({ Contact copyWith({
Uint8List? publicKey, Uint8List? publicKey,
String? name, String? name,
int? type, int? type,
int? flags,
int? pathLength, int? pathLength,
Uint8List? path, Uint8List? path,
int? pathOverride, int? pathOverride,
@@ -77,6 +81,7 @@ class Contact {
publicKey: publicKey ?? this.publicKey, publicKey: publicKey ?? this.publicKey,
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
flags: flags ?? this.flags,
pathLength: pathLength ?? this.pathLength, pathLength: pathLength ?? this.pathLength,
path: path ?? this.path, path: path ?? this.path,
pathOverride: clearPathOverride pathOverride: clearPathOverride
@@ -167,6 +172,7 @@ class Contact {
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
); );
final type = data[contactTypeOffset]; final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8); final pathLen = data[contactPathLenOffset].toSigned(8);
final safePathLen = pathLen > 0 final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen) ? (pathLen > maxPathSize ? maxPathSize : pathLen)
@@ -191,6 +197,7 @@ class Contact {
publicKey: pubKey, publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name, name: name.isEmpty ? 'Unknown' : name,
type: type, type: type,
flags: flags,
pathLength: pathLen, pathLength: pathLen,
path: pathBytes, path: pathBytes,
latitude: lat, latitude: lat,
+40 -22
View File
@@ -970,30 +970,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = _parseGifId(value.text);
if (gifId != null) { if (gifId != null) {
return Row( return Focus(
children: [ autofocus: true,
Expanded( onKeyEvent: (node, event) {
child: ClipRRect( if (event is KeyDownEvent &&
borderRadius: BorderRadius.circular(12), (event.logicalKey == LogicalKeyboardKey.enter ||
child: GifMessage( event.logicalKey ==
url: LogicalKeyboardKey.numpadEnter)) {
'https://media.giphy.com/media/$gifId/giphy.gif', _sendMessage();
backgroundColor: Theme.of( return KeyEventResult.handled;
context, }
).colorScheme.surfaceContainerHighest, return KeyEventResult.ignored;
fallbackTextColor: Theme.of( },
context, child: Row(
).colorScheme.onSurface.withValues(alpha: 0.6), children: [
maxSize: 160, Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
maxSize: 160,
),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), IconButton(
IconButton( icon: const Icon(Icons.close),
icon: const Icon(Icons.close), onPressed: () {
onPressed: () => _textController.clear(), _textController.clear();
), _textFieldFocusNode.requestFocus();
], },
),
],
),
); );
} }
@@ -1056,6 +1073,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
connector.sendChannelMessage(widget.channel, messageText); connector.sendChannelMessage(widget.channel, messageText);
_textController.clear(); _textController.clear();
_cancelReply(); _cancelReply();
_textFieldFocusNode.requestFocus();
} }
String _formatTime(DateTime time) { String _formatTime(DateTime time) {
+37 -20
View File
@@ -354,28 +354,44 @@ class _ChatScreenState extends State<ChatScreen> {
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = _parseGifId(value.text);
if (gifId != null) { if (gifId != null) {
return Row( return Focus(
children: [ autofocus: true,
Expanded( onKeyEvent: (node, event) {
child: ClipRRect( if (event is KeyDownEvent &&
borderRadius: BorderRadius.circular(12), (event.logicalKey == LogicalKeyboardKey.enter ||
child: GifMessage( event.logicalKey ==
url: LogicalKeyboardKey.numpadEnter)) {
'https://media.giphy.com/media/$gifId/giphy.gif', _sendMessage(connector);
backgroundColor: return KeyEventResult.handled;
colorScheme.surfaceContainerHighest, }
fallbackTextColor: colorScheme.onSurface return KeyEventResult.ignored;
.withValues(alpha: 0.6), },
maxSize: 160, child: Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor:
colorScheme.surfaceContainerHighest,
fallbackTextColor: colorScheme.onSurface
.withValues(alpha: 0.6),
maxSize: 160,
),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), IconButton(
IconButton( icon: const Icon(Icons.close),
icon: const Icon(Icons.close), onPressed: () {
onPressed: () => _textController.clear(), _textController.clear();
), _textFieldFocusNode.requestFocus();
], },
),
],
),
); );
} }
@@ -443,6 +459,7 @@ class _ChatScreenState extends State<ChatScreen> {
connector.sendMessage(widget.contact, text); connector.sendMessage(widget.contact, text);
_textController.clear(); _textController.clear();
_textFieldFocusNode.requestFocus();
} }
void _showPathHistory(BuildContext context) { void _showPathHistory(BuildContext context) {
+27
View File
@@ -481,6 +481,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact: contact, contact: contact,
lastSeen: _resolveLastSeen(contact), lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount, unreadCount: unreadCount,
isFavorite: contact.isFavorite,
onTap: () => _openChat(context, contact), onTap: () => _openChat(context, contact),
onLongPress: () => onLongPress: () =>
_showContactOptions(context, connector, contact), _showContactOptions(context, connector, contact),
@@ -517,6 +518,8 @@ class _ContactsScreenState extends State<ContactsScreen>
}) })
.where((group) { .where((group) {
if (_typeFilter == ContactTypeFilter.all) return true; if (_typeFilter == ContactTypeFilter.all) return true;
// Groups don't have a favorite flag, so hide them under favorites filter
if (_typeFilter == ContactTypeFilter.favorites) return false;
for (final key in group.memberKeys) { for (final key in group.memberKeys) {
final contact = contactsByKey[key]; final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true; if (contact != null && _matchesTypeFilter(contact)) return true;
@@ -591,6 +594,8 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_typeFilter) { switch (_typeFilter) {
case ContactTypeFilter.all: case ContactTypeFilter.all:
return true; return true;
case ContactTypeFilter.favorites:
return contact.isFavorite;
case ContactTypeFilter.users: case ContactTypeFilter.users:
return contact.type == advTypeChat; return contact.type == advTypeChat;
case ContactTypeFilter.repeaters: case ContactTypeFilter.repeaters:
@@ -981,6 +986,7 @@ class _ContactsScreenState extends State<ContactsScreen>
) { ) {
final isRepeater = contact.type == advTypeRepeater; final isRepeater = contact.type == advTypeRepeater;
final isRoom = contact.type == advTypeRoom; final isRoom = contact.type == advTypeRoom;
final isFavorite = contact.isFavorite;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -1087,6 +1093,21 @@ class _ContactsScreenState extends State<ContactsScreen>
}, },
), ),
], ],
ListTile(
leading: Icon(
isFavorite ? Icons.star : Icons.star_border,
color: Colors.amber[700],
),
title: Text(
isFavorite
? context.l10n.listFilter_removeFromFavorites
: context.l10n.listFilter_addToFavorites,
),
onTap: () async {
Navigator.pop(sheetContext);
await connector.setContactFavorite(contact, !isFavorite);
},
),
ListTile( ListTile(
leading: const Icon(Icons.copy), leading: const Icon(Icons.copy),
title: Text(context.l10n.contacts_ShareContact), title: Text(context.l10n.contacts_ShareContact),
@@ -1155,6 +1176,7 @@ class _ContactTile extends StatelessWidget {
final Contact contact; final Contact contact;
final DateTime lastSeen; final DateTime lastSeen;
final int unreadCount; final int unreadCount;
final bool isFavorite;
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback onLongPress; final VoidCallback onLongPress;
@@ -1162,6 +1184,7 @@ class _ContactTile extends StatelessWidget {
required this.contact, required this.contact,
required this.lastSeen, required this.lastSeen,
required this.unreadCount, required this.unreadCount,
required this.isFavorite,
required this.onTap, required this.onTap,
required this.onLongPress, required this.onLongPress,
}); });
@@ -1214,6 +1237,10 @@ class _ContactTile extends StatelessWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (isFavorite)
Icon(Icons.star, size: 14, color: Colors.amber[700]),
if (isFavorite && contact.hasLocation)
const SizedBox(width: 2),
if (contact.hasLocation) if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]), Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
], ],
+1
View File
@@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
_commandController.clear(); _commandController.clear();
_historyIndex = -1; _historyIndex = -1;
_commandFocusNode.requestFocus();
// Auto-scroll to bottom // Auto-scroll to bottom
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
+2
View File
@@ -33,6 +33,7 @@ class ContactStore {
'publicKey': base64Encode(contact.publicKey), 'publicKey': base64Encode(contact.publicKey),
'name': contact.name, 'name': contact.name,
'type': contact.type, 'type': contact.type,
'flags': contact.flags,
'pathLength': contact.pathLength, 'pathLength': contact.pathLength,
'path': base64Encode(contact.path), 'path': base64Encode(contact.path),
'pathOverride': contact.pathOverride, 'pathOverride': contact.pathOverride,
@@ -53,6 +54,7 @@ class ContactStore {
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
name: json['name'] as String? ?? 'Unknown', name: json['name'] as String? ?? 'Unknown',
type: json['type'] as int? ?? 0, type: json['type'] as int? ?? 0,
flags: json['flags'] as int? ?? 0,
pathLength: json['pathLength'] as int? ?? -1, pathLength: json['pathLength'] as int? ?? -1,
path: json['path'] != null path: json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String)) ? Uint8List.fromList(base64Decode(json['path'] as String))
+15 -6
View File
@@ -3,7 +3,7 @@ import '../l10n/l10n.dart';
enum ContactSortOption { lastSeen, recentMessages, name } enum ContactSortOption { lastSeen, recentMessages, name }
enum ContactTypeFilter { all, users, repeaters, rooms } enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
class SortFilterMenuOption { class SortFilterMenuOption {
final int value; final int value;
@@ -94,11 +94,12 @@ const int _actionSortRecentMessages = 1;
const int _actionSortName = 2; const int _actionSortName = 2;
const int _actionSortLastSeen = 3; const int _actionSortLastSeen = 3;
const int _actionFilterAll = 4; const int _actionFilterAll = 4;
const int _actionFilterUsers = 5; const int _actionFilterFavorites = 5;
const int _actionFilterRepeaters = 6; const int _actionFilterUsers = 6;
const int _actionFilterRooms = 7; const int _actionFilterRepeaters = 7;
const int _actionToggleUnreadOnly = 8; const int _actionFilterRooms = 8;
const int _actionNewGroup = 9; const int _actionToggleUnreadOnly = 9;
const int _actionNewGroup = 10;
class ContactsFilterMenu extends StatelessWidget { class ContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption; final ContactSortOption sortOption;
@@ -154,6 +155,11 @@ class ContactsFilterMenu extends StatelessWidget {
label: l10n.listFilter_all, label: l10n.listFilter_all,
checked: typeFilter == ContactTypeFilter.all, checked: typeFilter == ContactTypeFilter.all,
), ),
SortFilterMenuOption(
value: _actionFilterFavorites,
label: l10n.listFilter_favorites,
checked: typeFilter == ContactTypeFilter.favorites,
),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterUsers, value: _actionFilterUsers,
label: l10n.listFilter_users, label: l10n.listFilter_users,
@@ -198,6 +204,9 @@ class ContactsFilterMenu extends StatelessWidget {
case _actionFilterUsers: case _actionFilterUsers:
onTypeFilterChanged(ContactTypeFilter.users); onTypeFilterChanged(ContactTypeFilter.users);
break; break;
case _actionFilterFavorites:
onTypeFilterChanged(ContactTypeFilter.favorites);
break;
case _actionFilterRepeaters: case _actionFilterRepeaters:
onTypeFilterChanged(ContactTypeFilter.repeaters); onTypeFilterChanged(ContactTypeFilter.repeaters);
break; break;
-1143
View File
File diff suppressed because it is too large Load Diff