diff --git a/lib/app/components/gradient_border_painter/gradient_border_painter.dart b/lib/app/components/gradient_border_painter/gradient_border_painter.dart index bb983ed1d2..a530b7021c 100644 --- a/lib/app/components/gradient_border_painter/gradient_border_painter.dart +++ b/lib/app/components/gradient_border_painter/gradient_border_painter.dart @@ -7,22 +7,35 @@ class GradientBorderPainter extends CustomPainter { required this.gradient, this.strokeWidth = 2, this.cornerRadius = 12, + this.backgroundColor, }); final Gradient gradient; final double strokeWidth; final double cornerRadius; + final Color? backgroundColor; @override void paint(Canvas canvas, Size size) { final rect = Rect.fromLTWH(0, 0, size.width, size.height); - final paint = Paint() + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius)); + + // Draw background if color is provided + if (backgroundColor != null) { + final backgroundPaint = Paint() + ..color = backgroundColor! + ..style = PaintingStyle.fill; + + canvas.drawRRect(rrect, backgroundPaint); + } + + // Draw gradient border + final borderPaint = Paint() ..shader = gradient.createShader(rect) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; - final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius)); - canvas.drawRRect(rrect, paint); + canvas.drawRRect(rrect, borderPaint); } @override diff --git a/lib/app/features/ion_connect/providers/events_management_service.r.dart b/lib/app/features/ion_connect/providers/events_management_service.r.dart index 89b0d09aa9..5c6168e0e5 100644 --- a/lib/app/features/ion_connect/providers/events_management_service.r.dart +++ b/lib/app/features/ion_connect/providers/events_management_service.r.dart @@ -16,6 +16,7 @@ import 'package:ion/app/features/feed/notifications/providers/notifications/repo import 'package:ion/app/features/feed/notifications/providers/notifications/token_launch_notification_handler.r.dart'; import 'package:ion/app/features/ion_connect/ion_connect.dart'; import 'package:ion/app/features/ion_connect/model/global_subscription_event_handler.dart'; +import 'package:ion/app/features/tokenized_communities/providers/community_token_definition_handler.r.dart'; import 'package:ion/app/features/user/providers/badge_award_handler.r.dart'; import 'package:ion/app/features/user/providers/user_delegation_handler.r.dart'; import 'package:ion/app/services/logger/logger.dart'; @@ -37,6 +38,7 @@ Future eventsManagementService(Ref ref) async { ref.watch(badgeAwardHandlerProvider), ref.watch(userDelegationHandlerProvider), ref.watch(tokenLaunchNotificationHandlerProvider), + ref.watch(communityTokenDefinitionHandlerProvider), ]; final manager = EventsManagementService(handlers); diff --git a/lib/app/features/tokenized_communities/providers/community_token_definition_handler.r.dart b/lib/app/features/tokenized_communities/providers/community_token_definition_handler.r.dart new file mode 100644 index 0000000000..15f2961600 --- /dev/null +++ b/lib/app/features/tokenized_communities/providers/community_token_definition_handler.r.dart @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.m.dart'; +import 'package:ion/app/features/ion_connect/ion_connect.dart'; +import 'package:ion/app/features/ion_connect/model/event_reference.f.dart'; +import 'package:ion/app/features/ion_connect/model/global_subscription_event_handler.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.r.dart'; +import 'package:ion/app/features/tokenized_communities/models/entities/community_token_definition.f.dart'; +import 'package:ion/app/features/tokenized_communities/views/creator_token_is_live_dialog.dart'; +import 'package:ion/app/features/user/model/user_metadata.f.dart'; +import 'package:ion/app/services/storage/local_storage.r.dart'; +import 'package:ion/app/services/ui_event_queue/ui_event_queue_notifier.r.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'community_token_definition_handler.r.g.dart'; + +class CommunityTokenDefinitionHandler extends GlobalSubscriptionEventHandler { + CommunityTokenDefinitionHandler({ + required this.localStorage, + required this.ionConnectCache, + required this.uiEventQueueCallback, + required this.currentUserMasterPubkey, + }); + + final LocalStorage localStorage; + final IonConnectCache ionConnectCache; + final String? currentUserMasterPubkey; + final void Function(ReplaceableEventReference tokenDefinitionEventReference) uiEventQueueCallback; + + String get localStorageKey => 'creator_token_is_live_dialog_shown_$currentUserMasterPubkey'; + + @override + bool canHandle(EventMessage eventMessage) { + return eventMessage.kind == CommunityTokenDefinitionEntity.kind; + } + + @override + Future handle(EventMessage eventMessage) async { + final entity = CommunityTokenDefinitionEntity.fromEventMessage(eventMessage); + + if (entity + case CommunityTokenDefinitionEntity( + data: CommunityTokenDefinitionIon( + eventReference: ReplaceableEventReference( + masterPubkey: final originalEventMasterPubkey, + kind: UserMetadataEntity.kind + ), + type: CommunityTokenDefinitionIonType.firstBuyAction + ) + ) + when originalEventMasterPubkey == currentUserMasterPubkey && + !(localStorage.getBool(localStorageKey) ?? false)) { + uiEventQueueCallback(entity.toEventReference()); + await localStorage.setBool(key: localStorageKey, value: true); + } + + await ionConnectCache.cache(entity); + } +} + +@riverpod +CommunityTokenDefinitionHandler communityTokenDefinitionHandler(Ref ref) { + final localStorage = ref.watch(localStorageProvider); + final cache = ref.watch(ionConnectCacheProvider.notifier); + final currentUserMasterPubkey = ref.watch(currentPubkeySelectorProvider); + + void uiEventQueueCallback(ReplaceableEventReference tokenDefinitionEventReference) { + ref + .read(uiEventQueueNotifierProvider.notifier) + .emit(CreatorTokenIsLiveDialogEvent(tokenDefinitionEventReference)); + } + + return CommunityTokenDefinitionHandler( + ionConnectCache: cache, + localStorage: localStorage, + uiEventQueueCallback: uiEventQueueCallback, + currentUserMasterPubkey: currentUserMasterPubkey, + ); +} diff --git a/lib/app/features/tokenized_communities/views/creator_token_is_live_dialog.dart b/lib/app/features/tokenized_communities/views/creator_token_is_live_dialog.dart new file mode 100644 index 0000000000..db2a3c4f58 --- /dev/null +++ b/lib/app/features/tokenized_communities/views/creator_token_is_live_dialog.dart @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/components/avatar/avatar.dart'; +import 'package:ion/app/components/button/button.dart'; +import 'package:ion/app/components/gradient_border_painter/gradient_border_painter.dart'; +import 'package:ion/app/components/progress_bar/ion_loading_indicator.dart'; +import 'package:ion/app/components/screen_offset/screen_bottom_offset.dart'; +import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.m.dart'; +import 'package:ion/app/features/feed/views/pages/feed_page/components/stories/mock.dart'; +import 'package:ion/app/features/ion_connect/model/event_reference.f.dart'; +import 'package:ion/app/features/tokenized_communities/providers/token_market_info_provider.r.dart'; +import 'package:ion/app/features/user/model/profile_mode.dart'; +import 'package:ion/app/features/user/pages/profile_page/components/profile_background.dart'; +import 'package:ion/app/features/user/pages/profile_page/components/profile_details/user_name_tile/user_name_tile.dart'; +import 'package:ion/app/features/user/providers/user_metadata_provider.r.dart'; +import 'package:ion/app/hooks/use_avatar_colors.dart'; +import 'package:ion/app/router/app_routes.gr.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; +import 'package:ion/app/router/utils/show_simple_bottom_sheet.dart'; +import 'package:ion/app/services/logger/logger.dart'; +import 'package:ion/app/services/ui_event_queue/ui_event_queue_notifier.r.dart'; +import 'package:ion/generated/assets.gen.dart'; + +class CreatorTokenIsLiveDialogEvent extends UiEvent { + CreatorTokenIsLiveDialogEvent(this.tokenDefinitionEventReference); + + static bool shown = false; + final ReplaceableEventReference tokenDefinitionEventReference; + + @override + void performAction(BuildContext context) { + if (!shown) { + shown = true; + showSimpleBottomSheet( + context: context, + backgroundColor: context.theme.appColors.forest, + child: + CreatorTokenIsLiveDialog(tokenDefinitionEventReference: tokenDefinitionEventReference), + ).whenComplete(() => shown = false); + } + } +} + +class CreatorTokenIsLiveDialog extends HookConsumerWidget { + const CreatorTokenIsLiveDialog({required this.tokenDefinitionEventReference, super.key}); + + final ReplaceableEventReference tokenDefinitionEventReference; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final avatarUrl = ref.watch(currentUserMetadataProvider).value?.data.avatarUrl; + final imageColors = useImageColors(avatarUrl); + + return ProfileGradientBackground( + colors: imageColors ?? useAvatarFallbackColors, + disableDarkGradient: false, + child: _ContentState(tokenDefinitionEventReference), + ); + } +} + +class _ContentState extends HookConsumerWidget { + const _ContentState(this.tokenDefinitionEventReference); + + final ReplaceableEventReference tokenDefinitionEventReference; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isLoading = useState(false); + final currentUserMasterPubkey = ref.watch(currentPubkeySelectorProvider) ?? ''; + final userMetadata = ref.watch(currentUserMetadataProvider).valueOrNull; + final avatarUrl = userMetadata?.data.avatarUrl ?? ''; + + final eventReferenceString = userMetadata?.toEventReference().toString(); + + final token = eventReferenceString != null + ? ref.watch(tokenMarketInfoProvider(eventReferenceString)).valueOrNull + : null; + + return Stack( + children: [ + PositionedDirectional( + bottom: 0, + start: 0, + end: 0, + child: Assets.images.tokenizedCommunities.creatorMonetizationLiveRays + .iconWithDimensions(width: 461.s, height: 461.s), + ), + PositionedDirectional( + end: 8, + child: NavigationCloseButton(color: context.theme.appColors.onPrimaryAccent), + ), + ScreenSideOffset.medium( + child: Column( + children: [ + SizedBox(height: 30.0.s), + CustomPaint( + painter: GradientBorderPainter( + strokeWidth: 2.0.s, + cornerRadius: 26.0.s, + gradient: storyBorderGradients[3], + backgroundColor: context.theme.appColors.forest.withAlpha(125), + ), + child: Padding( + padding: EdgeInsets.all(18.0.s), + child: Avatar( + size: 64.0.s, + fit: BoxFit.cover, + imageUrl: avatarUrl, + borderRadius: BorderRadius.all(Radius.circular(16.0.s)), + ), + ), + ), + SizedBox(height: 8.0.s), + UserNameTile( + showProfileTokenPrice: true, + profileMode: ProfileMode.dark, + pubkey: currentUserMasterPubkey, + priceUsd: token?.marketData.priceUSD, + ), + SizedBox(height: 18.0.s), + Text( + context.i18n.tokenized_community_creator_token_live_title, + textAlign: TextAlign.center, + style: context.theme.appTextThemes.title + .copyWith(color: context.theme.appColors.onPrimaryAccent), + ), + SizedBox(height: 8.0.s), + Text( + context.i18n.tokenized_community_creator_token_live_subtitle, + textAlign: TextAlign.center, + style: context.theme.appTextThemes.body2 + .copyWith(color: context.theme.appColors.secondaryBackground), + ), + SizedBox(height: 24.0.s), + Button( + disabled: isLoading.value, + label: Text(context.i18n.button_share), + minimumSize: Size(double.infinity, 56.0.s), + trailingIcon: + isLoading.value ? const IONLoadingIndicator() : const SizedBox.shrink(), + onPressed: token != null + ? () async { + isLoading.value = true; + try { + if (context.mounted) { + context.pop(); + + await ShareViaMessageModalRoute( + eventReference: tokenDefinitionEventReference.encode(), + ).push(context); + } + } catch (e, st) { + Logger.error(e, stackTrace: st); + } finally { + isLoading.value = false; + } + } + : null, + ), + ScreenBottomOffset(), + ], + ), + ), + ], + ); + } +} diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 39f122efdc..0a2632f463 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -928,6 +928,9 @@ "tokenized_communities_trending_tokens": "التوكنات الرائجة", "tokenized_community_bonding_curve": "منحنى الترابط", "tokenized_community_chart_tab": "المخطط", + "tokenized_community_creator_token_live_title": "توكن المنشئ مباشر الآن!", + "tokenized_community_creator_token_live_subtitle": "تهانينا، توكن المنشئ الخاص بك مباشر الآن ومتاح للجميع للتداول", + "tokenized_community_comments_tab": "التعليقات", "tokenized_community_comments_empty": "كن أول من ينضم إلى المحادثة", "tokenized_community_comments_tab": "التعليقات", "tokenized_community_holders_tab": "الحائزون", diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 49b498f028..3503613724 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Графика", "tokenized_community_comments_empty": "Бъдете първият, който се присъединява към разговора", "tokenized_community_comments_tab": "Коментари", + "tokenized_community_creator_token_live_title": "Токенът на създателя е АКТИВЕН!", + "tokenized_community_creator_token_live_subtitle": "Поздравления, вашият токен на създателя вече е активен и достъпен за търговия от всички", "tokenized_community_holders_tab": "Притежатели", "token_comment_holders_only": "Коментарите са достъпни само за притежатели на токени.", "tokenized_community_not_available_description": "Създаването на токени не е налично за вече създадени публикации.", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 652ab28cf4..fdae60d6a1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Chart", "tokenized_community_comments_empty": "Sei der Erste, der sich an der Unterhaltung beteiligt", "tokenized_community_comments_tab": "Kommentare", + "tokenized_community_creator_token_live_title": "Creator-Token ist LIVE!", + "tokenized_community_creator_token_live_subtitle": "Herzlichen Glückwunsch, Ihr Creator-Token ist jetzt live und für jeden handelbar", "tokenized_community_holders_tab": "Inhaber", "token_comment_holders_only": "Kommentare sind nur für Token-Inhaber verfügbar.", "tokenized_community_not_available_description": "Die Tokenerstellung ist für zuvor erstellte Beiträge nicht verfügbar.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 41a09253e9..f23ccfb21f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -925,6 +925,8 @@ "tokenized_community_token_twitter": "X token", "tokenized_community_token_content": "Content token", "tokenized_community_trades_tab": "Trades", + "tokenized_community_creator_token_live_title": "Creator token is LIVE!", + "tokenized_community_creator_token_live_subtitle": "Congratulations, your creator token is now live and available for everyone to trade it", "toolbar_link_placeholder": "https://example.com", "toolbar_link_title": "Add a link", "top_holders_empty": "Become the first holder", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e0baa96510..5beee78851 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Gráfico", "tokenized_community_comments_empty": "Sé el primero en unirse a la conversación", "tokenized_community_comments_tab": "Comentarios", + "tokenized_community_creator_token_live_title": "¡El token del creador está EN VIVO!", + "tokenized_community_creator_token_live_subtitle": "Felicidades, tu token de creador ya está en vivo y disponible para que todos lo intercambien", "tokenized_community_holders_tab": "Tenedores", "token_comment_holders_only": "Los comentarios están disponibles solo para los tenedores de tokens.", "tokenized_community_not_available_description": "La creación de tokens no está disponible para publicaciones creadas previamente.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1797f6e257..733db9a72e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Graphique", "tokenized_community_comments_empty": "Soyez le premier à rejoindre la conversation", "tokenized_community_comments_tab": "Commentaires", + "tokenized_community_creator_token_live_title": "Le token du créateur est EN DIRECT !", + "tokenized_community_creator_token_live_subtitle": "Félicitations, votre token de créateur est maintenant en direct et disponible pour que tout le monde puisse l'échanger", "tokenized_community_holders_tab": "Détenteurs", "token_comment_holders_only": "Les commentaires sont disponibles uniquement pour les détenteurs de tokens.", "tokenized_community_not_available_description": "La création de tokens n'est pas disponible pour les publications déjà créées.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4de4e6be15..38528b9ee8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Grafico", "tokenized_community_comments_empty": "Sii il primo a unirti alla conversazione", "tokenized_community_comments_tab": "Commenti", + "tokenized_community_creator_token_live_title": "Il token del creatore è LIVE!", + "tokenized_community_creator_token_live_subtitle": "Congratulazioni, il tuo token creatore è ora attivo e disponibile per tutti per scambiarlo", "tokenized_community_holders_tab": "Detentori", "token_comment_holders_only": "I commenti sono disponibili solo per i detentori di token.", "tokenized_community_not_available_description": "La creazione di token non è disponibile per i post creati in precedenza.", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index f8f117583b..a61df767a7 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Wykres", "tokenized_community_comments_empty": "Bądź pierwszym, który dołączy do rozmowy", "tokenized_community_comments_tab": "Komentarze", + "tokenized_community_creator_token_live_title": "Token twórcy jest NA ŻYWO!", + "tokenized_community_creator_token_live_subtitle": "Gratulacje, Twój token twórcy jest teraz aktywny i dostępny dla wszystkich do handlu", "tokenized_community_holders_tab": "Posiadacze", "token_comment_holders_only": "Komentarze są dostępne tylko dla posiadaczy tokenów.", "tokenized_community_not_available_description": "Tworzenie tokenów nie jest dostępne dla wcześniej utworzonych postów.", diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 31aae1238b..00eaf66037 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Grafic", "tokenized_community_comments_empty": "Fii primul care se alătură conversației", "tokenized_community_comments_tab": "Comentarii", + "tokenized_community_creator_token_live_title": "Tokenul de creator este LIVE!", + "tokenized_community_creator_token_live_subtitle": "Felicitări, tokenul tău de creator este acum activ și disponibil pentru toată lumea să îl tranzacționeze", "tokenized_community_holders_tab": "Deținători", "token_comment_holders_only": "Comentariile sunt disponibile doar pentru deținătorii de tokenuri.", "tokenized_community_not_available_description": "Crearea de tokenuri nu este disponibilă pentru postările create anterior.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2827dbc256..e3ab61dfd8 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "График", "tokenized_community_comments_empty": "Станьте первым, кто присоединится к разговору", "tokenized_community_comments_tab": "Комментарии", + "tokenized_community_creator_token_live_title": "Токен создателя АКТИВЕН!", + "tokenized_community_creator_token_live_subtitle": "Поздравляем, ваш токен создателя теперь активен и доступен всем для торговли", "tokenized_community_holders_tab": "Держатели", "token_comment_holders_only": "Комментарии доступны только держателям токенов.", "tokenized_community_not_available_description": "Создание токенов недоступно для ранее созданных публикаций.", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 46d777121f..d3f5dfc169 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "Grafik", "tokenized_community_comments_empty": "Sohbete katılan ilk kişi olun", "tokenized_community_comments_tab": "Yorumlar", + "tokenized_community_creator_token_live_title": "İçerik üreticisi tokeni YAYINDA!", + "tokenized_community_creator_token_live_subtitle": "Tebrikler, içerik üreticisi tokeniniz artık yayında ve herkesin işlem yapması için mevcut", "tokenized_community_holders_tab": "Sahipler", "token_comment_holders_only": "Yorumlar yalnızca token sahipleri için kullanılabilir.", "tokenized_community_not_available_description": "Önceden oluşturulmuş gönderiler için token oluşturma kullanılamaz.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d022aa09b8..2e74a2a6d6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -917,6 +917,8 @@ "tokenized_community_chart_tab": "图表", "tokenized_community_comments_empty": "成为第一个加入对话的人", "tokenized_community_comments_tab": "评论", + "tokenized_community_creator_token_live_title": "创作者代币已上线!", + "tokenized_community_creator_token_live_subtitle": "恭喜,您的创作者代币现已上线,所有人都可以交易", "tokenized_community_holders_tab": "持有者", "token_comment_holders_only": "评论仅对代币持有者开放。", "tokenized_community_not_available_description": "已创建的帖子无法创建代币。",