From c37abb63e37b26ba25893f71fa97bcfd90047059 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 11:56:42 -0800 Subject: [PATCH 1/5] add export and import contact frame builders in meshcore_protocol.dart and implement contact export functionality in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 18 ++++++ lib/screens/contacts_screen.dart | 89 ++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index f9241e8..0e43e46 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -708,3 +708,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { } return writer.toBytes(); } + +// Build a export contact frame +// [cmd][pub_key x32 / if empty exports your contact info] +Uint8List buildExportContactFrame(Uint8List pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdExportContact); + writer.writeBytes(pubKey); + return writer.toBytes(); +} + +// Build a import contact frame +// [cmd][contact_frame x148] +Uint8List buildImportContactFrame(Uint8List contactFrame) { + final writer = BufferWriter(); + writer.writeByte(cmdImportContact); + writer.writeBytes(contactFrame); + return writer.toBytes(); +} \ No newline at end of file diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 54f819c..4c8e396 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -77,6 +78,13 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + Future _contactExport(Uint8List pubKey) async { + final connector = Provider.of(context, listen: false); + final exportContactFrame = buildExportContactFrame(pubKey); + await connector.sendFrame(exportContactFrame); + return; + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -96,18 +104,77 @@ class _ContactsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context, connector), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + PopupMenuButton(itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text("Zero Hop Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: false), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text("Flood Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: true), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text("Copy Advert to Clipboard"), + ], + ), + onTap: () => _contactExport(Uint8List.fromList([])), + ), + ], + icon: const Icon(Icons.connect_without_contact), + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + const Text('Settings'), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), From 641307a31632574a81dc59ca18f26ff9342eaf1b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 12:19:45 -0800 Subject: [PATCH 2/5] Added response code for exporting contacts and implement frame listener in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 1 + lib/screens/contacts_screen.dart | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0e43e46..dda07bd 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -159,6 +159,7 @@ const int respCodeContactMsgRecv = 7; const int respCodeChannelMsgRecv = 8; const int respCodeCurrTime = 9; const int respCodeNoMoreMessages = 10; +const int respCodeExportContact = 11; const int respCodeBattAndStorage = 12; const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 4c8e396..d5d3377 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -53,16 +54,20 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + StreamSubscription? _frameSubscription; + @override void initState() { super.initState(); _loadGroups(); + _setupFrameListener(); } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); + _frameSubscription?.cancel(); super.dispose(); } @@ -78,6 +83,22 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeExportContact) { + final advertPacket = frameBuffer.readRemainingBytes(); + final hexString = pubKeyToHex(advertPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + } + }); + } + Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); From eeb8ff34e8959bd060b0ae4aa706fb341d7f6c0a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 16:11:21 -0800 Subject: [PATCH 3/5] Implement contact import functionality from clipboard and add relevant UI options --- lib/connector/meshcore_protocol.dart | 14 +++++-- lib/l10n/app_en.arb | 6 ++- lib/screens/contacts_screen.dart | 60 +++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index dda07bd..0609adb 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -98,6 +98,14 @@ class BufferWriter { } writeBytes(bytes); } + + void writeHex(String hex) { + List result = []; + for (int i = 0; i < hex.length ~/ 2; i++) { + result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + } + writeBytes(Uint8List.fromList(result)); + } } // Command codes (to device) @@ -720,10 +728,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) { } // Build a import contact frame -// [cmd][contact_frame x148] -Uint8List buildImportContactFrame(Uint8List contactFrame) { +// [cmd][contact_frame x98+] +Uint8List buildImportContactFrame(String contactFrame) { final writer = BufferWriter(); writer.writeByte(cmdImportContact); - writer.writeBytes(contactFrame); + writer.writeHex(contactFrame); return writer.toBytes(); } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 56cb1cc..7c8a0a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1308,5 +1308,9 @@ "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", - "listFilter_newGroup": "New group" + "listFilter_newGroup": "New group", + + "contacts_clipboardEmpty": "Clipboard Is Empty.", + "contacts_invalidAdvertFormat": "Invalid Contact Data", + "contacts_contactAdded": "Contact has been added." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5d3377..89a07e0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -106,6 +106,36 @@ class _ContactsScreenState extends State return; } + Future _contactImport() async { + final clipboardData = await Clipboard.getData('text/plain'); + if (clipboardData == null || clipboardData.text == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + ); + return; + } + final text = clipboardData.text!.trim(); + if (!text.startsWith('meshcore://')) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + return; + } + final hexString = text.substring('meshcore://'.length); + try { + final connector = Provider.of(context, listen: false); + final importContactFrame = buildImportContactFrame(hexString); + await connector.sendFrame(importContactFrame); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text(context.l10n.contacts_contactAdded)), + // ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -166,21 +196,21 @@ class _ContactsScreenState extends State ), onTap: () => _contactExport(Uint8List.fromList([])), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text("Add Contact from Clipboard"), + ], + ), + onTap: () => _contactImport(), + ), ], icon: const Icon(Icons.connect_without_contact), ), PopupMenuButton( itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ), PopupMenuItem( child: Row( children: [ @@ -194,6 +224,16 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ) ], icon: const Icon(Icons.more_vert), ), From d0c8fab6fbe451456fe3e98ce3ab5b9150e7b454 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 27 Jan 2026 18:43:59 -0800 Subject: [PATCH 4/5] Add contact import functionality and update UI feedback for import status --- lib/connector/meshcore_connector.dart | 7 +++++ lib/l10n/app_en.arb | 17 ++++++++++- lib/screens/contacts_screen.dart | 44 ++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de39d7e..a5c5290 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,6 +1680,12 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeNewAdvert: + debugPrint('Got NEW_ADVERT'); + _handleContact(frame); + notifyListeners(); + unawaited(_persistContacts()); + break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1737,6 +1743,7 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); + break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c8a0a7..3a9383a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1309,8 +1309,23 @@ "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", "listFilter_newGroup": "New group", + + "contacts_pathTrace": "Path Trace", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Path trace to repeater", + "contacts_repeaterPing": "Ping repeater", + "contacts_roomPathTrace": "Path trace to room server", + "contacts_roomPing": "Ping room server", + "contacts_chatTraceRoute": "Path trace route", + "contacts_pathTraceTo": "Trace route to {name}", + "@contacts_pathTraceTo": { + "placeholders": { + "name": {"type": "String"} + } + }, "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", - "contacts_contactAdded": "Contact has been added." + "contacts_contactImported": "Contact has been Imported.", + "contacts_contactImportFailed": "Contact Failed to Imported." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 89a07e0..64c2924 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -54,6 +54,7 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + bool _imported = false; StreamSubscription? _frameSubscription; @override @@ -96,6 +97,23 @@ class _ContactsScreenState extends State final hexString = pubKeyToHex(advertPacket); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } + + if(code == respCodeOk && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if(code == respCodeErr && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), + ); + } + }); } @@ -126,9 +144,7 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final importContactFrame = buildImportContactFrame(hexString); await connector.sendFrame(importContactFrame); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text(context.l10n.contacts_contactAdded)), - // ); + _imported = true; } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), @@ -923,6 +939,7 @@ class _ContactsScreenState extends State _openChat(context, contact); }, ), + ], ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( @@ -997,7 +1014,7 @@ class _ContactTile extends StatelessWidget { ), title: Text(contact.name), subtitle: Text( - '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', + '${contact.typeLabel}\n${contact.shortPubKeyHex}', ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. @@ -1019,13 +1036,24 @@ class _ContactTile extends StatelessWidget { _formatLastSeen(context, lastSeen), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(contact.pathLabel, + style: TextStyle(fontSize: 12, color: Colors.grey[600])), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), + Text( + _formatLastSeen(context, lastSeen), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), - onTap: onTap, - onLongPress: onLongPress, ); } From 42115bf200e4421c3d914b9c6ebd74345b5c8060 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 28 Jan 2026 11:04:34 -0800 Subject: [PATCH 5/5] Refactor contact handling and enhance UI with new advert options and localized strings --- lib/connector/meshcore_connector.dart | 7 ---- lib/connector/meshcore_protocol.dart | 21 +++++++++++ lib/l10n/app_en.arb | 6 +++- lib/models/contact.dart | 4 +++ lib/screens/contacts_screen.dart | 51 ++++++++++++--------------- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a5c5290..de39d7e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,12 +1680,6 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; - case pushCodeNewAdvert: - debugPrint('Got NEW_ADVERT'); - _handleContact(frame); - notifyListeners(); - unawaited(_persistContacts()); - break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1743,7 +1737,6 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); - break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0609adb..df07758 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -18,6 +18,10 @@ class BufferReader { return data; } + void skipBytes(int count) { + _pointer += count; + } + Uint8List readRemainingBytes() => readBytes(remaining); String readString() => @@ -135,6 +139,7 @@ const int cmdSendStatusReq = 27; const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; +const int cmdSendTracePath = 36; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -185,6 +190,7 @@ const int pushCodeLoginSuccess = 0x85; const int pushCodeLoginFail = 0x86; const int pushCodeStatusResponse = 0x87; const int pushCodeLogRxData = 0x88; +const int pushCodeTraceData = 0x89; const int pushCodeNewAdvert = 0x8A; const int pushCodeTelemetryResponse = 0x8B; const int pushCodeBinaryResponse = 0x8C; @@ -718,6 +724,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { return writer.toBytes(); } +//Build a trace request frame +//[cmd][tag x4][auth x4][flag][payload] +Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) +{ + final writer = BufferWriter(); + writer.writeByte(cmdSendTracePath); + writer.writeUInt32LE(tag); + writer.writeUInt32LE(auth); + writer.writeByte(flag); + if (payload != null && payload.isNotEmpty) { + writer.writeBytes(payload); + } + return writer.toBytes(); +} + // Build a export contact frame // [cmd][pub_key x32 / if empty exports your contact info] Uint8List buildExportContactFrame(Uint8List pubKey) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3a9383a..706d361 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1327,5 +1327,9 @@ "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", "contacts_contactImported": "Contact has been Imported.", - "contacts_contactImportFailed": "Contact Failed to Imported." + "contacts_contactImportFailed": "Contact Failed to Imported.", + "contacts_zeroHopAdvert":"Zero Hop Advert", + "contacts_floodAdvert":"Flood Advert", + "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", + "contacts_addContactFromClipboard":"Add Contact from Clipboard" } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 364deff..9599e01 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -102,6 +102,10 @@ class Contact { return parts.join(','); } + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + Uint8List get _pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 64c2924..e9aef26 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -177,7 +177,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.connect_without_contact), const SizedBox(width: 8), - Text("Zero Hop Advert"), + Text(context.l10n.contacts_zeroHopAdvert), ], ), onTap: () => { @@ -192,7 +192,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.cell_tower), const SizedBox(width: 8), - Text("Flood Advert"), + Text(context.l10n.contacts_floodAdvert), ], ), onTap: () => { @@ -207,7 +207,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.copy), const SizedBox(width: 8), - Text("Copy Advert to Clipboard"), + Text(context.l10n.contacts_copyAdvertToClipboard), ], ), onTap: () => _contactExport(Uint8List.fromList([])), @@ -217,7 +217,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.paste), const SizedBox(width: 8), - Text("Add Contact from Clipboard"), + Text(context.l10n.contacts_addContactFromClipboard), ], ), onTap: () => _contactImport(), @@ -227,12 +227,22 @@ class _ContactsScreenState extends State ), PopupMenuButton( itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context, connector), + ), PopupMenuItem( child: Row( children: [ const Icon(Icons.settings), const SizedBox(width: 8), - const Text('Settings'), + Text(context.l10n.settings_title), ], ), onTap: () => Navigator.push( @@ -240,16 +250,6 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ) ], icon: const Icon(Icons.more_vert), ), @@ -930,7 +930,7 @@ class _ContactsScreenState extends State _showRoomLogin(context, contact, RoomLoginDestination.management); }, ), - ] else + ] else ...[ ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -1005,17 +1005,16 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final shotPublicKey = - "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; return ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), title: Text(contact.name), - subtitle: Text( - '${contact.typeLabel}\n${contact.shortPubKeyHex}', - ), + subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(contact.pathLabel), + Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)) + ],), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. trailing: MediaQuery( @@ -1039,21 +1038,15 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Text(contact.pathLabel, - style: TextStyle(fontSize: 12, color: Colors.grey[600])), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), + onTap: onTap, + onLongPress: onLongPress, ); }