Compare commits

..

47 Commits

Author SHA1 Message Date
Winston Lowe ba4fd3eff5 Merge branch 'dev-offline' of github.com:zjs81/meshcore-open into dev-offline 2026-05-27 10:31:23 -07:00
Winston Lowe beb3e1996d fix: Improve error handling when restoring last companion public key and update disconnection message in Dutch localization 2026-05-27 10:26:19 -07:00
Winston Lowe 3f4e6f4e13 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:49:25 -07:00
Winston Lowe e1cc285c8a fix: Correct channel addition logic to prevent duplicates based on pskHex 2026-05-12 18:31:55 -07:00
Winston Lowe f4b32e8a8a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:30:39 -07:00
Winston Lowe 01bf95c98e fix: Implement disconnect method in _FakeMeshCoreConnector for TCP and USB flow tests 2026-05-12 18:21:18 -07:00
Winston Lowe fa044dd204 feat: Add dialog messages for companion connection and disconnection
- Introduced new dialog messages for connecting to a companion and handling disconnection across multiple languages.
- Updated localization files for French, Hungarian, Italian, Japanese, Korean, Bulgarian, German, English, Spanish, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Modified the contacts and map screens to utilize the new dialog messages.
- Enhanced the disconnect confirmation dialog to show a message upon successful disconnection.
- Updated app bar to conditionally display radio stats based on companion connection status.
2026-05-12 18:01:31 -07:00
Winston Lowe 72fea3fc32 fix: Enhance channel deduplication logic in loadChannels method 2026-05-12 18:01:31 -07:00
Winston Lowe f0bd61144c fix: Remove unnecessary whitespace in keyedMessageIds initialization 2026-05-12 18:01:31 -07:00
Winston Lowe 61c897630c fix: Improve message key handling and enhance contact tile key uniqueness 2026-05-12 18:01:31 -07:00
Winston Lowe a270e2e6d1 fix: Add byteCount initialization in PathRecord.fromJson method 2026-05-12 18:01:31 -07:00
Winston Lowe 247db6a36d fix: Initialize _selfPublicKey with lastCompanionPublicKeyHex in MeshCoreConnector 2026-05-12 18:01:31 -07:00
Winston Lowe 78d08afb47 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:01:31 -07:00
Winston Lowe c77264cc81 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:01:31 -07:00
Winston Lowe d6ed8c5f13 feat: Enhance companion connection features and UI updates
- Added functionality to load and restore the last companion's scope on app startup.
- Implemented caching mechanisms for contacts, channels, and messages related to the current companion.
- Updated UI to reflect connection status, including disabling message input when disconnected.
- Introduced new dialog prompts to inform users when they need to connect to a companion for accessing features.
- Refactored navigation logic to improve user experience when disconnected, directing users to the scanner screen.
- Added localization strings for new companion connection prompts in multiple languages.
2026-05-12 18:01:31 -07:00
Winston Lowe 209fee48ca Add byteCount field to PathRecord class with default value 2026-05-12 17:59:58 -07:00
zjs81 8892823337 Merge pull request #444 from sethoscope/fix-channel-chat-icon
Use correct channel icons in channel chat screen
2026-05-12 09:55:30 -07:00
zjs81 e738664f89 Merge pull request #448 from zjs81/main
Main
2026-05-12 09:54:39 -07:00
zjs81 e37616fa15 Merge pull request #145 from pioneer/unread-peoplefirst
Unread badges for tabs
2026-05-12 09:53:33 -07:00
Seth Golub 2763d83fe4 Use correct channel icons in channel chat screen
At the top of the channel chat screen is an icon, indicating the
channel type.

Previously, the public icon was used correctly, but the
hashtag icon was used for all other types.

Now, consistent with the channels screen, we use the lock icon for
private channels, and the composite icons for community public &
community hashtag types.

The fix for private channels was trivial, as we can identify hashtag
channels by their name. Finding out whether a channel belongs to a
community is much more involved. All the hard-working code was copied
from channels_screen.dart. (I tried refactoring to reduce duplication,
but my results were complex and not worth it.)

Closes #432
2026-05-11 21:13:50 -07:00
Serge Tarkovski 77018dc358 Recompute channels unread total after cachedChannels is updated 2026-05-12 00:47:26 +03:00
zjs81 21c58d4e13 Merge pull request #443 from Maxb0tbeep/dev
Add "NRF52" as a device name prefix
2026-05-10 21:05:59 -07:00
Max Cooley 3af97ff6dd Accidentally wrote quotes instead of backticks...oops 2026-05-10 17:19:57 -07:00
Max Cooley 703d5a1ec4 Add "NRF52" as a device name prefix 2026-05-10 17:12:48 -07:00
zjs81 e801a497f8 Merge pull request #435 from zjs81/dev
merge dev into main
2026-05-09 19:30:34 -07:00
Serge Tarkovski d3c7d8e43a Red dot unread indicator in bottom tabs, keep numeric unreads only for the lists; fixed unread indicator wasn't on all screens 2026-04-22 01:57:12 +03:00
Serge Tarkovski 0c1e163b88 Reverted Ukrainian translations, will be in a separate PR 2026-04-21 23:50:08 +03:00
Serge Tarkovski d0d6a34fb5 Restore jni to whatever is in main 2026-04-21 23:44:14 +03:00
Serge Tarkovski 16ce1359d7 Remove unused 'Users first' translation key 2026-04-21 23:10:39 +03:00
Serge Tarkovski 9fe4a3710d Add missing users-first translations for hu/ja/ko and regen outputs 2026-04-21 17:01:51 +03:00
Serge Tarkovski 8611adab1f Run dart format and verify analyze 2026-04-21 16:51:10 +03:00
Serge Tarkovski 7d457cb863 Merge main into unread-peoplefirst
Resolved conflicts by accepting refactored state management from main:
- list_filter_widget.dart: Adopt sealed class pattern for filter actions
- contacts_screen.dart: Move state to UiViewStateService instead of local setState
- device_screen.dart: Accept deletion (consolidated into other screens in main)

