Add native deep links for users, chats, and settings (#661)
* Add copy deep link action to chat debug screen * Address deep link review feedback * Address deep link PR review follow-ups * Scale profile QR code size
This commit is contained in:
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "Gruppen-ID",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 Nachricht} other{{count} Nachrichten}}",
|
||||
"rawDebugViewCopied": "In die Zwischenablage kopiert",
|
||||
"rawDebugViewCopyDeepLink": "Deep-Link kopieren",
|
||||
"appLogsTitle": "App-Logs",
|
||||
"appLogsViewLogs": "Logs anzeigen",
|
||||
"appLogsViewLogsDescription": "Alle Logger-Ausgaben in der App anzeigen",
|
||||
|
||||
@@ -1606,6 +1606,10 @@
|
||||
"@rawDebugViewCopied": {
|
||||
"description": "Feedback text after copying debug data"
|
||||
},
|
||||
"rawDebugViewCopyDeepLink": "Copy deep link",
|
||||
"@rawDebugViewCopyDeepLink": {
|
||||
"description": "Button label for copying a debug chat deep link"
|
||||
},
|
||||
"appLogsTitle": "App Logs",
|
||||
"@appLogsTitle": {
|
||||
"description": "Screen title for the in-app log viewer"
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "ID del Grupo",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 mensaje} other{{count} mensajes}}",
|
||||
"rawDebugViewCopied": "Copiado al portapapeles",
|
||||
"rawDebugViewCopyDeepLink": "Copiar enlace profundo",
|
||||
"appLogsTitle": "Registros de la aplicación",
|
||||
"appLogsViewLogs": "Ver registros",
|
||||
"appLogsViewLogsDescription": "Ver todas las salidas del Logger en la aplicación",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "ID du Groupe",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 message} other{{count} messages}}",
|
||||
"rawDebugViewCopied": "Copié dans le presse-papiers",
|
||||
"rawDebugViewCopyDeepLink": "Copier le lien profond",
|
||||
"appLogsTitle": "Journaux de l'application",
|
||||
"appLogsViewLogs": "Voir les journaux",
|
||||
"appLogsViewLogsDescription": "Afficher toutes les sorties du journal de l'application",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "ID del Gruppo",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 messaggio} other{{count} messaggi}}",
|
||||
"rawDebugViewCopied": "Copiato negli appunti",
|
||||
"rawDebugViewCopyDeepLink": "Copia link diretto",
|
||||
"appLogsTitle": "Registri dell'app",
|
||||
"appLogsViewLogs": "Visualizza registri",
|
||||
"appLogsViewLogsDescription": "Visualizza tutti gli output del Logger nell'app",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "ID do Grupo",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 mensagem} other{{count} mensagens}}",
|
||||
"rawDebugViewCopied": "Copiado para a área de transferência",
|
||||
"rawDebugViewCopyDeepLink": "Copiar link profundo",
|
||||
"appLogsTitle": "Registros do App",
|
||||
"appLogsViewLogs": "Ver registros",
|
||||
"appLogsViewLogsDescription": "Ver todas as saídas do Logger no app",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "ID группы",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 сообщение} few{{count} сообщения} many{{count} сообщений} other{{count} сообщений}}",
|
||||
"rawDebugViewCopied": "Скопировано в буфер обмена",
|
||||
"rawDebugViewCopyDeepLink": "Скопировать диплинк",
|
||||
"appLogsTitle": "Журналы приложения",
|
||||
"appLogsViewLogs": "Просмотреть логи",
|
||||
"appLogsViewLogsDescription": "Просмотреть все записи Logger в приложении",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"rawDebugViewGroupId": "Grup Kimliği",
|
||||
"rawDebugViewMessageCount": "{count, plural, =1{1 mesaj} other{{count} mesaj}}",
|
||||
"rawDebugViewCopied": "Panoya kopyalandı",
|
||||
"rawDebugViewCopyDeepLink": "Derin bağlantıyı kopyala",
|
||||
"appLogsTitle": "Uygulama Günlükleri",
|
||||
"appLogsViewLogs": "Günlüklere bak",
|
||||
"appLogsViewLogsDescription": "Uygulamadaki tüm Logger çıktısını görüntüle",
|
||||
|
||||
@@ -366,6 +366,7 @@
|
||||
"rawDebugViewGroupId": "群组 ID",
|
||||
"rawDebugViewMessageCount": "{count, plural, =0{无消息} =1{1 条消息} other{{count} 条消息}}",
|
||||
"rawDebugViewCopied": "已复制到剪贴板",
|
||||
"rawDebugViewCopyDeepLink": "复制深层链接",
|
||||
"appLogsTitle": "应用日志",
|
||||
"appLogsViewLogs": "查看日志",
|
||||
"appLogsViewLogsDescription": "在应用内查看所有日志输出",
|
||||
|
||||
@@ -366,6 +366,7 @@
|
||||
"rawDebugViewGroupId": "群組 ID",
|
||||
"rawDebugViewMessageCount": "{count, plural, =0{沒有訊息} =1{1 則訊息} other{{count} 則訊息}}",
|
||||
"rawDebugViewCopied": "已複製到剪貼簿",
|
||||
"rawDebugViewCopyDeepLink": "複製深層連結",
|
||||
"appLogsTitle": "應用程式記錄",
|
||||
"appLogsViewLogs": "查看記錄",
|
||||
"appLogsViewLogsDescription": "在應用程式內查看所有 Logger 輸出",
|
||||
|
||||
@@ -2307,6 +2307,12 @@ abstract class AppLocalizations {
|
||||
/// **'Copied to clipboard'**
|
||||
String get rawDebugViewCopied;
|
||||
|
||||
/// Button label for copying a debug chat deep link
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy deep link'**
|
||||
String get rawDebugViewCopyDeepLink;
|
||||
|
||||
/// Screen title for the in-app log viewer
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -1301,6 +1301,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'In die Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Deep-Link kopieren';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'App-Logs';
|
||||
|
||||
|
||||
@@ -1264,6 +1264,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Copied to clipboard';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Copy deep link';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'App Logs';
|
||||
|
||||
|
||||
@@ -1282,6 +1282,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Copiado al portapapeles';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Copiar enlace profundo';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Registros de la aplicación';
|
||||
|
||||
|
||||
@@ -1280,6 +1280,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Copié dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Copier le lien profond';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Journaux de l\'application';
|
||||
|
||||
|
||||
@@ -1268,6 +1268,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Copiato negli appunti';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Copia link diretto';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Registri dell\'app';
|
||||
|
||||
|
||||
@@ -1278,6 +1278,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Copiado para a área de transferência';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Copiar link profundo';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Registros do App';
|
||||
|
||||
|
||||
@@ -1298,6 +1298,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Скопировано в буфер обмена';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Скопировать диплинк';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Журналы приложения';
|
||||
|
||||
|
||||
@@ -1265,6 +1265,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => 'Panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => 'Derin bağlantıyı kopyala';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => 'Uygulama Günlükleri';
|
||||
|
||||
|
||||
@@ -1227,6 +1227,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get rawDebugViewCopied => '已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => '复制深层链接';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => '应用日志';
|
||||
|
||||
@@ -2640,6 +2643,9 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
|
||||
@override
|
||||
String get rawDebugViewCopied => '已複製到剪貼簿';
|
||||
|
||||
@override
|
||||
String get rawDebugViewCopyDeepLink => '複製深層連結';
|
||||
|
||||
@override
|
||||
String get appLogsTitle => '應用程式記錄';
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:whitenoise/utils/deep_links.dart';
|
||||
|
||||
final deepLinkSchemeProvider = FutureProvider<String>((ref) async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return deepLinkSchemeForPackageName(info.packageName);
|
||||
});
|
||||
|
||||
String deepLinkSchemeForPackageName(String packageName) {
|
||||
if (packageName.endsWith('.staging')) return DeepLinks.stagingScheme;
|
||||
return DeepLinks.productionScheme;
|
||||
}
|
||||
+36
-1
@@ -50,6 +50,7 @@ import 'package:whitenoise/screens/switch_profile_screen.dart' show SwitchProfil
|
||||
import 'package:whitenoise/screens/user_search_screen.dart' show UserSearchScreen;
|
||||
import 'package:whitenoise/screens/user_selection_screen.dart' show UserSelectionScreen;
|
||||
import 'package:whitenoise/src/rust/api/users.dart' show User;
|
||||
import 'package:whitenoise/utils/deep_links.dart' show DeepLinks;
|
||||
import 'package:whitenoise/widgets/wn_slate_content_transition.dart' show WnSlateContentTransition;
|
||||
|
||||
final class AddRelayArgs {
|
||||
@@ -116,6 +117,12 @@ abstract final class Routes {
|
||||
observers: [routeObserver, ActiveChatRouteObserver(ref.read(activeChatProvider.notifier))],
|
||||
redirect: (context, state) {
|
||||
final pubkey = ref.read(authProvider).value;
|
||||
final deepLinkLocation = DeepLinks.parse(state.uri)?.location;
|
||||
if (deepLinkLocation != null) {
|
||||
if (pubkey == null) return _loginLocation(redirect: deepLinkLocation);
|
||||
return deepLinkLocation;
|
||||
}
|
||||
|
||||
final isOnPublicPage = _publicRoutes.contains(state.matchedLocation);
|
||||
final isAddingAccount = ref.read(isAddingAccountProvider);
|
||||
|
||||
@@ -498,6 +505,11 @@ abstract final class Routes {
|
||||
GoRouter.of(context).go(_login);
|
||||
}
|
||||
|
||||
static void goToPostAuthDestination(BuildContext context) {
|
||||
final redirect = _safeRedirect(GoRouterState.of(context).uri.queryParameters['redirect']);
|
||||
GoRouter.of(context).go(redirect ?? _chatList);
|
||||
}
|
||||
|
||||
static void pushToLogin(BuildContext context) {
|
||||
GoRouter.of(context).push(_login);
|
||||
}
|
||||
@@ -630,8 +642,9 @@ abstract final class Routes {
|
||||
required String pubkey,
|
||||
required bool isExternalSigner,
|
||||
}) {
|
||||
final redirect = _safeRedirect(GoRouterState.of(context).uri.queryParameters['redirect']);
|
||||
GoRouter.of(context).push(
|
||||
_relayResolution,
|
||||
_relayResolutionLocation(redirect: redirect),
|
||||
extra: RelayResolutionArgs(pubkey: pubkey, isExternalSigner: isExternalSigner),
|
||||
);
|
||||
}
|
||||
@@ -698,4 +711,26 @@ abstract final class Routes {
|
||||
static void pushToChatRawDebug(BuildContext context, String groupId) {
|
||||
GoRouter.of(context).pushNamed('chatRawDebug', pathParameters: {'groupId': groupId});
|
||||
}
|
||||
|
||||
static String _loginLocation({String? redirect}) {
|
||||
if (redirect == null) return _login;
|
||||
return Uri(path: _login, queryParameters: {'redirect': redirect}).toString();
|
||||
}
|
||||
|
||||
static String _relayResolutionLocation({String? redirect}) {
|
||||
if (redirect == null) return _relayResolution;
|
||||
return Uri(path: _relayResolution, queryParameters: {'redirect': redirect}).toString();
|
||||
}
|
||||
|
||||
static String? _safeRedirect(String? location) {
|
||||
if (location == null || location.isEmpty) return null;
|
||||
|
||||
final uri = Uri.tryParse(location);
|
||||
if (uri == null) return null;
|
||||
if (uri.hasScheme || uri.hasAuthority) return null;
|
||||
if (!uri.path.startsWith('/')) return null;
|
||||
if (_publicRoutes.contains(uri.path)) return null;
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:whitenoise/hooks/use_chat_messages.dart';
|
||||
import 'package:whitenoise/l10n/l10n.dart';
|
||||
import 'package:whitenoise/providers/account_pubkey_provider.dart';
|
||||
import 'package:whitenoise/providers/deep_link_provider.dart';
|
||||
import 'package:whitenoise/providers/message_debug_log_provider.dart';
|
||||
import 'package:whitenoise/routes.dart';
|
||||
import 'package:whitenoise/src/rust/api/groups.dart';
|
||||
@@ -15,8 +16,10 @@ import 'package:whitenoise/src/rust/api/media_files.dart';
|
||||
import 'package:whitenoise/src/rust/api/messages.dart';
|
||||
import 'package:whitenoise/src/rust/api/metadata.dart';
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/utils/deep_links.dart';
|
||||
import 'package:whitenoise/widgets/debug_key_value_row.dart';
|
||||
import 'package:whitenoise/widgets/debug_section_card.dart';
|
||||
import 'package:whitenoise/widgets/wn_icon.dart';
|
||||
import 'package:whitenoise/widgets/wn_pill.dart';
|
||||
import 'package:whitenoise/widgets/wn_slate.dart';
|
||||
import 'package:whitenoise/widgets/wn_slate_navigation_header.dart';
|
||||
@@ -153,7 +156,7 @@ class ChatRawDebugScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugHeader extends StatelessWidget {
|
||||
class _DebugHeader extends ConsumerWidget {
|
||||
const _DebugHeader({
|
||||
required this.groupId,
|
||||
required this.messageCount,
|
||||
@@ -167,7 +170,13 @@ class _DebugHeader extends StatelessWidget {
|
||||
final String? latestMessagePubkey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = context.colors;
|
||||
final typography = context.typographyScaled;
|
||||
final deepLinkSchemeState = ref.watch(deepLinkSchemeProvider);
|
||||
final chatDeepLink = deepLinkSchemeState.hasValue
|
||||
? DeepLinks.chatUri(groupId, scheme: deepLinkSchemeState.value!)
|
||||
: null;
|
||||
final copyText = [
|
||||
'group_id: $groupId',
|
||||
'message_count: $messageCount',
|
||||
@@ -187,6 +196,42 @@ class _DebugHeader extends StatelessWidget {
|
||||
value: groupId,
|
||||
valueKey: const Key('debug_group_id'),
|
||||
),
|
||||
if (chatDeepLink != null) ...[
|
||||
SizedBox(height: 8.h),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: const Key('debug_chat_deep_link_copy_button'),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
onTap: () => _copyDebugText(context, chatDeepLink),
|
||||
child: Ink(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.fillSecondary,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: colors.borderTertiary),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WnIcon(
|
||||
WnIcons.copy,
|
||||
size: 14.w,
|
||||
color: colors.backgroundContentSecondary,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
context.l10n.rawDebugViewCopyDeepLink,
|
||||
style: typography.medium10.copyWith(
|
||||
color: colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 4.h),
|
||||
DebugKeyValueRow(
|
||||
label: 'message_count',
|
||||
|
||||
@@ -74,7 +74,7 @@ void _handleLoginResult(
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (result.status == LoginStatus.complete) {
|
||||
Routes.goToChatList(context);
|
||||
Routes.goToPostAuthDestination(context);
|
||||
} else if (result.status == LoginStatus.needsRelayLists) {
|
||||
Routes.pushToRelayResolution(
|
||||
context,
|
||||
|
||||
@@ -69,14 +69,14 @@ class RelayResolutionScreen extends HookConsumerWidget {
|
||||
Future<void> onPublishDefaults() async {
|
||||
final success = await publishDefaults();
|
||||
if (success && context.mounted) {
|
||||
Routes.goToChatList(context);
|
||||
Routes.goToPostAuthDestination(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onTryCustomRelay() async {
|
||||
final success = await tryCustomRelay();
|
||||
if (success && context.mounted) {
|
||||
Routes.goToChatList(context);
|
||||
Routes.goToPostAuthDestination(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:gap/gap.dart' show Gap;
|
||||
import 'package:go_router/go_router.dart' show GoRouter;
|
||||
import 'package:whitenoise/l10n/l10n.dart';
|
||||
import 'package:whitenoise/routes.dart' show Routes;
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/utils/deep_links.dart' show DeepLinks;
|
||||
import 'package:whitenoise/utils/encoding.dart' show hexFromNpub;
|
||||
import 'package:whitenoise/widgets/qr_scanner.dart' show QrScanner;
|
||||
import 'package:whitenoise/widgets/wn_slate.dart';
|
||||
@@ -20,6 +22,13 @@ class ScanNpubScreen extends HookWidget {
|
||||
final showInvalidNpubError = useState(false);
|
||||
|
||||
void onBarcodeDetected(String value) {
|
||||
final deepLinkTarget = DeepLinks.parseString(value);
|
||||
if (deepLinkTarget != null) {
|
||||
Routes.goBack(context);
|
||||
GoRouter.of(context).push(deepLinkTarget.location);
|
||||
return;
|
||||
}
|
||||
|
||||
final hexPubkey = hexFromNpub(value);
|
||||
if (hexPubkey != null) {
|
||||
Routes.goBack(context);
|
||||
|
||||
@@ -7,8 +7,10 @@ import 'package:whitenoise/hooks/use_system_notice.dart';
|
||||
import 'package:whitenoise/hooks/use_user_metadata.dart';
|
||||
import 'package:whitenoise/l10n/l10n.dart';
|
||||
import 'package:whitenoise/providers/account_pubkey_provider.dart';
|
||||
import 'package:whitenoise/providers/deep_link_provider.dart';
|
||||
import 'package:whitenoise/routes.dart';
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/utils/deep_links.dart';
|
||||
import 'package:whitenoise/utils/formatting.dart';
|
||||
import 'package:whitenoise/utils/metadata.dart';
|
||||
import 'package:whitenoise/widgets/wn_avatar.dart';
|
||||
@@ -29,6 +31,10 @@ class ShareProfileScreen extends HookConsumerWidget {
|
||||
final pubkey = ref.watch(accountPubkeyProvider);
|
||||
final metadataSnapshot = useUserMetadata(context, pubkey);
|
||||
final npub = npubFromHex(pubkey);
|
||||
final deepLinkSchemeState = ref.watch(deepLinkSchemeProvider);
|
||||
final profileDeepLink = npub != null && deepLinkSchemeState.hasValue
|
||||
? DeepLinks.userUri(npub, scheme: deepLinkSchemeState.value!)
|
||||
: null;
|
||||
final (:noticeMessage, :noticeType, :showSuccessNotice, :showErrorNotice, :dismissNotice) =
|
||||
useSystemNotice();
|
||||
|
||||
@@ -84,30 +90,33 @@ class ShareProfileScreen extends HookConsumerWidget {
|
||||
snapToWords: true,
|
||||
),
|
||||
),
|
||||
Gap(36.h),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: SizedBox.square(
|
||||
dimension: 256.w,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: QrImageView(
|
||||
data: npub,
|
||||
size: 98,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: colors.backgroundContentPrimary,
|
||||
if (profileDeepLink != null) ...[
|
||||
Gap(36.h),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: SizedBox.square(
|
||||
dimension: 256.w,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: QrImageView(
|
||||
key: ValueKey<String>(profileDeepLink),
|
||||
data: profileDeepLink,
|
||||
size: 98.w,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
] else
|
||||
Gap(32.h),
|
||||
Gap(12.h),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:whitenoise/utils/encoding.dart' show hexFromNpub;
|
||||
|
||||
enum DeepLinkTargetType { user, chat, settings }
|
||||
|
||||
class DeepLinkTarget {
|
||||
const DeepLinkTarget({
|
||||
required this.type,
|
||||
required this.location,
|
||||
});
|
||||
|
||||
final DeepLinkTargetType type;
|
||||
final String location;
|
||||
}
|
||||
|
||||
abstract final class DeepLinks {
|
||||
static const productionScheme = 'whitenoise';
|
||||
static const stagingScheme = 'whitenoise-staging';
|
||||
|
||||
static const _settingsLocations = {
|
||||
'settings': '/settings',
|
||||
'settings/share-profile': '/share-profile',
|
||||
'settings/switch-profile': '/switch-profile',
|
||||
'settings/edit-profile': '/edit-profile',
|
||||
'settings/profile-keys': '/profile-keys',
|
||||
'settings/network': '/network',
|
||||
'settings/privacy-security': '/privacy-security',
|
||||
'settings/appearance': '/appearance',
|
||||
'settings/notifications': '/notification-settings',
|
||||
'settings/report-bug': '/report-bug',
|
||||
'settings/donate': '/donate',
|
||||
'settings/developer': '/developer-settings',
|
||||
'settings/developer/key-packages': '/key-package-management',
|
||||
'settings/developer/relay-state': '/relay-control-state',
|
||||
'settings/developer/app-logs': '/app-logs',
|
||||
};
|
||||
|
||||
static String userUri(String npub, {String scheme = productionScheme}) {
|
||||
return Uri(scheme: scheme, host: 'user', pathSegments: [npub]).toString();
|
||||
}
|
||||
|
||||
static String chatUri(String groupId, {String scheme = productionScheme}) {
|
||||
return Uri(scheme: scheme, host: 'chat', pathSegments: [groupId]).toString();
|
||||
}
|
||||
|
||||
static DeepLinkTarget? parse(Uri uri) {
|
||||
if (!_isSupportedScheme(uri.scheme)) return null;
|
||||
|
||||
final segments = _segments(uri);
|
||||
if (segments.isEmpty) return null;
|
||||
|
||||
return switch (segments.first.toLowerCase()) {
|
||||
'user' => _parseUser(segments),
|
||||
'chat' => _parseChat(segments),
|
||||
'settings' => _parseSettings(segments),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
static DeepLinkTarget? parseString(String value) {
|
||||
final uri = Uri.tryParse(value.trim());
|
||||
if (uri == null) return null;
|
||||
return parse(uri);
|
||||
}
|
||||
|
||||
static bool _isSupportedScheme(String scheme) {
|
||||
return scheme == productionScheme || scheme == stagingScheme;
|
||||
}
|
||||
|
||||
static List<String> _segments(Uri uri) {
|
||||
return [
|
||||
if (uri.host.isNotEmpty) uri.host,
|
||||
...uri.pathSegments,
|
||||
].where((segment) => segment.isNotEmpty).toList(growable: false);
|
||||
}
|
||||
|
||||
static DeepLinkTarget? _parseUser(List<String> segments) {
|
||||
if (segments.length != 2) return null;
|
||||
|
||||
final pubkey = hexFromNpub(segments[1]);
|
||||
if (pubkey == null) return null;
|
||||
|
||||
return DeepLinkTarget(
|
||||
type: DeepLinkTargetType.user,
|
||||
location: '/start-chat/${Uri.encodeComponent(pubkey)}',
|
||||
);
|
||||
}
|
||||
|
||||
static DeepLinkTarget? _parseChat(List<String> segments) {
|
||||
if (segments.length != 2) return null;
|
||||
|
||||
final groupId = segments[1];
|
||||
if (groupId.isEmpty) return null;
|
||||
|
||||
return DeepLinkTarget(
|
||||
type: DeepLinkTargetType.chat,
|
||||
location: '/chats/${Uri.encodeComponent(groupId)}',
|
||||
);
|
||||
}
|
||||
|
||||
static DeepLinkTarget? _parseSettings(List<String> segments) {
|
||||
final location = _settingsLocations[segments.join('/')];
|
||||
if (location == null) return null;
|
||||
|
||||
return DeepLinkTarget(
|
||||
type: DeepLinkTargetType.settings,
|
||||
location: location,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user