diff --git a/lib/app/features/auth/providers/auth_provider.m.dart b/lib/app/features/auth/providers/auth_provider.m.dart index e55deef624..a41e00270c 100644 --- a/lib/app/features/auth/providers/auth_provider.m.dart +++ b/lib/app/features/auth/providers/auth_provider.m.dart @@ -4,13 +4,19 @@ import 'dart:async'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/extensions/bool.dart'; +import 'package:ion/app/components/message_notification/models/message_notification.f.dart'; +import 'package:ion/app/components/message_notification/providers/message_notification_notifier_provider.r.dart'; +import 'package:ion/app/extensions/extensions.dart'; import 'package:ion/app/features/auth/providers/local_passkey_creds_provider.r.dart'; import 'package:ion/app/features/core/providers/main_wallet_provider.r.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_event_signer_provider.r.dart'; +import 'package:ion/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart'; import 'package:ion/app/features/user/providers/biometrics_provider.r.dart'; +import 'package:ion/app/features/user/providers/user_metadata_provider.r.dart'; +import 'package:ion/app/router/app_routes.gr.dart'; import 'package:ion/app/services/ion_identity/ion_identity_provider.r.dart'; import 'package:ion/app/services/storage/local_storage.r.dart'; +import 'package:ion/generated/assets.gen.dart'; import 'package:ion_identity_client/ion_identity.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -100,6 +106,9 @@ class Auth extends _$Auth { final authenticatedIdentityKeyNames = state.valueOrNull?.authenticatedIdentityKeyNames ?? []; if (authenticatedIdentityKeyNames.length > 1) { ref.read(userSwitchInProgressProvider.notifier).startSwitchingViaLogout(); + + // Remove push subscription for logged out user + await ref.read(pushSubscriptionSyncProvider.notifier).deletePushSubscriptionForCurrentUser(); } final ionIdentity = await ref.read(ionIdentityProvider.future); @@ -262,6 +271,7 @@ class UserSwitchState with _$UserSwitchState { const factory UserSwitchState({ @Default(false) bool isSwitchingProgress, @Default(false) bool isLogoutTriggered, + @Default(false) bool isShowNotification, }) = _UserSwitchState; } @@ -275,6 +285,10 @@ class UserSwitchInProgress extends _$UserSwitchInProgress { if (prev != null && next != null && prev != next) { completeSwitching(); } + + if (state.isShowNotification) { + _showPushNotificationAfterSwitching(next); + } }, ); return const UserSwitchState(); @@ -291,11 +305,39 @@ class UserSwitchInProgress extends _$UserSwitchInProgress { ); } + void needToShowPushSwitchNotification() { + state = state.copyWith( + isShowNotification: true, + ); + } + void completeSwitching() { if (state.isLogoutTriggered) { state = state.copyWith(isLogoutTriggered: false); } else { - state = const UserSwitchState(); + state = state.copyWith( + isSwitchingProgress: false, + isLogoutTriggered: false, + ); } } + + void _showPushNotificationAfterSwitching(String? identityKeyName) { + state = state.copyWith(isShowNotification: false); + if (identityKeyName == null) return; + + final userMetadata = ref.read(userMetadataProvider(identityKeyName)).valueOrNull; + final username = userMetadata?.data.name ?? identityKeyName; + + final context = rootNavigatorKey.currentContext; + final message = + (context != null && context.mounted) ? context.i18n.switched_to_username(username) : ''; + + ref.read(messageNotificationNotifierProvider.notifier).show( + MessageNotification( + message: message, + icon: Assets.svg.iconCheckSuccess.icon(size: 16.0.s), + ), + ); + } } diff --git a/lib/app/features/core/providers/main_wallet_provider.r.dart b/lib/app/features/core/providers/main_wallet_provider.r.dart index d085accb72..cf0bd10553 100644 --- a/lib/app/features/core/providers/main_wallet_provider.r.dart +++ b/lib/app/features/core/providers/main_wallet_provider.r.dart @@ -5,6 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/exceptions/exceptions.dart'; import 'package:ion/app/features/auth/providers/auth_provider.m.dart'; import 'package:ion/app/features/core/providers/wallets_provider.r.dart'; +import 'package:ion/app/services/ion_identity/ion_identity_provider.r.dart'; +import 'package:ion/app/services/logger/logger.dart'; +import 'package:ion/app/services/storage/local_storage.r.dart'; import 'package:ion_identity_client/ion_identity.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -28,3 +31,40 @@ Future mainWallet(Ref ref) async { return mainWallet; } + +@riverpod +Future userPubkeyByIdentityKeyName( + Ref ref, + String identityKeyName, +) async { + const cacheKeyPrefix = 'user_pubkey_'; + + final sharedPrefs = await ref.read(sharedPreferencesFoundationProvider.future); + final cacheKey = '$cacheKeyPrefix$identityKeyName'; + final cachedPubkey = await sharedPrefs.getString(cacheKey); + + if (cachedPubkey != null && cachedPubkey.isNotEmpty) { + return cachedPubkey; + } + + try { + final ionIdentity = await ref.read(ionIdentityProvider.future); + final wallets = await ionIdentity(username: identityKeyName).wallets.getWallets(); + final mainWallet = wallets.firstWhereOrNull((Wallet wallet) => wallet.name == 'main'); + + if (mainWallet != null) { + final userPubkey = mainWallet.signingKey.publicKey; + await sharedPrefs.setString(cacheKey, userPubkey); + return userPubkey; + } + } catch (error, stackTrace) { + Logger.error( + error, + message: 'Failed to get user pubkey for identity key name: $identityKeyName', + stackTrace: stackTrace, + ); + return null; + } + + return null; +} diff --git a/lib/app/features/push_notifications/data/models/ion_connect_push_data_payload.f.dart b/lib/app/features/push_notifications/data/models/ion_connect_push_data_payload.f.dart index ee00184fc5..afc830a133 100644 --- a/lib/app/features/push_notifications/data/models/ion_connect_push_data_payload.f.dart +++ b/lib/app/features/push_notifications/data/models/ion_connect_push_data_payload.f.dart @@ -73,9 +73,18 @@ class IonConnectPushDataPayload { UserMetadataEntity? userMetadata; if (parsedEvent.kind == IonConnectGiftWrapEntity.kind) { - final result = await unwrapGift(parsedEvent); - decryptedEvent = result.$1; - userMetadata = result.$2; + // Attempt to decrypt GiftWrap message. + // If decryption fails (e.g., message encrypted for another user), + // this is expected - we leave decryptedEvent = null. + // The notification will be shown, but user needs to switch accounts to view content. + try { + final result = await unwrapGift(parsedEvent); + decryptedEvent = result.$1; + userMetadata = result.$2; + } catch (_) { + decryptedEvent = null; + userMetadata = null; + } } return IonConnectPushDataPayload._( @@ -109,6 +118,10 @@ class IonConnectPushDataPayload { return false; } + bool isRecipient(String pubkey) { + return _checkMainEventRelevant(currentPubkey: pubkey); + } + Future getNotificationType({ required String currentPubkey, required Future Function(EventReference) getRelatedEntity, diff --git a/lib/app/features/push_notifications/providers/foreground_messages_handler_provider.r.dart b/lib/app/features/push_notifications/providers/foreground_messages_handler_provider.r.dart index 1fe185bdfd..c626d89e3b 100644 --- a/lib/app/features/push_notifications/providers/foreground_messages_handler_provider.r.dart +++ b/lib/app/features/push_notifications/providers/foreground_messages_handler_provider.r.dart @@ -42,14 +42,23 @@ class ForegroundMessagesHandler extends _$ForegroundMessagesHandler { final data = await IonConnectPushDataPayload.fromEncoded( response.data, unwrapGift: (eventMassage) async { - final giftUnwrapService = await ref.read(giftUnwrapServiceProvider.future); - - final event = await giftUnwrapService.unwrap(eventMassage); - final userMetadata = await ref.read( - userMetadataProvider(event.masterPubkey).future, - ); - - return (event, userMetadata); + // Attempt to decrypt GiftWrap message. + // If decryption fails (e.g., message encrypted for another user), + // this is expected - we return (null, null) to allow notification display. + // User can switch accounts to view the content. + try { + final giftUnwrapService = await ref.read(giftUnwrapServiceProvider.future); + + final event = await giftUnwrapService.unwrap(eventMassage); + final userMetadata = await ref.read( + userMetadataProvider(event.masterPubkey).future, + ); + + return (event, userMetadata); + } catch (error) { + // Expected: message encrypted for another user + return (null, null); + } }, ); @@ -140,16 +149,20 @@ class ForegroundMessagesHandler extends _$ForegroundMessagesHandler { required IonConnectPushDataPayload data, }) async { if (data.event.kind == IonConnectGiftWrapEntity.kind) { - final giftUnwrapService = await ref.watch(giftUnwrapServiceProvider.future); final currentPubkey = ref.watch(currentPubkeySelectorProvider); + // If no user is logged in, skip the notification if (currentPubkey == null) { return true; } - final rumor = await giftUnwrapService.unwrap(data.event); + if (data.decryptedEvent != null) { + // Skip if message is from current user (self-message) + return data.decryptedEvent!.masterPubkey == currentPubkey; + } - return rumor.masterPubkey == currentPubkey; + // If decryptedEvent is null, message is encrypted for another user - show notification + return false; } return false; diff --git a/lib/app/features/push_notifications/providers/notification_response_service.r.dart b/lib/app/features/push_notifications/providers/notification_response_service.r.dart index 6beba5798c..6e94062c58 100644 --- a/lib/app/features/push_notifications/providers/notification_response_service.r.dart +++ b/lib/app/features/push_notifications/providers/notification_response_service.r.dart @@ -1,5 +1,6 @@ // SPDX-License-Identifier: ice License 1.0 +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,6 +10,7 @@ import 'package:ion/app/features/auth/providers/auth_provider.m.dart'; import 'package:ion/app/features/chat/e2ee/model/entities/private_direct_message_data.f.dart'; import 'package:ion/app/features/chat/e2ee/model/entities/private_message_reaction_data.f.dart'; import 'package:ion/app/features/chat/e2ee/providers/gift_unwrap_service_provider.r.dart'; +import 'package:ion/app/features/core/providers/main_wallet_provider.r.dart'; import 'package:ion/app/features/feed/data/models/entities/article_data.f.dart'; import 'package:ion/app/features/feed/data/models/entities/generic_repost.f.dart'; import 'package:ion/app/features/feed/data/models/entities/modifiable_post_data.f.dart'; @@ -41,17 +43,29 @@ class NotificationResponseService { required Future Function(EventReference eventReference) getEntityData, required EventParser eventParser, required String? currentPubkey, + required Future Function() getAuthState, + required Future Function(String identityKeyName) setCurrentUser, + required void Function() markShowNotificationAfterSwitchingAcc, + required Future Function(String identityKeyName) userPubkeyByIdentityKeyName, }) : _getGiftUnwrapService = getGiftUnwrapService, _getUserMetadata = getUserMetadata, _getEntityData = getEntityData, _eventParser = eventParser, - _currentPubkey = currentPubkey; + _currentPubkey = currentPubkey, + _getAuthState = getAuthState, + _setCurrentUser = setCurrentUser, + _markShowNotificationAfterSwitchingAcc = markShowNotificationAfterSwitchingAcc, + _userPubkeyByIdentityKeyName = userPubkeyByIdentityKeyName; final Future Function() _getGiftUnwrapService; final UserMetadataEntity? Function(String pubkey) _getUserMetadata; final Future Function(EventReference eventReference) _getEntityData; final EventParser _eventParser; final String? _currentPubkey; + final Future Function() _getAuthState; + final Future Function(String identityKeyName) _setCurrentUser; + final void Function() _markShowNotificationAfterSwitchingAcc; + final Future Function(String identityKeyName) _userPubkeyByIdentityKeyName; /// Checks if any modal is open and closes it before navigation void _checkModal() { @@ -67,6 +81,16 @@ class NotificationResponseService { } } + void _closeAllModalsAndNavigateToHomeFeed() { + final context = _getNavigatorContext(); + if (context != null) { + if (context.canPop()) { + Navigator.of(context).popUntil((Route route) => route.isFirst); + } + FeedRoute().go(context); + } + } + /// Safely gets the navigator context, returning null if context is null or not mounted BuildContext? _getNavigatorContext() { final context = rootNavigatorKey.currentContext; @@ -103,6 +127,8 @@ class NotificationResponseService { final entity = _eventParser.parse(notificationPayload.event); + await _switchToRecipientUserForEntity(notificationPayload); + _checkModal(); switch (entity) { @@ -161,6 +187,77 @@ class NotificationResponseService { } } + String? _getRecipientPubkey(IonConnectEntity entity) { + return switch (entity) { + ReactionEntity() => entity.data.eventReference.masterPubkey, + RepostEntity() => entity.data.eventReference.masterPubkey, + GenericRepostEntity() => entity.data.eventReference.masterPubkey, + ModifiablePostEntity() => () { + final relatedPubkeys = entity.data.relatedPubkeys; + if (relatedPubkeys != null && relatedPubkeys.isNotEmpty) { + return relatedPubkeys.first.value; + } + final quotedEvent = entity.data.quotedEvent; + return quotedEvent?.eventReference.masterPubkey; + }(), + PostEntity() => () { + final relatedPubkeys = entity.data.relatedPubkeys; + if (relatedPubkeys != null && relatedPubkeys.isNotEmpty) { + return relatedPubkeys.first.value; + } + final quotedEvent = entity.data.quotedEvent; + return quotedEvent?.eventReference.masterPubkey; + }(), + FollowListEntity() => entity.masterPubkeys.lastOrNull, + IonConnectGiftWrapEntity() => () { + final relatedPubkeys = entity.data.relatedPubkeys; + if (relatedPubkeys.isNotEmpty) { + return relatedPubkeys.first.value; + } + return null; + }(), + _ => null, + }; + } + + Future _switchToRecipientUserForEntity( + IonConnectPushDataPayload notificationPayload, + ) async { + if (_currentPubkey != null && notificationPayload.isRecipient(_currentPubkey)) { + return; + } + + _closeAllModalsAndNavigateToHomeFeed(); + + final entity = _eventParser.parse(notificationPayload.event); + final recipientPubkey = _getRecipientPubkey(entity); + if (recipientPubkey == null) { + return; + } + + final authState = await _getAuthState(); + final authenticatedIdentityKeyNames = authState.authenticatedIdentityKeyNames; + + String? recipientIdentityKeyName; + + final pubkeyResults = await Future.wait( + authenticatedIdentityKeyNames.map(_userPubkeyByIdentityKeyName), + ); + + for (final entry in authenticatedIdentityKeyNames.asMap().entries) { + if (pubkeyResults[entry.key] == recipientPubkey) { + recipientIdentityKeyName = entry.value; + break; + } + } + + if (recipientIdentityKeyName != null) { + _markShowNotificationAfterSwitchingAcc(); + await _setCurrentUser(recipientIdentityKeyName); + await _getAuthState(); + } + } + Future _handleGiftWrap(EventMessage giftWrap, {bool isInitialNotification = false}) async { final giftUnwrapService = await _getGiftUnwrapService(); @@ -349,6 +446,15 @@ NotificationResponseService notificationResponseService(Ref ref) { Future getEntityData(EventReference eventReference) => ref.read(ionConnectEntityWithCountersProvider(eventReference: eventReference).future); final eventParser = ref.watch(eventParserProvider); + Future getAuthState() => ref.read(authProvider.future); + Future setCurrentUser(String identityKeyName) => + ref.read(authProvider.notifier).setCurrentUser(identityKeyName); + void markShowNotificationAfterSwitchingAcc() { + ref.read(userSwitchInProgressProvider.notifier).needToShowPushSwitchNotification(); + } + + Future userPubkeyByIdentityKeyName(String identityKeyName) => + ref.read(userPubkeyByIdentityKeyNameProvider(identityKeyName).future); return NotificationResponseService( getGiftUnwrapService: getGiftUnwrapService, @@ -356,5 +462,9 @@ NotificationResponseService notificationResponseService(Ref ref) { getEntityData: getEntityData, eventParser: eventParser, currentPubkey: currentPubkey, + getAuthState: getAuthState, + setCurrentUser: setCurrentUser, + markShowNotificationAfterSwitchingAcc: markShowNotificationAfterSwitchingAcc, + userPubkeyByIdentityKeyName: userPubkeyByIdentityKeyName, ); } diff --git a/lib/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart b/lib/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart index f2ecad9250..db4000c75f 100644 --- a/lib/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart +++ b/lib/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart @@ -10,6 +10,7 @@ import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.r.da import 'package:ion/app/features/push_notifications/data/models/push_subscription.f.dart'; import 'package:ion/app/features/push_notifications/providers/push_subscription_provider.r.dart'; import 'package:ion/app/features/push_notifications/providers/selected_push_categories_ion_subscription_provider.r.dart'; +import 'package:ion/app/services/logger/logger.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'push_subscription_sync_provider.r.g.dart'; @@ -24,6 +25,9 @@ class PushSubscriptionSync extends _$PushSubscriptionSync { return; } + // React to user switch and sync push subscription + ref.watch(currentIdentityKeyNameSelectorProvider); + final delegationComplete = await ref.watch(delegationCompleteProvider.future); if (!delegationComplete) { @@ -66,4 +70,38 @@ class PushSubscriptionSync extends _$PushSubscriptionSync { ); ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } + + Future deletePushSubscriptionForCurrentUser() async { + try { + // Use currentUserPushSubscriptionProvider since user is still active before logout + final subscription = await ref.read(currentUserPushSubscriptionProvider.future); + + if (subscription == null) { + return; + } + + // First, update subscription with empty filters (same as when unsubscribing from all categories) + final relayUrl = subscription.data.relay.url; + final emptyFiltersSubscription = PushSubscriptionData( + deviceId: subscription.data.deviceId, + platform: subscription.data.platform, + relay: subscription.data.relay, + fcmToken: subscription.data.fcmToken, + filters: [], // Set empty filters + ); + + await ref.read(ionConnectNotifierProvider.notifier).sendEntityData( + emptyFiltersSubscription, + actionSource: ActionSourceRelayUrl(relayUrl), + ); + + await _deleteSubscription(subscription); + } catch (error, stackTrace) { + Logger.error( + error, + message: 'Failed to delete current user push subscription', + stackTrace: stackTrace, + ); + } + } } diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 7dafe4afea..08be9b4ae3 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -881,6 +881,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "تم التبديل إلى {username}", "tokenized_communities_top_tokens": "أفضل التوكنات", "tokenized_communities_trending_tokens": "التوكنات الرائجة", "tokenized_community_chart_tab": "المخطط", diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 0a3411a437..97a3b8924c 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Превключено към {username}", "tokenized_communities_top_tokens": "Топ токени", "tokenized_communities_trending_tokens": "Популярни токени", "tokenized_community_chart_tab": "Графика", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e010274544..c21c572bb7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Zu {username} gewechselt", "tokenized_communities_top_tokens": "Top Tokens", "tokenized_communities_trending_tokens": "Trending Tokens", "tokenized_community_chart_tab": "Chart", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4292f37295..e8c6570f43 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Switched to {username}", "tokenized_communities_top_tokens": "Top Tokens", "tokenized_communities_trending_tokens": "Trending Tokens", "tokenized_community_chart_tab": "Chart", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0fae252508..a4b5fa282f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Cambiado a {username}", "tokenized_communities_top_tokens": "Principales tokens", "tokenized_communities_trending_tokens": "Tokens en tendencia", "tokenized_community_chart_tab": "Gráfico", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c63e258375..4b222ada17 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Passé à {username}", "tokenized_communities_top_tokens": "Meilleurs jetons", "tokenized_communities_trending_tokens": "Jetons tendance", "tokenized_community_chart_tab": "Graphique", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 92f71d755f..93a22cec73 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Passato a {username}", "tokenized_communities_top_tokens": "Token principali", "tokenized_communities_trending_tokens": "Token di tendenza", "tokenized_community_chart_tab": "Grafico", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index da65145ff2..7d33436eb3 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Przełączono na {username}", "tokenized_communities_top_tokens": "Najlepsze tokeny", "tokenized_communities_trending_tokens": "Popularne tokeny", "tokenized_community_chart_tab": "Wykres", diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index b10be5c7bd..8c0c32664a 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Comutat la {username}", "tokenized_communities_top_tokens": "Tokenuri de top", "tokenized_communities_trending_tokens": "Tokenuri în tendință", "tokenized_community_chart_tab": "Grafic", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 4e631d218a..1bb8f205e8 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "Переключено на {username}", "tokenized_communities_top_tokens": "Топ токенов", "tokenized_communities_trending_tokens": "Популярные токены", "tokenized_community_chart_tab": "График", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index dd9fcbee38..182d509c34 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "{username} hesabına geçildi", "tokenized_communities_top_tokens": "En iyi tokenler", "tokenized_communities_trending_tokens": "Trend tokenler", "tokenized_community_chart_tab": "Grafik", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 842af04bd3..8e661067ff 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -868,6 +868,7 @@ "token_stats_market_cap_title": "Market Cap", "token_stats_volume_description": "Volume refers to the total amount of the token that has been traded (bought and sold) over a specific period.\n\nA higher trading volume means there is strong activity and liquidity, making it easier to enter or exit positions. Low volume may indicate weak interest or higher price volatility due to limited buyers and sellers.", "token_stats_volume_title": "Volume", + "switched_to_username": "已切换到 {username}", "tokenized_communities_top_tokens": "顶级代币", "tokenized_communities_trending_tokens": "热门代币", "tokenized_community_chart_tab": "图表",