Main branch includes significant improvements:
- TCP and USB transport support
- Service-based state management with UiViewStateService
- Translation support with message translation buttons
- Signal UI consistency improvements
- Additional language support (hu, ja, ko)
- Comprehensive test coverage
- Discovery screen refactoring
2026-04-21 16:45:43 +03:00
Serge Tarkovski 297516fc80 Update cached unread total when removing contact unread entries
When contacts are removed in removeContact, _handleContact, or _handleContactAdvert,
subtract their unread count from _cachedContactsUnreadTotal immediately so badge
counts reflect the true total without waiting for a full reload.
2026-04-21 16:34:22 +03:00
Serge Tarkovski 1b94442ab6 Fix action constant collision: change _actionTogglePrioritizeUsers from 10 to 11 2026-02-27 21:19:13 +02:00
Serge Tarkovski 3ae14781f0 AI translations for "Users first" 2026-02-27 12:58:32 +02:00
Serge Tarkovski ecc496f2af Merge branch 'main' into unread-peoplefirst 2026-02-27 12:57:59 +02:00
Serge Tarkovski 87b25655d0 Package updates from main 2026-02-27 12:43:21 +02:00
Serge Tarkovski c47a4cb622 fix: filter by _shouldTrackUnreadForContactKey when recalculating cached contacts unread total 2026-02-27 12:28:57 +02:00
Serge Tarkovski a30fc439f3 refactor: use UnreadBadge widget in QuickSwitchBar for consistent badge styling 2026-02-27 12:22:26 +02:00
Serge Tarkovski afcc4db405 fix: clamp cached unread totals to prevent negative badge counts
Clamp both _cachedContactsUnreadTotal and _cachedChannelsUnreadTotal
to >= 0 after decrementing in markContactRead() and markChannelRead().
This prevents the totals from going negative if the cache drifts
out-of-sync, which could cause UI badges to display incorrect values.
2026-02-18 20:37:34 +02:00
Serge Tarkovski 87bcb6a6a3 Proper formatting 2026-02-09 17:40:56 +02:00
Serge Tarkovski 68bb031bb6 "Users first" instead of "People first" everywhere 2026-02-09 17:34:18 +02:00
Serge Tarkovski c4f5c7b171 Cache for unread total 2026-02-09 17:18:21 +02:00
Serge Tarkovski 2bce14224d Update generated plugin registrants after merge 2026-02-09 16:27:53 +02:00
Serge Tarkovski fd305fd55b Update generated plugin registrants after merge 2026-02-09 13:19:31 +02:00
Serge Tarkovski d0dd805244 Merge branch 'main' into unread-peoplefirst 2026-02-09 13:16:05 +02:00
Serge Tarkovski 8668564464 Correct unread badges for tabs; people first contacts sort option 2026-02-09 12:56:38 +02:00
56 changed files with 1137 additions and 1164 deletions
+1
View File
@@ -193,6 +193,7 @@ Devices are discovered by scanning for BLE advertisements with known MeshCore de
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
- `NRF52`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
+65 -9
View File
@@ -305,6 +305,8 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<String, int> _contactUnreadCount = {};
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
bool _unreadStateLoaded = false;
int _cachedContactsUnreadTotal = 0;
int _cachedChannelsUnreadTotal = 0;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
int? _activeChannelIndex;
@@ -612,16 +614,42 @@ class MeshCoreConnector extends ChangeNotifier {
int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0;
var total = 0;
// Count unread contact messages
for (final contact in _contacts) {
total += getUnreadCountForContact(contact);
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
}
// Count unread channel messages
for (final channelIndex in _channelMessages.keys) {
total += getUnreadCountForChannelIndex(channelIndex);
int getTotalContactsUnreadCount() {
if (!_unreadStateLoaded) return 0;
return _cachedContactsUnreadTotal;
}
return total;
int getTotalChannelsUnreadCount() {
if (!_unreadStateLoaded) return 0;
return _cachedChannelsUnreadTotal;
}
/// Recalculates both cached unread totals from scratch.
/// Called when unread state is first loaded.
void _recalculateCachedUnreadTotals() {
_recalculateCachedContactsUnreadTotal();
_recalculateCachedChannelsUnreadTotal();
}
void _recalculateCachedContactsUnreadTotal() {
int total = 0;
_contactUnreadCount.forEach((contactKeyHex, count) {
if (_shouldTrackUnreadForContactKey(contactKeyHex)) {
total += count;
}
});
_cachedContactsUnreadTotal = total;
}
void _recalculateCachedChannelsUnreadTotal() {
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
_cachedChannelsUnreadTotal = allChannels.fold(
0,
(total, ch) => total + ch.unreadCount,
);
}
bool isChannelSmazEnabled(int channelIndex) {
@@ -655,6 +683,7 @@ class MeshCoreConnector extends ChangeNotifier {
..clear()
..addAll(await _unreadStore.loadContactUnreadCount());
_unreadStateLoaded = true;
_recalculateCachedUnreadTotals();
notifyListeners();
}
@@ -693,6 +722,8 @@ class MeshCoreConnector extends ChangeNotifier {
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
if (previousCount > 0) {
_contactUnreadCount[contactKeyHex] = 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
.clamp(0, _cachedContactsUnreadTotal);
_appDebugLogService?.info(
'Contact $contactKeyHex marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -734,6 +765,8 @@ class MeshCoreConnector extends ChangeNotifier {
if (channel != null && channel.unreadCount > 0) {
final previousCount = channel.unreadCount;
channel.unreadCount = 0;
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
.clamp(0, _cachedChannelsUnreadTotal);
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -940,11 +973,18 @@ class MeshCoreConnector extends ChangeNotifier {
final lastCompanionPublicKeyHex = prefs.getString(
_lastCompanionPublicKeyPref,
);
try {
if (lastCompanionPublicKeyHex == null ||
lastCompanionPublicKeyHex.trim().isEmpty) {
return;
}
_selfPublicKey = hexToPubKey(lastCompanionPublicKeyHex);
_setScopedStorePublicKey(lastCompanionPublicKeyHex);
} catch (e) {
_appDebugLogService?.error(
'Failed to restore last companion scope with public key hex: $lastCompanionPublicKeyHex, error: $e',
);
}
}
Future<void> loadDiscoveredContactCache() => _loadDiscoveredContactCache();
@@ -966,7 +1006,9 @@ class MeshCoreConnector extends ChangeNotifier {
_conversations.clear();
_loadedConversationKeys.clear();
_channelMessages.clear();
_channels.clear();
_cachedChannels.clear();
_previousChannelsCache.clear();
_knownContactKeys.clear();
_contactUnreadCount.clear();
_unreadStateLoaded = false;
@@ -987,6 +1029,7 @@ class MeshCoreConnector extends ChangeNotifier {
await restoreLastCompanionScope();
await loadContactCache();
await _loadDiscoveredContactCache();
notifyListeners();
}
Future<void> _loadDiscoveredContactCache() async {
@@ -3203,6 +3246,9 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex);
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -3596,6 +3642,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Cache channels for offline use
_cachedChannels = List<Channel>.from(_channels);
unawaited(_channelStore.saveChannels(_channels));
_recalculateCachedChannelsUnreadTotal();
// Apply ordering and notify UI
_applyChannelOrder();
@@ -4134,6 +4181,9 @@ class MeshCoreConnector extends ChangeNotifier {
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -4224,6 +4274,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -5104,7 +5157,8 @@ class MeshCoreConnector extends ChangeNotifier {
_channelSyncRetries = 0; // Reset retry counter on success
// Only add non-empty channels
if (!channel.isEmpty) {
if (!channel.isEmpty &&
!_channels.any((c) => c.pskHex == channel.pskHex)) {
_channels.add(channel);
}
@@ -5236,6 +5290,7 @@ class MeshCoreConnector extends ChangeNotifier {
final channel = _findChannelByIndex(channelIndex);
if (channel != null) {
channel.unreadCount++;
_cachedChannelsUnreadTotal++;
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
tag: 'Unread',
@@ -5280,6 +5335,7 @@ class MeshCoreConnector extends ChangeNotifier {
final currentCount = _contactUnreadCount[contactKey] ?? 0;
_contactUnreadCount[contactKey] = currentCount + 1;
_cachedContactsUnreadTotal++;
_appDebugLogService?.info(
'Contact $contactKey unread count incremented to ${currentCount + 1}',
tag: 'Unread',
+1
View File
@@ -11,5 +11,6 @@ class MeshCoreUuids {
"Lilygo",
"HT-",
"LowMesh_MC_",
"NRF52",
];
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_newMessages": "Нови съобщения",
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
"repeater_chanUtil": "Използване на канала",
"contact_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи."
"dialog_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.",
"dialog_disconnectedTitle": "Прекъснато",
"dialog_disconnectedMessage": "Свързването ви с вашия спътник е прекъснато.",
"contact_connectCompanion": "Свържете се с спътник, за да получите достъп до функциите на repeater и room server."
}
+4 -1
View File
@@ -2340,5 +2340,8 @@
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals",
"contact_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen."
"dialog_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.",
"dialog_disconnectedTitle": "Getrennt",
"dialog_disconnectedMessage": "Du wurdest von deinem Begleiter getrennt.",
"contact_connectCompanion": "Mit einem Companion verbinden, um auf Repeater- und Raumserver-Funktionen zuzugreifen."
}
+4 -2
View File
@@ -1062,6 +1062,9 @@
"time_allTime": "All Time",
"dialog_disconnect": "Disconnect",
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"dialog_disconnectedTitle": "Disconnected",
"dialog_disconnectedMessage": "You have been disconnected from your companion.",
"dialog_connectCompanion": "Connect to a companion to access repeater and room server features.",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
@@ -2366,6 +2369,5 @@
"contact_typeRepeater": "Repeater",
"contact_typeRoom": "Room",
"contact_typeSensor": "Sensor",
"contact_typeUnknown": "Unknown",
"contact_connectCompanion": "Connect to a companion to access repeater and room server features."
"contact_typeUnknown": "Unknown"
}
+4 -1
View File
@@ -2340,5 +2340,8 @@
"settings_companionDebugLogSubtitle": "Comandos, respuestas y datos brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como no leído",
"repeater_chanUtil": "Utilización del canal",
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala."
"dialog_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Te has desconectado de tu compañero.",
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones del repetidor y del servidor de la sala."
}
+4 -1
View File
@@ -2319,5 +2319,8 @@
"chat_newMessages": "Nouveaux messages",
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
"repeater_chanUtil": "Utilisation du canal",
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle."
"dialog_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.",
"dialog_disconnectedTitle": "Déconnecté",
"dialog_disconnectedMessage": "Vous avez été déconnecté de votre compagnon.",
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités du répéteur et du serveur de salle."
}
+4 -1
View File
@@ -2350,5 +2350,8 @@
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
"repeater_chanUtil": "Csatorna-használat",
"contact_connectCompanion": "Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez."
"dialog_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez.",
"dialog_disconnectedTitle": "Kapcsolat megszakadt",
"dialog_disconnectedMessage": "A kapcsolat megszakadt a kísérővel.",
"contact_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez."
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_newMessages": "Nuovi messaggi",
"chat_markAsUnread": "Segna come non letto",
"repeater_chanUtil": "Utilizzo del canale",
"contact_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza."
"dialog_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.",
"dialog_disconnectedTitle": "Disconnesso",
"dialog_disconnectedMessage": "Sei stato disconnesso dal tuo compagno.",
"contact_connectCompanion": "Connettiti a un companion per accedere alle funzioni del repeater e del server di stanza."
}
+4 -1
View File
@@ -2350,5 +2350,8 @@
"chat_newMessages": "新しいメッセージ",
"chat_markAsUnread": "未読としてマークする",
"repeater_chanUtil": "チャンネルの利用状況",
"contact_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス"
"dialog_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス",
"dialog_disconnectedTitle": "切断済み",
"dialog_disconnectedMessage": "コンパニオンとの接続が切れました。",
"contact_connectCompanion": "リピーターおよびルームサーバー機能にアクセスするには、コンパニオンに接続してください。"
}
+3
View File
@@ -2350,5 +2350,8 @@
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
"chat_markAsUnread": "미리 읽지 않음으로 표시",
"repeater_chanUtil": "채널 활용도",
"dialog_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.",
"dialog_disconnectedTitle": "연결 끊김",
"dialog_disconnectedMessage": "컴패니언과의 연결이 끊어졌습니다.",
"contact_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요."
}
+18 -6
View File
@@ -3561,6 +3561,24 @@ abstract class AppLocalizations {
/// **'Are you sure you want to disconnect from this device?'**
String get dialog_disconnectConfirm;
/// No description provided for @dialog_disconnectedTitle.
///
/// In en, this message translates to:
/// **'Disconnected'**
String get dialog_disconnectedTitle;
/// No description provided for @dialog_disconnectedMessage.
///
/// In en, this message translates to:
/// **'You have been disconnected from your companion.'**
String get dialog_disconnectedMessage;
/// No description provided for @dialog_connectCompanion.
///
/// In en, this message translates to:
/// **'Connect to a companion to access repeater and room server features.'**
String get dialog_connectCompanion;
/// No description provided for @login_repeaterLogin.
///
/// In en, this message translates to:
@@ -7335,12 +7353,6 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Unknown'**
String get contact_typeUnknown;
/// No description provided for @contact_connectCompanion.
///
/// In en, this message translates to:
/// **'Connect to a companion to access repeater and room server features.'**
String get contact_connectCompanion;
}
class _AppLocalizationsDelegate
+11 -4
View File
@@ -1979,6 +1979,17 @@ class AppLocalizationsBg extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Сигурни ли сте, че искате да се откъснете от това устройство?';
@override
String get dialog_disconnectedTitle => 'Прекъснато';
@override
String get dialog_disconnectedMessage =>
'Свързването ви с вашия спътник е прекъснато.';
@override
String get dialog_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
@override
String get login_repeaterLogin => 'Повторител Вход';
@@ -4288,8 +4299,4 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
}
+11 -4
View File
@@ -1977,6 +1977,17 @@ class AppLocalizationsDe extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?';
@override
String get dialog_disconnectedTitle => 'Getrennt';
@override
String get dialog_disconnectedMessage =>
'Du wurdest von deinem Begleiter getrennt.';
@override
String get dialog_connectCompanion =>
'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.';
@override
String get login_repeaterLogin => 'Beim Repeater anmelden';
@@ -4305,8 +4316,4 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.';
}
+11 -4
View File
@@ -1938,6 +1938,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Are you sure you want to disconnect from this device?';
@override
String get dialog_disconnectedTitle => 'Disconnected';
@override
String get dialog_disconnectedMessage =>
'You have been disconnected from your companion.';
@override
String get dialog_connectCompanion =>
'Connect to a companion to access repeater and room server features.';
@override
String get login_repeaterLogin => 'Repeater Login';
@@ -4210,8 +4221,4 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connect to a companion to access repeater and room server features.';
}
+11 -4
View File
@@ -1974,6 +1974,17 @@ class AppLocalizationsEs extends AppLocalizations {
String get dialog_disconnectConfirm =>
'¿Está seguro de que desea desconectarse de este dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Te has desconectado de tu compañero.';
@override
String get dialog_connectCompanion =>
'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.';
@override
String get login_repeaterLogin => 'Iniciar sesión en el Repetidor';
@@ -4292,8 +4303,4 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.';
}
+11 -4
View File
@@ -1985,6 +1985,17 @@ class AppLocalizationsFr extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
@override
String get dialog_disconnectedTitle => 'Déconnecté';
@override
String get dialog_disconnectedMessage =>
'Vous avez été déconnecté de votre compagnon.';
@override
String get dialog_connectCompanion =>
'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.';
@override
String get login_repeaterLogin => 'Connexion au répéteur';
@@ -4321,8 +4332,4 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.';
}
+10 -4
View File
@@ -1986,6 +1986,16 @@ class AppLocalizationsHu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Biztosan szeretné kiírni ezt a készüléket?';
@override
String get dialog_disconnectedTitle => 'Lejárat';
@override
String get dialog_disconnectedMessage => 'Lehentetőtől megszakadtál.';
@override
String get dialog_connectCompanion =>
'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.';
@override
String get login_repeaterLogin => 'Ismételt bejelentkezés';
@@ -4309,8 +4319,4 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.';
}
+11 -4
View File
@@ -1976,6 +1976,17 @@ class AppLocalizationsIt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sei sicuro di voler disconnetterti da questo dispositivo?';
@override
String get dialog_disconnectedTitle => 'Disconnesso';
@override
String get dialog_disconnectedMessage =>
'Sei stato disconnesso dal tuo compagno.';
@override
String get dialog_connectCompanion =>
'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.';
@override
String get login_repeaterLogin => 'Login Ripetitore';
@@ -4297,8 +4308,4 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.';
}
+9 -3
View File
@@ -1894,6 +1894,15 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?';
@override
String get dialog_disconnectedTitle => '切断済み';
@override
String get dialog_disconnectedMessage => 'コンパニオンとの接続が切れました。';
@override
String get dialog_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
@override
String get login_repeaterLogin => '再ログイン';
@@ -4063,7 +4072,4 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
}
+9 -3
View File
@@ -1890,6 +1890,15 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '이 장치와의 연결을 해제하시겠습니까?';
@override
String get dialog_disconnectedTitle => '연결 끊김';
@override
String get dialog_disconnectedMessage => '컴패니언과의 연결이 끊어졌습니다.';
@override
String get dialog_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
@override
String get login_repeaterLogin => '다시 로그인';
@@ -4064,7 +4073,4 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
}
+11 -4
View File
@@ -1963,6 +1963,17 @@ class AppLocalizationsNl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?';
@override
String get dialog_disconnectedTitle => 'Verbroken';
@override
String get dialog_disconnectedMessage =>
'Je bent losgekoppeld van je companion.';
@override
String get dialog_connectCompanion =>
'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.';
@override
String get login_repeaterLogin => 'Inloggen Repeater';
@@ -4273,8 +4284,4 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.';
}
+11 -4
View File
@@ -1990,6 +1990,17 @@ class AppLocalizationsPl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Czy na pewno chcesz się odłączyć od tego urządzenia?';
@override
String get dialog_disconnectedTitle => 'Rozłączono';
@override
String get dialog_disconnectedMessage =>
'Zostałeś rozłączony ze swoim towarzyszem.';
@override
String get dialog_connectCompanion =>
'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.';
@override
String get login_repeaterLogin => 'Logowanie do przekaźnika';
@@ -4309,8 +4320,4 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.';
}
+11 -4
View File
@@ -1973,6 +1973,17 @@ class AppLocalizationsPt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Tem certeza de que deseja desconectar deste dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Você foi desconectado do seu companheiro.';
@override
String get dialog_connectCompanion =>
'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.';
@override
String get login_repeaterLogin => 'Login ao Repetidor';
@@ -4285,8 +4296,4 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.';
}
+11 -4
View File
@@ -1977,6 +1977,17 @@ class AppLocalizationsRu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Вы уверены, что хотите отключиться от этого устройства?';
@override
String get dialog_disconnectedTitle => 'Отключено';
@override
String get dialog_disconnectedMessage =>
'Вы были отключены от вашего компаньона.';
@override
String get dialog_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
@override
String get login_repeaterLogin => 'Вход в репитер';
@@ -4303,8 +4314,4 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Неизвестно';
@override
String get contact_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
}
+11 -4
View File
@@ -1963,6 +1963,17 @@ class AppLocalizationsSk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste si istý/á, že chcete odpojiť od tohto zariadenia?';
@override
String get dialog_disconnectedTitle => 'Odpojené';
@override
String get dialog_disconnectedMessage =>
'Od vášho spoločníka ste boli odpojený.';
@override
String get dialog_connectCompanion =>
'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.';
@override
String get login_repeaterLogin => 'Opätovné prihlásenie';
@@ -4269,8 +4280,4 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.';
}
+11 -4
View File
@@ -1961,6 +1961,17 @@ class AppLocalizationsSl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste prepričani, da želite se odklopiti s tega naprave?';
@override
String get dialog_disconnectedTitle => 'Prekinjeno';
@override
String get dialog_disconnectedMessage =>
'Prekinjena povezava s vašim spre伴ovalcem.';
@override
String get dialog_connectCompanion =>
'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.';
@override
String get login_repeaterLogin => 'Ponovni vnos';
@@ -4267,8 +4278,4 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.';
}
+11 -4
View File
@@ -1950,6 +1950,17 @@ class AppLocalizationsSv extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Är du säker på att du vill koppla från enheten?';
@override
String get dialog_disconnectedTitle => 'Ansluten ej';
@override
String get dialog_disconnectedMessage =>
'Du har kopplats från din companion.';
@override
String get dialog_connectCompanion =>
'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.';
@override
String get login_repeaterLogin => 'Återuppta Inloggning';
@@ -4241,8 +4252,4 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.';
}
+11 -4
View File
@@ -1972,6 +1972,17 @@ class AppLocalizationsUk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ви впевнені, що хочете відключитись від цього пристрою?';
@override
String get dialog_disconnectedTitle => 'Від’єднано';
@override
String get dialog_disconnectedMessage =>
'Вас від’єднано від вашого супутника.';
@override
String get dialog_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
@override
String get login_repeaterLogin => 'Вхід у ретранслятор';
@@ -4304,8 +4315,4 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Невідомо';
@override
String get contact_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
}
+9 -3
View File
@@ -1861,6 +1861,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '确定要断开与此设备的连接吗?';
@override
String get dialog_disconnectedTitle => '已断开连接';
@override
String get dialog_disconnectedMessage => '你已与你的伙伴断开连接。';
@override
String get dialog_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
@override
String get login_repeaterLogin => '转发节点登录';
@@ -3938,7 +3947,4 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal",
"contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken."
"dialog_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.",
"dialog_disconnectedTitle": "Verbroken",
"dialog_disconnectedMessage": "Je bent losgekoppeld van je companion.",
"contact_connectCompanion": "Maak verbinding met een companion om toegang te krijgen tot repeater- en roomserverfuncties."
}
+4 -1
View File
@@ -2350,5 +2350,8 @@
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi."
"dialog_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.",
"dialog_disconnectedTitle": "Rozłączono",
"dialog_disconnectedMessage": "Zostałeś rozłączony ze swoim towarzyszem.",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji repeatera i serwera pokojowego."
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal",
"contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas."
"dialog_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Você foi desconectado do seu companheiro.",
"contact_connectCompanion": "Conecte-se a um companheiro para acessar recursos de repetidor e servidor de sala."
}
+4 -1
View File
@@ -1615,5 +1615,8 @@
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат."
"dialog_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.",
"dialog_disconnectedTitle": "Отключено",
"dialog_disconnectedMessage": "Вы были отключены от вашего компаньона.",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям репитера и серверу комнаты."
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu",
"contact_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností."
"dialog_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.",
"dialog_disconnectedTitle": "Odpojené",
"dialog_disconnectedMessage": "Od vášho spoločníka ste boli odpojený.",
"contact_connectCompanion": "Pripojte sa k spoločníkovi pre prístup k funkciám opakovača a miestneho servera."
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala",
"contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob."
"dialog_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.",
"dialog_disconnectedTitle": "Prekinjeno",
"dialog_disconnectedMessage": "Prekinjena povezava s vašim spre伴ovalcem.",
"contact_connectCompanion": "Povežite se s ponсоbnikom za dostop do funkcij pon 반복nika in strežnika prostorov."
}
+4 -1
View File
@@ -2312,5 +2312,8 @@
"chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal",
"contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner."
"dialog_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.",
"dialog_disconnectedTitle": "Ansluten ej",
"dialog_disconnectedMessage": "Du har kopplats från din companion.",
"contact_connectCompanion": "Anslut till en companion för att få tillgång till repeater- och rumsserverfunktioner."
}
+4 -1
View File
@@ -2292,5 +2292,8 @@
"chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу",
"contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат."
"dialog_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.",
"dialog_disconnectedTitle": "Від’єднано",
"dialog_disconnectedMessage": "Вас від’єднано від вашого супутника.",
"contact_connectCompanion": "Підключіться до супутника, щоб отримати доступ до функцій репітера та серверів кімнат."
}
+4 -1
View File
@@ -2317,5 +2317,8 @@
"chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率",
"contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。"
"dialog_connectCompanion": "连接伴机以访问中继器和房间服务器功能。",
"dialog_disconnectedTitle": "已断开连接",
"dialog_disconnectedMessage": "你已与你的伙伴断开连接。",
"contact_connectCompanion": "连接至伴侣设备以访问中继器和房间服务器功能。"
}
+34
View File
@@ -4,6 +4,9 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
import 'community.dart';
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
class Channel {
final int index;
@@ -111,5 +114,36 @@ class Channel {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
static bool isCommunityChannel(ChannelType channelType) {
switch (channelType) {
case ChannelType.communityPublic:
case ChannelType.communityHashtag:
return true;
case ChannelType.public:
case ChannelType.private:
case ChannelType.hashtag:
return false;
}
}
static ChannelType getChannelType(
Channel channel,
CommunityPskIndex communityIndex,
) {
Community? community = communityIndex.getCommunityForChannel(channel);
if (community != null) {
if (Community.isCommunityPublicChannel(channel, community)) {
return ChannelType.communityPublic;
}
return ChannelType.communityHashtag;
}
if (channel.isPublicChannel) {
return ChannelType.public;
} else if (channel.name.startsWith('#')) {
return ChannelType.hashtag;
}
return ChannelType.private;
}
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
}
+33
View File
@@ -4,6 +4,8 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'channel.dart';
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
@@ -162,6 +164,12 @@ class Community {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Returns true if this is the community's public channel
static bool isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
@@ -237,3 +245,28 @@ class Community {
@override
int get hashCode => id.hashCode;
}
class CommunityPskIndex {
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
void initialize(List<Community> communities) {
_pskToCommunity.clear();
for (final community in communities) {
// Map the community public channel PSK
final publicPsk = community.deriveCommunityPublicPsk();
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
// Map all known hashtag channel PSKs
for (final hashtag in community.hashtagChannels) {
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// Returns the community this channel belongs to, or null if not a community channel
Community? getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
}
+1
View File
@@ -50,6 +50,7 @@ class PathRecord {
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
byteCount: json['byte_count'] as int? ?? 0,
);
}
}
+79 -6
View File
@@ -8,6 +8,8 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
@@ -56,8 +58,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final ChatScrollController _scrollController = ChatScrollController();
final FocusNode _textFieldFocusNode = FocusNode();
ChannelMessage? _replyingToMessage;
final CommunityStore _communityStore = CommunityStore();
final CommunityPskIndex _communityIndex = CommunityPskIndex();
final Map<String, GlobalKey> _messageKeys = {};
bool _isLoadingOlder = false;
bool _communitiesLoaded = false;
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
@@ -81,6 +86,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final idx = widget.channel.index;
final unread = widget.initialUnreadCount;
final messages = connector.getChannelMessages(widget.channel);
_loadCommunities();
ChannelMessage? anchor;
if (unread > 0) {
anchor = _findOldestUnreadChannelAnchor(messages, unread);
@@ -107,6 +113,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
});
}
// TODO: Reload communities when returning from another screen
Future<void> _loadCommunities() async {
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
final communities = await _communityStore.loadCommunities();
if (mounted) {
setState(() {
_communityIndex.initialize(communities);
_communitiesLoaded = true;
});
}
}
ChannelMessage? _findOldestUnreadChannelAnchor(
List<ChannelMessage> messages,
int unreadCount,
@@ -193,16 +212,63 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
Widget _channelIcon(Channel channel) {
// Determine icon based on channel type
final ChannelType channelType = Channel.getChannelType(
channel,
_communityIndex,
);
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
IconData icon;
switch (channelType) {
case ChannelType.communityPublic:
icon = Icons.groups;
case ChannelType.communityHashtag:
icon = Icons.tag;
case ChannelType.public:
icon = Icons.public;
case ChannelType.hashtag:
icon = Icons.tag;
case ChannelType.private:
icon = Icons.lock;
}
return Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: _communitiesLoaded
? Icon(icon, size: 20)
: SizedBox.square(dimension: 20),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
),
),
child: const Icon(Icons.people, size: 8, color: Colors.white),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Icon(
widget.channel.isPublicChannel ? Icons.public : Icons.tag,
size: 20,
),
_channelIcon(widget.channel),
const SizedBox(width: 8),
Expanded(
child: Column(
@@ -311,6 +377,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final reversedMessages = messages.reversed.toList();
final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
final keyedMessageIds = <String>{};
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -347,14 +414,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
final shouldAttachMessageKey = keyedMessageIds.add(
message.messageId,
);
if (shouldAttachMessageKey &&
!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
final isUnreadAnchor =
_unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId;
return Container(
key: _messageKeys[message.messageId]!,
key: shouldAttachMessageKey
? _messageKeys[message.messageId]
: null,
child: Builder(
builder: (context) {
final textScale = context
+21 -48
View File
@@ -40,15 +40,12 @@ class ChannelsScreen extends StatefulWidget {
State<ChannelsScreen> createState() => _ChannelsScreenState();
}
class _ChannelsScreenState extends State<ChannelsScreen>
{
class _ChannelsScreenState extends State<ChannelsScreen> {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
Timer? _searchDebounce;
final CommunityPskIndex _communityIndex = CommunityPskIndex();
List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
Timer? _searchDebounce;
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@@ -71,37 +68,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
if (mounted) {
setState(() {
_communities = communities;
_buildPskCommunityMap();
_communityIndex.initialize(communities);
});
}
}
void _buildPskCommunityMap() {
_pskToCommunity.clear();
for (final community in _communities) {
// Map the community public channel PSK
final publicPsk = community.deriveCommunityPublicPsk();
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
// Map all known hashtag channel PSKs
for (final hashtag in community.hashtagChannels) {
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// Returns the community this channel belongs to, or null if not a community channel
Community? _getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
/// Returns true if this is the community's public channel
bool _isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
@override
void dispose() {
_searchDebounce?.cancel();
@@ -372,6 +343,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
selectedIndex: 1,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -387,41 +360,41 @@ class _ChannelsScreenState extends State<ChannelsScreen>
int? dragIndex,
}) {
final unreadCount = connector.getUnreadCountForChannel(channel);
final community = _getCommunityForChannel(channel);
final isCommunityChannel = community != null;
final isCommunityPublic =
isCommunityChannel && _isCommunityPublicChannel(channel, community);
// Determine icon and colors based on channel type
IconData icon;
Color iconColor;
Color bgColor;
if (isCommunityChannel) {
// Community channel styling
final ChannelType channelType = Channel.getChannelType(
channel,
_communityIndex,
);
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
switch (channelType) {
case ChannelType.communityPublic:
icon = Icons.groups;
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
if (isCommunityPublic) {
icon = Icons.groups;
} else {
case ChannelType.communityHashtag:
icon = Icons.tag;
}
} else if (channel.isPublicChannel) {
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
case ChannelType.public:
icon = Icons.public;
iconColor = Colors.green;
bgColor = Colors.green.withValues(alpha: 0.2);
} else if (channel.name.startsWith('#')) {
case ChannelType.hashtag:
icon = Icons.tag;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
} else {
case ChannelType.private:
icon = Icons.lock;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
}
return Card(
key: ValueKey('channel_${channel.index}'),
key: ValueKey('${channel.index}_${channel.pskHex}_${channel.name}'),
margin: const EdgeInsets.only(bottom: 12),
child: GestureDetector(
onSecondaryTapUp: PlatformInfo.isDesktop
+6 -1
View File
@@ -451,6 +451,7 @@ class _ChatScreenState extends State<ChatScreen> {
) {
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
var unreadAnchorKeyAssigned = false;
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
@@ -525,7 +526,11 @@ class _ChatScreenState extends State<ChatScreen> {
children: [const UnreadDivider(), bubble],
)
: bubble;
if (identical(message, _pendingUnreadScrollTarget)) {
final shouldAttachUnreadScrollKey =
!unreadAnchorKeyAssigned &&
identical(message, _pendingUnreadScrollTarget);
if (shouldAttachUnreadScrollKey) {
unreadAnchorKeyAssigned = true;
return KeyedSubtree(key: _unreadScrollKey, child: child);
}
return child;
+13 -9
View File
@@ -49,8 +49,7 @@ class ContactsScreen extends StatefulWidget {
State<ContactsScreen> createState() => _ContactsScreenState();
}
class _ContactsScreenState extends State<ContactsScreen>
{
class _ContactsScreenState extends State<ContactsScreen> {
final TextEditingController _searchController = TextEditingController();
final ContactGroupStore _groupStore = ContactGroupStore();
MeshCoreConnector? _scopeSyncConnector;
@@ -314,6 +313,7 @@ class _ContactsScreenState extends State<ContactsScreen>
title: AppBarTitle(context.l10n.contacts_title),
automaticallyImplyLeading: false,
actions: [
if (connector.isConnected)
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
@@ -324,12 +324,12 @@ class _ContactsScreenState extends State<ContactsScreen>
Text(context.l10n.contacts_zeroHopAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: false),
onTap: () async {
await connector.sendSelfAdvert(flood: false);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
);
},
),
PopupMenuItem(
@@ -340,12 +340,12 @@ class _ContactsScreenState extends State<ContactsScreen>
Text(context.l10n.contacts_floodAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: true),
onTap: () async {
await connector.sendSelfAdvert(flood: true);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
);
},
),
PopupMenuItem(
@@ -442,6 +442,8 @@ class _ContactsScreenState extends State<ContactsScreen>
selectedIndex: 0,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -826,6 +828,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact,
);
return _ContactTile(
key: ValueKey(contact.publicKeyHex),
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
@@ -1046,7 +1049,7 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.contact_connectCompanion),
content: Text(context.l10n.dialog_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
@@ -1494,6 +1497,7 @@ class _ContactTile extends StatelessWidget {
final VoidCallback onLongPress;
const _ContactTile({
super.key,
required this.contact,
required this.lastSeen,
required this.unreadCount,
@@ -539,6 +539,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
child: QuickSwitchBar(
selectedIndex: 2,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
contactsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalContactsUnreadCount(),
channelsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalChannelsUnreadCount(),
),
),
);
+29 -1
View File
@@ -416,7 +416,7 @@ class _MapScreenState extends State<MapScreen> {
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (!_isBuildingPathTrace)
if (!_isBuildingPathTrace && connector.isConnected)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(
@@ -694,6 +694,8 @@ class _MapScreenState extends State<MapScreen> {
selectedIndex: 2,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
floatingActionButton: FloatingActionButton(
@@ -1498,7 +1500,28 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showCompanionRequiredDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.dialog_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
void _showRepeaterLogin(BuildContext context, Contact repeater) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RepeaterLoginDialog(
@@ -1521,6 +1544,11 @@ class _MapScreenState extends State<MapScreen> {
}
void _showRoomLogin(BuildContext context, Contact room) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
+10 -1
View File
@@ -42,9 +42,18 @@ class ChannelStore {
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
final channels = jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
// Deduplicate: keep the last entry per channel index
final seen = <int>{};
final deduped = <Channel>[];
for (final channel in channels.reversed) {
if (seen.add(channel.index)) {
deduped.add(channel);
}
}
return deduped.reversed.toList();
} catch (_) {
return [];
}
+15
View File
@@ -31,6 +31,21 @@ Future<bool> showDisconnectDialog(
if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect();
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.dialog_disconnectedTitle),
content: Text(context.l10n.dialog_disconnectedMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_ok),
),
],
),
);
}
return true;
}
return false;
+1
View File
@@ -69,6 +69,7 @@ class AppBarTitle extends StatelessWidget {
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
if (connector.supportsCompanionRadioStats)
if (connector.isConnected)
const RadioStatsIconButton(compact: true),
],
),
+44 -2
View File
@@ -6,11 +6,15 @@ import '../l10n/l10n.dart';
class QuickSwitchBar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
const QuickSwitchBar({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
});
@override
@@ -62,15 +66,30 @@ class QuickSwitchBar extends StatelessWidget {
onDestinationSelected: onDestinationSelected,
destinations: [
NavigationDestination(
icon: const Icon(Icons.people_outline),
icon: _buildIconWithBadge(
const Icon(Icons.people_outline),
contactsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.people),
contactsUnreadCount,
),
label: context.l10n.nav_contacts,
),
NavigationDestination(
icon: const Icon(Icons.tag),
icon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
label: context.l10n.nav_channels,
),
NavigationDestination(
icon: const Icon(Icons.map_outlined),
selectedIcon: const Icon(Icons.map),
label: context.l10n.nav_map,
),
],
@@ -81,4 +100,27 @@ class QuickSwitchBar extends StatelessWidget {
),
);
}
Widget _buildIconWithBadge(Icon icon, int count) {
if (count <= 0) return icon;
return Stack(
clipBehavior: Clip.none,
children: [
icon,
Positioned(
right: -2,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.redAccent,
shape: BoxShape.circle,
),
),
),
],
);
}
}
+6
View File
@@ -38,6 +38,12 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
lastHost = host;
lastPort = port;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({
+6
View File
@@ -73,6 +73,12 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
void setUsbFallbackDeviceName(String label) {
fallbackDeviceName = label;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({
+373 -881
View File
File diff suppressed because it is too large Load Diff