mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: UX polish, localization, advert relocation, and map filter fixes
- i18n: translate connection screen + chat tab keys into 13 locales; add "Send my contact" (advert) strings in all locales - feat: move self-advert from home header into composer "+" action menu (new lib/utils/advert_helper.dart, always shows Flood/Direct sheet) - fix: hide-repeaters map toggle was bypassed in simple mode - fix: simple mode map now shows only favourite chat contacts - fix: wrap ListTiles in Material (contact_tile secondary actions, inferred contact group card) to satisfy Flutter ink assertions - device-authoritative contact/channel sync and UX review fixes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
|
||||
final _nameController = TextEditingController();
|
||||
final _secretController = TextEditingController();
|
||||
bool _isCreating = false;
|
||||
bool _obscureSecret = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -203,8 +204,23 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
|
||||
labelText: l10n.channelSecret,
|
||||
hintText: l10n.channelSecretHint,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _isCreating
|
||||
? null
|
||||
: () => setState(() {
|
||||
_obscureSecret = !_obscureSecret;
|
||||
}),
|
||||
icon: Icon(
|
||||
_obscureSecret
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
tooltip: _obscureSecret
|
||||
? 'Show secret'
|
||||
: 'Hide secret',
|
||||
),
|
||||
),
|
||||
obscureText: true,
|
||||
obscureText: _obscureSecret,
|
||||
enabled: !_isCreating,
|
||||
maxLength: 32,
|
||||
validator: _validateSecret,
|
||||
|
||||
@@ -120,34 +120,6 @@ class ContactTile extends StatelessWidget {
|
||||
: null;
|
||||
void handleTap() => _handlePrimaryTap(context, contact);
|
||||
|
||||
final onLongPress = isPingInProgress
|
||||
? null
|
||||
: () async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final hasPath = contact.routeHasPath;
|
||||
|
||||
final result = await connectionProvider.smartPing(
|
||||
contactPublicKey: contact.publicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: () {
|
||||
if (context.mounted) {
|
||||
ToastLogger.warning(
|
||||
context,
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.directPingTimeout(contact.displayName),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (context.mounted && !result.success) {
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.pingFailed(contact.displayName),
|
||||
);
|
||||
}
|
||||
};
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final titleStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
@@ -233,7 +205,7 @@ class ContactTile extends StatelessWidget {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: handleTap,
|
||||
onLongPress: onLongPress,
|
||||
onLongPress: handleTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
@@ -418,6 +390,36 @@ class ContactTile extends StatelessWidget {
|
||||
_showContactActionSheet(context, contact);
|
||||
}
|
||||
|
||||
Future<void> _smartPingContact(BuildContext context, Contact contact) async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final hasPath = contact.routeHasPath;
|
||||
|
||||
final result = await connectionProvider.smartPing(
|
||||
contactPublicKey: contact.publicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: () {
|
||||
if (context.mounted) {
|
||||
ToastLogger.warning(
|
||||
context,
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.directPingTimeout(contact.displayName),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
if (result.success) {
|
||||
ToastLogger.success(context, 'Ping reply from ${contact.displayName}');
|
||||
} else {
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.pingFailed(contact.displayName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _locationSharingErrorMessage(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Bad state: ')) {
|
||||
@@ -498,6 +500,11 @@ class ContactTile extends StatelessWidget {
|
||||
contact.type == ContactType.repeater ||
|
||||
contact.type == ContactType.sensor;
|
||||
final canPreviewSensor = contact.isSensor;
|
||||
final canSmartPing =
|
||||
contact.type == ContactType.chat || contact.type == ContactType.sensor;
|
||||
final isPingInProgress = context.read<ConnectionProvider>().isPingInProgress(
|
||||
contact.publicKey,
|
||||
);
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final isInSensors = sensorsProvider.isWatched(contact.publicKeyHex);
|
||||
final channelLocationSharingFuture =
|
||||
@@ -662,6 +669,16 @@ class ContactTile extends StatelessWidget {
|
||||
_pingRelay(context, contact);
|
||||
},
|
||||
),
|
||||
if (canSmartPing)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.network_ping,
|
||||
label: l10n.ping,
|
||||
enabled: !isPingInProgress,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _smartPingContact(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.edit_outlined,
|
||||
@@ -1596,11 +1613,12 @@ class _ContactActionSheetState extends State<_ContactActionSheet> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
Material(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
side: BorderSide(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.28),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,6 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool _isLoggingIn = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isDisposed = false; // Track disposal state for async callbacks
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,7 +30,6 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_passwordController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
@@ -228,6 +226,15 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
// Save password before sending
|
||||
await _savePassword(password);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Capture references before the sheet pops: the login result arrives
|
||||
// after Navigator.pop, when this State is disposed, so the callbacks
|
||||
// below must not depend on this widget's context.
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Set up login callbacks
|
||||
Function(Uint8List, int, bool, int)? originalOnSuccess;
|
||||
Function(Uint8List)? originalOnFail;
|
||||
@@ -251,12 +258,12 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
' Messages will be fetched when onMessageWaiting callback is triggered',
|
||||
);
|
||||
|
||||
// Check both _isDisposed flag and mounted to handle race conditions
|
||||
if (_isDisposed || !mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
// Use the captured messenger: the sheet has already been popped by the
|
||||
// time this callback fires, so this State's context is gone.
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
content: Text(l10n.loggedInSuccessfully),
|
||||
backgroundColor: colorScheme.primary,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
@@ -269,12 +276,12 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
debugPrint('❌ [RoomLogin] Login failed - incorrect password');
|
||||
|
||||
// Check both _isDisposed flag and mounted to handle race conditions
|
||||
if (_isDisposed || !mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
// Use the captured messenger: the sheet has already been popped by the
|
||||
// time this callback fires, so this State's context is gone.
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loginFailed),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
content: Text(l10n.loginFailed),
|
||||
backgroundColor: colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
@@ -289,15 +296,14 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
_focusNode.unfocus();
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context); // Close the dialog
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close the dialog
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.loggingIn(widget.contact.displayName),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
content: Text(l10n.loggingIn(widget.contact.displayName)),
|
||||
backgroundColor: colorScheme.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -306,13 +312,10 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
connectionProvider.onLoginSuccess = originalOnSuccess;
|
||||
connectionProvider.onLoginFail = originalOnFail;
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.failedToSendLogin(e.toString()),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
content: Text(l10n.failedToSendLogin(e.toString())),
|
||||
backgroundColor: colorScheme.error,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user