From 5ca80b6d1aa362fb82d8c664cb9d9e2fc0fb3ff8 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 3 Dec 2024 15:12:20 +0100 Subject: [PATCH] Implement primal legend avatar customization across the app (#239) --------- Co-authored-by: Aleksandar Ilic --- app/detekt-baseline.xml | 5 +- .../47.json | 1789 +++++++++++++++++ .../articles/feed/ui/FeedArticleListItem.kt | 11 +- .../android/articles/feed/ui/FeedArticleUi.kt | 11 +- .../android/articles/reads/ReadsScreen.kt | 8 +- .../articles/reads/ReadsScreenContract.kt | 4 +- .../android/articles/reads/ReadsViewModel.kt | 3 +- .../primal/android/auth/login/LoginScreen.kt | 7 +- .../ui/OnboardingProfileFollowsScreen.kt | 4 +- .../compose/AvatarThumbnailListItemImage.kt | 80 +- .../core/compose/AvatarThumbnailsRow.kt | 9 +- .../android/core/compose/PrimalTopAppBar.kt | 13 +- .../compose/profile/model/ProfileDetailsUi.kt | 11 +- .../profile/model/UserProfileItemUi.kt | 5 + .../compose/zaps/ArticleTopZapsSection.kt | 15 +- .../android/core/compose/zaps/EventZapItem.kt | 5 +- .../compose/zaps/ZappersAvatarThumbnailRow.kt | 15 +- .../net/primal/android/db/PrimalDatabase.kt | 2 +- .../net/primal/android/drawer/PrimalDrawer.kt | 28 +- .../android/drawer/PrimalDrawerContract.kt | 6 +- .../android/drawer/PrimalDrawerViewModel.kt | 6 +- .../android/editor/NoteEditorContract.kt | 2 + .../android/editor/NoteEditorViewModel.kt | 3 + .../android/editor/ui/NoteEditorScreen.kt | 6 +- .../explore/asearch/AdvancedSearchScreen.kt | 1 + .../explore/asearch/ui/MultipleUserPicker.kt | 5 +- .../explore/home/ExploreHomeContract.kt | 4 +- .../android/explore/home/ExploreHomeScreen.kt | 17 +- .../explore/home/ExploreHomeViewModel.kt | 3 +- .../explore/home/people/ExplorePeople.kt | 18 +- .../android/explore/home/zaps/ExploreZaps.kt | 13 +- .../explore/search/ui/UserProfileListItem.kt | 57 +- .../android/feeds/dvm/ui/DvmFeedListItem.kt | 5 +- .../primal/android/feeds/dvm/ui/DvmFeedUi.kt | 2 + .../feeds/repository/DvmFeedListHandler.kt | 11 +- .../android/messages/chat/ChatScreen.kt | 5 +- .../MessageConversationListScreen.kt | 6 +- .../MessageConversationListViewModel.kt | 2 + .../model/MessageConversationUi.kt | 2 + .../android/notes/feed/list/NoteFeedList.kt | 1 + .../notes/feed/list/NoteFeedViewModel.kt | 17 +- .../android/notes/feed/model/FeedPostUi.kt | 11 +- .../notes/feed/model/FeedPostsSyncStats.kt | 2 + .../android/notes/feed/note/FeedNoteCard.kt | 16 +- .../android/notes/feed/note/ui/NoteHeader.kt | 95 +- .../notes/feed/note/ui/ReferencedNoteCard.kt | 1 + .../android/notes/home/HomeFeedContract.kt | 4 +- .../android/notes/home/HomeFeedScreen.kt | 8 +- .../android/notes/home/HomeFeedViewModel.kt | 3 +- .../list/NotificationsContract.kt | 4 +- .../notifications/list/NotificationsScreen.kt | 2 +- .../list/NotificationsViewModel.kt | 4 +- .../list/ui/NotificationListItem.kt | 8 + .../notifications/list/ui/NotificationUi.kt | 2 + .../buying/name/PremiumPrimalNameStage.kt | 4 +- .../buying/purchase/PremiumPurchaseStage.kt | 5 +- .../premium/home/PremiumHomeContract.kt | 6 +- .../android/premium/home/PremiumHomeScreen.kt | 17 +- .../premium/home/PremiumHomeViewModel.kt | 7 +- .../premium/legend/LegendaryCustomization.kt | 16 + .../become/amount/BecomeLegendAmountStage.kt | 4 +- .../LegendaryProfileCustomizationContract.kt | 5 +- .../LegendaryProfileCustomizationScreen.kt | 45 +- .../LegendaryProfileCustomizationViewModel.kt | 7 +- .../nameChange/ConfirmNameChangeStage.kt | 12 +- .../PremiumChangePrimalNameContract.kt | 2 + .../PremiumChangePrimalNameScreen.kt | 1 + .../PremiumChangePrimalNameViewModel.kt | 2 + .../details/ui/ProfileDetailsHeader.kt | 5 +- .../profile/details/ui/ProfileTopCoverBar.kt | 27 +- .../profile/qr/ui/ProfileQrCodeViewer.kt | 7 +- .../settings/keys/KeysSettingsContract.kt | 2 + .../settings/keys/KeysSettingsScreen.kt | 15 +- .../settings/keys/KeysSettingsViewModel.kt | 2 + .../muted/list/MutedSettingsScreen.kt | 5 +- .../muted/list/MutedSettingsViewModel.kt | 2 + .../settings/muted/list/model/MutedUserUi.kt | 2 + .../net/primal/android/stats/db/EventZap.kt | 2 + .../ui/GenericReactionsLazyColumn.kt | 5 +- .../reactions/ui/ReactionsZapsLazyColumn.kt | 5 +- .../android/stats/ui/EventZapUiModel.kt | 4 + .../articles/details/ArticleDetailsScreen.kt | 4 +- .../articles/details/ui/ArticleAuthorRow.kt | 42 +- .../articles/details/ui/ArticleDetailsUi.kt | 11 +- .../android/user/accounts/UserAccountExt.kt | 4 +- .../user/accounts/UserAccountFetcher.kt | 5 +- .../primal/android/user/domain/UserAccount.kt | 6 +- .../dashboard/WalletDashboardContract.kt | 4 +- .../wallet/dashboard/WalletDashboardScreen.kt | 2 +- .../dashboard/WalletDashboardViewModel.kt | 4 +- .../details/TransactionDetailDataUi.kt | 2 + .../details/TransactionDetailsScreen.kt | 3 +- .../details/TransactionDetailsViewModel.kt | 2 + .../transactions/list/TransactionListItem.kt | 8 +- .../list/TransactionListItemDataUi.kt | 2 + .../send/create/CreateTransactionContract.kt | 2 + .../send/create/CreateTransactionViewModel.kt | 3 + .../send/create/ui/TransactionEditor.kt | 5 +- 98 files changed, 2339 insertions(+), 363 deletions(-) create mode 100644 app/schemas/net.primal.android.db.PrimalDatabase/47.json create mode 100644 app/src/main/kotlin/net/primal/android/premium/legend/LegendaryCustomization.kt diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 59e18a72d..c29699e2b 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -62,7 +62,6 @@ LongMethod:NoteDropdownMenu.kt$@Composable fun NoteDropdownMenuIcon( modifier: Modifier, noteId: String, noteContent: String, noteRawData: String, authorId: String, isBookmarked: Boolean, enabled: Boolean = true, onBookmarkClick: (() -> Unit)? = null, onMuteUserClick: (() -> Unit)? = null, onReportContentClick: (() -> Unit)? = null, ) LongMethod:NoteEditorScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun NoteEditorBox( state: NoteEditorContract.UiState, eventPublisher: (UiEvent) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues, ) LongMethod:NoteFeedLazyColumn.kt$@ExperimentalMaterial3Api @ExperimentalFoundationApi @Composable fun NoteFeedLazyColumn( modifier: Modifier = Modifier, pagingItems: LazyPagingItems<FeedPostUi>, listState: LazyListState, showPaywall: Boolean, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, showTopZaps: Boolean = false, shouldShowLoadingState: Boolean = true, shouldShowNoContentState: Boolean = true, showReplyTo: Boolean = true, noContentVerticalArrangement: Arrangement.Vertical = Arrangement.Center, noContentPaddingValues: PaddingValues = PaddingValues(all = 0.dp), noContentText: String = stringResource(id = R.string.feed_no_content), contentPadding: PaddingValues = PaddingValues(all = 0.dp), header: @Composable (LazyItemScope.() -> Unit)? = null, stickyHeader: @Composable (LazyItemScope.() -> Unit)? = null, onUiError: ((UiError) -> Unit)? = null, ) - LongMethod:NoteHeader.kt$@Composable fun FeedNoteHeader( modifier: Modifier = Modifier, authorDisplayName: String, singleLine: Boolean = false, postTimestamp: Instant? = null, authorAvatarSize: Dp = 42.dp, authorAvatarVisible: Boolean = true, authorAvatarCdnImage: CdnImage? = null, authorInternetIdentifier: String? = null, authorLegendAvatarGlow: Boolean = false, authorLegendCustomBadge: Boolean = false, authorLegendaryStyle: LegendaryStyle? = null, replyToAuthor: String? = null, label: String? = authorInternetIdentifier, labelStyle: TextStyle? = null, onAuthorAvatarClick: (() -> Unit)? = null, ) LongMethod:NoteVideoLinkPreview.kt$@Composable fun NoteVideoLinkPreview( url: String, title: String?, thumbnailUrl: String?, thumbnailImageSize: DpSize, type: NoteAttachmentType, onClick: (() -> Unit)? = null, ) LongMethod:NotificationListItem.kt$@Composable private fun NotificationType.toSuffixText(usersZappedCount: Int = 0, totalSatsZapped: String? = null): String LongMethod:NotificationsScreen.kt$@ExperimentalMaterial3Api @Composable private fun NotificationsList( state: NotificationsContract.UiState, noteState: NoteContract.UiState, listState: LazyListState, seenPagingItems: LazyPagingItems<NotificationUi>, paddingValues: PaddingValues, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onPostLikeClick: (FeedPostUi) -> Unit, onRepostClick: (FeedPostUi) -> Unit, onZapClick: (FeedPostUi, ULong?, String?) -> Unit, onPostQuoteClick: (FeedPostUi) -> Unit, onBookmarkClick: (FeedPostUi) -> Unit, ) @@ -76,11 +75,11 @@ LongMethod:PremiumOrderHistoryScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun PremiumOrderHistoryScreen( state: PremiumOrderHistoryContract.UiState, eventPublisher: (PremiumOrderHistoryContract.UiEvent) -> Unit, onExtendSubscription: (primalName: String) -> Unit, onClose: () -> Unit, ) LongMethod:PremiumPurchaseStage.kt$@ExperimentalMaterial3Api @Composable fun PremiumPurchaseStage( state: PremiumBuyingContract.UiState, onBack: () -> Unit, onLearnMoreClick: () -> Unit, eventPublisher: (PremiumBuyingContract.UiEvent) -> Unit, ) LongMethod:PrimalAppNavigation.kt$@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun SharedTransitionScope.PrimalAppNavigation() - LongMethod:PrimalDrawer.kt$@Composable private fun DrawerHeader( userAccount: UserAccount?, customBadge: Boolean, avatarGlow: Boolean, legendaryStyle: LegendaryStyle?, onQrCodeClick: () -> Unit, ) + LongMethod:PrimalDrawer.kt$@Composable private fun DrawerHeader( userAccount: UserAccount?, legendaryCustomization: LegendaryCustomization?, onQrCodeClick: () -> Unit, ) LongMethod:PrimalDrawerScaffold.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PrimalDrawerScaffold( modifier: Modifier = Modifier, drawerState: DrawerState, activeDestination: PrimalTopLevelDestination, onPrimaryDestinationChanged: (PrimalTopLevelDestination) -> Unit, onDrawerDestinationClick: (DrawerScreenDestination) -> Unit, onDrawerQrCodeClick: () -> Unit, badges: Badges = Badges(), onActiveDestinationClick: () -> Unit = {}, topAppBarState: TopAppBarState = remember { TopAppBarState( initialHeightOffsetLimit = -Float.MAX_VALUE, initialHeightOffset = 0f, initialContentOffset = 0f, ) }, topAppBar: @Composable (TopAppBarScrollBehavior?) -> Unit = {}, content: @Composable (PaddingValues) -> Unit = {}, floatingNewDataHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, focusModeEnabled: Boolean = true, ) LongMethod:PrimalNavigationBar.kt$@Composable fun PrimalNavigationBarLightningBolt( modifier: Modifier = Modifier, activeDestination: PrimalTopLevelDestination, onTopLevelDestinationChanged: (PrimalTopLevelDestination) -> Unit, onActiveDestinationClick: (() -> Unit)? = null, badges: Badges = Badges(), ) LongMethod:PrimalOutlinedTextField.kt$@Composable fun PrimalOutlinedTextField( header: String?, value: String, onValueChange: (String) -> Unit, forceFocus: Boolean = false, isRequired: Boolean = false, prefix: String? = null, isMultiline: Boolean = false, fontSize: TextUnit = 16.sp, textAlign: TextAlign = TextAlign.Start, isError: Boolean = false, ) - LongMethod:PrimalTopAppBar.kt$@OptIn(ExperimentalFoundationApi::class) @ExperimentalMaterial3Api @Composable fun PrimalTopAppBar( modifier: Modifier = Modifier, title: String = "", subtitle: String? = null, titleTrailingIcon: ImageVector? = null, textColor: Color = LocalContentColor.current, navigationIcon: ImageVector? = null, navigationIconTintColor: Color = LocalContentColor.current, navigationIconContentDescription: String? = null, onNavigationIconClick: (() -> Unit)? = null, autoCloseKeyboardOnNavigationIconClick: Boolean = true, avatarCdnImage: CdnImage? = null, avatarLegendaryStyle: LegendaryStyle? = null, actions: (@Composable RowScope.() -> Unit)? = null, showDivider: Boolean = true, scrollBehavior: TopAppBarScrollBehavior? = null, onTitleClick: (() -> Unit)? = null, onTitleLongClick: (() -> Unit)? = null, colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = AppTheme.colorScheme.surface, scrolledContainerColor = AppTheme.colorScheme.surface, ), footer: @Composable () -> Unit = {}, ) + LongMethod:PrimalTopAppBar.kt$@OptIn(ExperimentalFoundationApi::class) @ExperimentalMaterial3Api @Composable fun PrimalTopAppBar( modifier: Modifier = Modifier, title: String = "", subtitle: String? = null, titleTrailingIcon: ImageVector? = null, textColor: Color = LocalContentColor.current, navigationIcon: ImageVector? = null, navigationIconTintColor: Color = LocalContentColor.current, navigationIconContentDescription: String? = null, onNavigationIconClick: (() -> Unit)? = null, autoCloseKeyboardOnNavigationIconClick: Boolean = true, avatarCdnImage: CdnImage? = null, legendaryCustomization: LegendaryCustomization? = null, actions: (@Composable RowScope.() -> Unit)? = null, showDivider: Boolean = true, scrollBehavior: TopAppBarScrollBehavior? = null, onTitleClick: (() -> Unit)? = null, onTitleLongClick: (() -> Unit)? = null, colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = AppTheme.colorScheme.surface, scrolledContainerColor = AppTheme.colorScheme.surface, ), footer: @Composable () -> Unit = {}, ) LongMethod:ReactionsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReactionsScreen( state: ReactionsContract.UiState, onClose: () -> Unit, onProfileClick: (profileId: String) -> Unit, ) LongMethod:ReadsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReadsScreen( state: ReadsScreenContract.UiState, onTopLevelDestinationChanged: (PrimalTopLevelDestination) -> Unit, onDrawerScreenClick: (DrawerScreenDestination) -> Unit, onDrawerQrCodeClick: () -> Unit, onSearchClick: () -> Unit, onArticleClick: (naddr: String) -> Unit, onGetPremiumClick: () -> Unit, eventPublisher: (ReadsScreenContract.UiEvent) -> Unit, ) LongMethod:ReceivePaymentScreen.kt$@Composable private fun ReceivePaymentViewer( paddingValues: PaddingValues, state: UiState, onBuyPremium: () -> Unit, onCopyClick: () -> Unit, onEditClick: () -> Unit, ) diff --git a/app/schemas/net.primal.android.db.PrimalDatabase/47.json b/app/schemas/net.primal.android.db.PrimalDatabase/47.json new file mode 100644 index 000000000..4fa7d2fd6 --- /dev/null +++ b/app/schemas/net.primal.android.db.PrimalDatabase/47.json @@ -0,0 +1,1789 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "3b388fe04b85780703286c11bae8a02e", + "entities": [ + { + "tableName": "PostData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`postId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `uris` TEXT NOT NULL, `hashtags` TEXT NOT NULL, `sig` TEXT NOT NULL, `raw` TEXT NOT NULL, `authorMetadataId` TEXT, `replyToPostId` TEXT, `replyToAuthorId` TEXT, PRIMARY KEY(`postId`))", + "fields": [ + { + "fieldPath": "postId", + "columnName": "postId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uris", + "columnName": "uris", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hashtags", + "columnName": "hashtags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sig", + "columnName": "sig", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorMetadataId", + "columnName": "authorMetadataId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyToPostId", + "columnName": "replyToPostId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyToAuthorId", + "columnName": "replyToAuthorId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "postId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ProfileData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ownerId` TEXT NOT NULL, `eventId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `raw` TEXT NOT NULL, `handle` TEXT, `displayName` TEXT, `internetIdentifier` TEXT, `lightningAddress` TEXT, `lnUrlDecoded` TEXT, `avatarCdnImage` TEXT, `bannerCdnImage` TEXT, `website` TEXT, `about` TEXT, `aboutUris` TEXT NOT NULL, `aboutHashtags` TEXT NOT NULL, `primalName` TEXT, `primalLegendProfile` TEXT, PRIMARY KEY(`ownerId`))", + "fields": [ + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "handle", + "columnName": "handle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "internetIdentifier", + "columnName": "internetIdentifier", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lightningAddress", + "columnName": "lightningAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lnUrlDecoded", + "columnName": "lnUrlDecoded", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarCdnImage", + "columnName": "avatarCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerCdnImage", + "columnName": "bannerCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "about", + "columnName": "about", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "aboutUris", + "columnName": "aboutUris", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aboutHashtags", + "columnName": "aboutHashtags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primalName", + "columnName": "primalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primalLegendProfile", + "columnName": "primalLegendProfile", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "ownerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RepostData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repostId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `tags` TEXT NOT NULL, `postId` TEXT NOT NULL, `postAuthorId` TEXT NOT NULL, `sig` TEXT NOT NULL, PRIMARY KEY(`repostId`))", + "fields": [ + { + "fieldPath": "repostId", + "columnName": "repostId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postId", + "columnName": "postId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postAuthorId", + "columnName": "postAuthorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sig", + "columnName": "sig", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repostId" + ] + }, + "indices": [ + { + "name": "index_RepostData_postId", + "unique": false, + "columnNames": [ + "postId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_RepostData_postId` ON `${TABLE_NAME}` (`postId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "EventStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `likes` INTEGER NOT NULL, `replies` INTEGER NOT NULL, `mentions` INTEGER NOT NULL, `reposts` INTEGER NOT NULL, `zaps` INTEGER NOT NULL, `satsZapped` INTEGER NOT NULL, `score` INTEGER NOT NULL, `score24h` INTEGER NOT NULL, PRIMARY KEY(`eventId`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "likes", + "columnName": "likes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replies", + "columnName": "replies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reposts", + "columnName": "reposts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "zaps", + "columnName": "zaps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "satsZapped", + "columnName": "satsZapped", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "score24h", + "columnName": "score24h", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eventId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EventZap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `zapSenderId` TEXT NOT NULL, `zapReceiverId` TEXT NOT NULL, `zapRequestAt` INTEGER NOT NULL, `zapReceiptAt` INTEGER NOT NULL, `amountInBtc` REAL NOT NULL, `message` TEXT, `zapSenderDisplayName` TEXT, `zapSenderHandle` TEXT, `zapSenderInternetIdentifier` TEXT, `zapSenderAvatarCdnImage` TEXT, `zapSenderPrimalLegendProfile` TEXT, PRIMARY KEY(`zapSenderId`, `eventId`, `zapRequestAt`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zapSenderId", + "columnName": "zapSenderId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zapReceiverId", + "columnName": "zapReceiverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zapRequestAt", + "columnName": "zapRequestAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "zapReceiptAt", + "columnName": "zapReceiptAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amountInBtc", + "columnName": "amountInBtc", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapSenderDisplayName", + "columnName": "zapSenderDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapSenderHandle", + "columnName": "zapSenderHandle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapSenderInternetIdentifier", + "columnName": "zapSenderInternetIdentifier", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapSenderAvatarCdnImage", + "columnName": "zapSenderAvatarCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapSenderPrimalLegendProfile", + "columnName": "zapSenderPrimalLegendProfile", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "zapSenderId", + "eventId", + "zapRequestAt" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EventUserStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `userId` TEXT NOT NULL, `replied` INTEGER NOT NULL, `liked` INTEGER NOT NULL, `reposted` INTEGER NOT NULL, `zapped` INTEGER NOT NULL, PRIMARY KEY(`eventId`, `userId`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replied", + "columnName": "replied", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reposted", + "columnName": "reposted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "zapped", + "columnName": "zapped", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eventId", + "userId" + ] + }, + "indices": [ + { + "name": "index_EventUserStats_eventId", + "unique": false, + "columnNames": [ + "eventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventUserStats_eventId` ON `${TABLE_NAME}` (`eventId`)" + }, + { + "name": "index_EventUserStats_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventUserStats_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "NoteNostrUri", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `uri` TEXT NOT NULL, `type` TEXT NOT NULL, `refEvent_alt` TEXT, `refHighlight_text` TEXT, `refHighlight_eventId` TEXT, `refHighlight_authorId` TEXT, `refHighlight_aTag` TEXT, `refNote_postId` TEXT, `refNote_createdAt` INTEGER, `refNote_content` TEXT, `refNote_authorId` TEXT, `refNote_authorName` TEXT, `refNote_authorAvatarCdnImage` TEXT, `refNote_authorInternetIdentifier` TEXT, `refNote_authorLightningAddress` TEXT, `refNote_attachments` TEXT, `refNote_nostrUris` TEXT, `refArticle_naddr` TEXT, `refArticle_aTag` TEXT, `refArticle_eventId` TEXT, `refArticle_articleId` TEXT, `refArticle_articleTitle` TEXT, `refArticle_authorId` TEXT, `refArticle_authorName` TEXT, `refArticle_authorAvatarCdnImage` TEXT, `refArticle_createdAt` INTEGER, `refArticle_raw` TEXT, `refArticle_articleImageCdnImage` TEXT, `refArticle_articleReadingTimeInMinutes` INTEGER, `refUser_userId` TEXT, `refUser_handle` TEXT, PRIMARY KEY(`noteId`, `uri`))", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "referencedEventAlt", + "columnName": "refEvent_alt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedHighlight.text", + "columnName": "refHighlight_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedHighlight.eventId", + "columnName": "refHighlight_eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedHighlight.authorId", + "columnName": "refHighlight_authorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedHighlight.aTag", + "columnName": "refHighlight_aTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.postId", + "columnName": "refNote_postId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.createdAt", + "columnName": "refNote_createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "referencedNote.content", + "columnName": "refNote_content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.authorId", + "columnName": "refNote_authorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.authorName", + "columnName": "refNote_authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.authorAvatarCdnImage", + "columnName": "refNote_authorAvatarCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.authorInternetIdentifier", + "columnName": "refNote_authorInternetIdentifier", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.authorLightningAddress", + "columnName": "refNote_authorLightningAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.attachments", + "columnName": "refNote_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedNote.nostrUris", + "columnName": "refNote_nostrUris", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.naddr", + "columnName": "refArticle_naddr", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.aTag", + "columnName": "refArticle_aTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.eventId", + "columnName": "refArticle_eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.articleId", + "columnName": "refArticle_articleId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.articleTitle", + "columnName": "refArticle_articleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.authorId", + "columnName": "refArticle_authorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.authorName", + "columnName": "refArticle_authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.authorAvatarCdnImage", + "columnName": "refArticle_authorAvatarCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.createdAt", + "columnName": "refArticle_createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "referencedArticle.raw", + "columnName": "refArticle_raw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.articleImageCdnImage", + "columnName": "refArticle_articleImageCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedArticle.articleReadingTimeInMinutes", + "columnName": "refArticle_articleReadingTimeInMinutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "referencedUser.userId", + "columnName": "refUser_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referencedUser.handle", + "columnName": "refUser_handle", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "noteId", + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NoteAttachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `mimeType` TEXT, `variants` TEXT, `title` TEXT, `description` TEXT, `thumbnail` TEXT, `authorAvatarUrl` TEXT, PRIMARY KEY(`eventId`, `url`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variants", + "columnName": "variants", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorAvatarUrl", + "columnName": "authorAvatarUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eventId", + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spec` TEXT NOT NULL, `specKind` TEXT NOT NULL, `feedKind` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`spec`))", + "fields": [ + { + "fieldPath": "spec", + "columnName": "spec", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "specKind", + "columnName": "specKind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feedKind", + "columnName": "feedKind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "spec" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FeedPostDataCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedSpec` TEXT NOT NULL, `eventId` TEXT NOT NULL, `orderIndex` INTEGER NOT NULL, PRIMARY KEY(`feedSpec`, `eventId`))", + "fields": [ + { + "fieldPath": "feedSpec", + "columnName": "feedSpec", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderIndex", + "columnName": "orderIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "feedSpec", + "eventId" + ] + }, + "indices": [ + { + "name": "index_FeedPostDataCrossRef_feedSpec", + "unique": false, + "columnNames": [ + "feedSpec" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeedPostDataCrossRef_feedSpec` ON `${TABLE_NAME}` (`feedSpec`)" + }, + { + "name": "index_FeedPostDataCrossRef_eventId", + "unique": false, + "columnNames": [ + "eventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeedPostDataCrossRef_eventId` ON `${TABLE_NAME}` (`eventId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "FeedPostRemoteKey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `directive` TEXT NOT NULL, `sinceId` INTEGER NOT NULL, `untilId` INTEGER NOT NULL, `cachedAt` INTEGER NOT NULL, PRIMARY KEY(`eventId`, `directive`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directive", + "columnName": "directive", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sinceId", + "columnName": "sinceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "untilId", + "columnName": "untilId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cachedAt", + "columnName": "cachedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eventId", + "directive" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NoteConversationCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `replyNoteId` TEXT NOT NULL, PRIMARY KEY(`noteId`, `replyNoteId`))", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replyNoteId", + "columnName": "replyNoteId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "noteId", + "replyNoteId" + ] + }, + "indices": [ + { + "name": "index_NoteConversationCrossRef_noteId", + "unique": false, + "columnNames": [ + "noteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NoteConversationCrossRef_noteId` ON `${TABLE_NAME}` (`noteId`)" + }, + { + "name": "index_NoteConversationCrossRef_replyNoteId", + "unique": false, + "columnNames": [ + "replyNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NoteConversationCrossRef_replyNoteId` ON `${TABLE_NAME}` (`replyNoteId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ProfileStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `following` INTEGER, `followers` INTEGER, `notesCount` INTEGER, `readsCount` INTEGER, `mediaCount` INTEGER, `repliesCount` INTEGER, `relaysCount` INTEGER, `totalReceivedZaps` INTEGER, `contentZapCount` INTEGER, `totalReceivedSats` INTEGER, `joinedAt` INTEGER, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followers", + "columnName": "followers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "readsCount", + "columnName": "readsCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCount", + "columnName": "mediaCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "relaysCount", + "columnName": "relaysCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalReceivedZaps", + "columnName": "totalReceivedZaps", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentZapCount", + "columnName": "contentZapCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalReceivedSats", + "columnName": "totalReceivedSats", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "joinedAt", + "columnName": "joinedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "profileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TrendingTopic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`topic` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`topic`))", + "fields": [ + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "topic" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NotificationData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `type` TEXT NOT NULL, `seenGloballyAt` INTEGER, `actionUserId` TEXT, `actionPostId` TEXT, `satsZapped` INTEGER, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "seenGloballyAt", + "columnName": "seenGloballyAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "actionUserId", + "columnName": "actionUserId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actionPostId", + "columnName": "actionPostId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "satsZapped", + "columnName": "satsZapped", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MutedUserData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `userMetadataEventId` TEXT, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userMetadataEventId", + "columnName": "userMetadataEventId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DirectMessageData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `senderId` TEXT NOT NULL, `receiverId` TEXT NOT NULL, `participantId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `content` TEXT NOT NULL, `uris` TEXT NOT NULL, `hashtags` TEXT NOT NULL, PRIMARY KEY(`messageId`))", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receiverId", + "columnName": "receiverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantId", + "columnName": "participantId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uris", + "columnName": "uris", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hashtags", + "columnName": "hashtags", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "messageId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageConversationData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`participantId` TEXT NOT NULL, `lastMessageId` TEXT NOT NULL, `lastMessageAt` INTEGER NOT NULL, `unreadMessagesCount` INTEGER NOT NULL, `relation` TEXT NOT NULL, `participantMetadataId` TEXT, PRIMARY KEY(`participantId`))", + "fields": [ + { + "fieldPath": "participantId", + "columnName": "participantId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastMessageAt", + "columnName": "lastMessageAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessagesCount", + "columnName": "unreadMessagesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "relation", + "columnName": "relation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantMetadataId", + "columnName": "participantMetadataId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "participantId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "WalletTransactionData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `walletLightningAddress` TEXT NOT NULL, `type` TEXT NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `completedAt` INTEGER, `amountInBtc` REAL NOT NULL, `amountInUsd` REAL, `isZap` INTEGER NOT NULL, `isStorePurchase` INTEGER NOT NULL, `userId` TEXT NOT NULL, `userSubWallet` TEXT NOT NULL, `userLightningAddress` TEXT, `otherUserId` TEXT, `otherLightningAddress` TEXT, `note` TEXT, `invoice` TEXT, `totalFeeInBtc` TEXT, `exchangeRate` TEXT, `onChainAddress` TEXT, `onChainTxId` TEXT, `zapNoteId` TEXT, `zapNoteAuthorId` TEXT, `zappedByUserId` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletLightningAddress", + "columnName": "walletLightningAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "amountInBtc", + "columnName": "amountInBtc", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "amountInUsd", + "columnName": "amountInUsd", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "isZap", + "columnName": "isZap", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStorePurchase", + "columnName": "isStorePurchase", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userSubWallet", + "columnName": "userSubWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userLightningAddress", + "columnName": "userLightningAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "otherUserId", + "columnName": "otherUserId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "otherLightningAddress", + "columnName": "otherLightningAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "invoice", + "columnName": "invoice", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalFeeInBtc", + "columnName": "totalFeeInBtc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exchangeRate", + "columnName": "exchangeRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onChainAddress", + "columnName": "onChainAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onChainTxId", + "columnName": "onChainTxId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapNoteId", + "columnName": "zapNoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zapNoteAuthorId", + "columnName": "zapNoteAuthorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "zappedByUserId", + "columnName": "zappedByUserId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Relay", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `kind` TEXT NOT NULL, `url` TEXT NOT NULL, `read` INTEGER NOT NULL, `write` INTEGER NOT NULL, PRIMARY KEY(`userId`, `kind`, `url`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "write", + "columnName": "write", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "kind", + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EventRelayHints", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` TEXT NOT NULL, `relays` TEXT NOT NULL, PRIMARY KEY(`eventId`))", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "eventId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PublicBookmark", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagValue` TEXT NOT NULL, `tagType` TEXT NOT NULL, `bookmarkType` TEXT NOT NULL, `ownerId` TEXT NOT NULL, PRIMARY KEY(`tagValue`))", + "fields": [ + { + "fieldPath": "tagValue", + "columnName": "tagValue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tagType", + "columnName": "tagType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookmarkType", + "columnName": "bookmarkType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagValue" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ProfileInteraction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `lastInteractionAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastInteractionAt", + "columnName": "lastInteractionAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "profileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArticleData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aTag` TEXT NOT NULL, `eventId` TEXT NOT NULL, `articleId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `content` TEXT NOT NULL, `title` TEXT NOT NULL, `publishedAt` INTEGER NOT NULL, `raw` TEXT NOT NULL, `imageCdnImage` TEXT, `summary` TEXT, `authorMetadataId` TEXT, `wordsCount` INTEGER, `uris` TEXT NOT NULL, `hashtags` TEXT NOT NULL, PRIMARY KEY(`aTag`))", + "fields": [ + { + "fieldPath": "aTag", + "columnName": "aTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "articleId", + "columnName": "articleId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publishedAt", + "columnName": "publishedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageCdnImage", + "columnName": "imageCdnImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorMetadataId", + "columnName": "authorMetadataId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordsCount", + "columnName": "wordsCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uris", + "columnName": "uris", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hashtags", + "columnName": "hashtags", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "aTag" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArticleCommentCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`articleId` TEXT NOT NULL, `articleAuthorId` TEXT NOT NULL, `commentNoteId` TEXT NOT NULL, PRIMARY KEY(`articleId`, `articleAuthorId`, `commentNoteId`))", + "fields": [ + { + "fieldPath": "articleId", + "columnName": "articleId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "articleAuthorId", + "columnName": "articleAuthorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "commentNoteId", + "columnName": "commentNoteId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "articleId", + "articleAuthorId", + "commentNoteId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArticleFeedCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spec` TEXT NOT NULL, `articleId` TEXT NOT NULL, `articleAuthorId` TEXT NOT NULL, PRIMARY KEY(`spec`, `articleId`, `articleAuthorId`))", + "fields": [ + { + "fieldPath": "spec", + "columnName": "spec", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "articleId", + "columnName": "articleId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "articleAuthorId", + "columnName": "articleAuthorId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "spec", + "articleId", + "articleAuthorId" + ] + }, + "indices": [ + { + "name": "index_ArticleFeedCrossRef_spec", + "unique": false, + "columnNames": [ + "spec" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ArticleFeedCrossRef_spec` ON `${TABLE_NAME}` (`spec`)" + }, + { + "name": "index_ArticleFeedCrossRef_articleId", + "unique": false, + "columnNames": [ + "articleId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ArticleFeedCrossRef_articleId` ON `${TABLE_NAME}` (`articleId`)" + }, + { + "name": "index_ArticleFeedCrossRef_articleAuthorId", + "unique": false, + "columnNames": [ + "articleAuthorId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ArticleFeedCrossRef_articleAuthorId` ON `${TABLE_NAME}` (`articleAuthorId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b388fe04b85780703286c11bae8a02e')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleListItem.kt b/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleListItem.kt index d057d857a..535742da6 100644 --- a/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleListItem.kt +++ b/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleListItem.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity @@ -45,8 +44,8 @@ import coil.compose.SubcomposeAsyncImage import java.time.Instant import net.primal.android.R import net.primal.android.attachments.domain.findNearestOrNull -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.IconText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.WrappedContentWithSuffix import net.primal.android.core.compose.asBeforeNowFormat import net.primal.android.core.compose.icons.PrimalIcons @@ -161,14 +160,10 @@ private fun ListItemHeader( .padding(end = overflowIconSizeDp - 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarSize = 24.dp, avatarCdnImage = data.authorAvatarCdnImage, - hasBorder = data.authorLegendAvatarGlow && data.authorLegendaryStyle != null, - borderBrush = when { - data.authorLegendaryStyle != null -> data.authorLegendaryStyle.brush - else -> Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, + legendaryCustomization = data.authorLegendaryCustomization, ) Box(modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleUi.kt b/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleUi.kt index 2d720587a..158291868 100644 --- a/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleUi.kt +++ b/app/src/main/kotlin/net/primal/android/articles/feed/ui/FeedArticleUi.kt @@ -9,7 +9,8 @@ import net.primal.android.nostr.model.NostrEventKind import net.primal.android.nostr.utils.Naddr import net.primal.android.nostr.utils.Nip19TLV.toNaddrString import net.primal.android.notes.feed.model.EventStatsUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.stats.ui.EventZapUiModel import net.primal.android.stats.ui.asEventZapUiModel @@ -29,9 +30,7 @@ data class FeedArticleUi( val imageCdnImage: CdnImage? = null, val readingTimeInMinutes: Int? = null, val eventZaps: List = emptyList(), - val authorLegendAvatarGlow: Boolean = false, - val authorLegendCustomBadge: Boolean = false, - val authorLegendaryStyle: LegendaryStyle? = null, + val authorLegendaryCustomization: LegendaryCustomization? = null, ) fun Article.mapAsFeedArticleUi(): FeedArticleUi { @@ -51,9 +50,7 @@ fun Article.mapAsFeedArticleUi(): FeedArticleUi { stats = EventStatsUi.from(eventStats = this.eventStats, userStats = null), readingTimeInMinutes = this.data.wordsCount.wordsCountToReadingTime(), eventZaps = this.eventZaps.map { it.asEventZapUiModel() }, - authorLegendAvatarGlow = this.author?.primalLegendProfile?.avatarGlow == true, - authorLegendCustomBadge = this.author?.primalLegendProfile?.customBadge == true, - authorLegendaryStyle = LegendaryStyle.valueById(this.author?.primalLegendProfile?.styleId), + authorLegendaryCustomization = this.author?.primalLegendProfile?.asLegendaryCustomization(), ) } diff --git a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreen.kt b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreen.kt index 1fdb4a538..9266654a6 100644 --- a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreen.kt @@ -53,7 +53,7 @@ import net.primal.android.drawer.PrimalDrawerScaffold import net.primal.android.feeds.domain.FeedSpecKind import net.primal.android.feeds.list.FeedsBottomSheet import net.primal.android.feeds.list.ui.model.FeedUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization @Composable fun ReadsScreen( @@ -129,7 +129,7 @@ private fun ReadsScreen( title = activeFeed?.title ?: "", activeFeed = activeFeed, avatarCdnImage = state.activeAccountAvatarCdnImage, - avatarLegendaryStyle = state.activeAccountLegendaryStyle, + avatarLegendaryCustomization = state.activeAccountLegendaryCustomization, onAvatarClick = { uiScope.launch { drawerState.open() } }, onSearchClick = onSearchClick, onFeedChanged = { feed -> @@ -195,7 +195,7 @@ private fun ArticleFeedTopAppBar( onSearchClick: () -> Unit, activeFeed: FeedUi?, onFeedChanged: (FeedUi) -> Unit, - avatarLegendaryStyle: LegendaryStyle? = null, + avatarLegendaryCustomization: LegendaryCustomization? = null, scrollBehavior: TopAppBarScrollBehavior? = null, ) { var feedPickerVisible by rememberSaveable { mutableStateOf(false) } @@ -222,7 +222,7 @@ private fun ArticleFeedTopAppBar( } }, avatarCdnImage = avatarCdnImage, - avatarLegendaryStyle = avatarLegendaryStyle, + legendaryCustomization = avatarLegendaryCustomization, navigationIcon = PrimalIcons.AvatarDefault, onNavigationIconClick = onAvatarClick, actions = { diff --git a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreenContract.kt b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreenContract.kt index 7d1346bf0..4d4ed06a0 100644 --- a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreenContract.kt +++ b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsScreenContract.kt @@ -2,14 +2,14 @@ package net.primal.android.articles.reads import net.primal.android.attachments.domain.CdnImage import net.primal.android.feeds.list.ui.model.FeedUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges interface ReadsScreenContract { data class UiState( val feeds: List = emptyList(), val activeAccountAvatarCdnImage: CdnImage? = null, - val activeAccountLegendaryStyle: LegendaryStyle? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val badges: Badges = Badges(), val loading: Boolean = false, ) diff --git a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsViewModel.kt b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsViewModel.kt index 3c507d4ca..cee0d03f5 100644 --- a/app/src/main/kotlin/net/primal/android/articles/reads/ReadsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/articles/reads/ReadsViewModel.kt @@ -16,6 +16,7 @@ import net.primal.android.feeds.list.ui.model.asFeedUi import net.primal.android.feeds.repository.FeedsRepository import net.primal.android.networking.primal.retryNetworkCall import net.primal.android.networking.sockets.errors.WssException +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.subscriptions.SubscriptionsManager import timber.log.Timber @@ -102,7 +103,7 @@ class ReadsViewModel @Inject constructor( setState { copy( activeAccountAvatarCdnImage = it.avatarCdnImage, - activeAccountLegendaryStyle = if (it.avatarRing) it.legendaryStyle else null, + activeAccountLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/auth/login/LoginScreen.kt b/app/src/main/kotlin/net/primal/android/auth/login/LoginScreen.kt index 007737484..9d690c673 100644 --- a/app/src/main/kotlin/net/primal/android/auth/login/LoginScreen.kt +++ b/app/src/main/kotlin/net/primal/android/auth/login/LoginScreen.kt @@ -54,9 +54,9 @@ import net.primal.android.auth.compose.DefaultOnboardingAvatar import net.primal.android.auth.compose.OnboardingButton import net.primal.android.auth.compose.defaultOnboardingAvatarBackground import net.primal.android.core.compose.AppBarIcon -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.PrimalDefaults import net.primal.android.core.compose.UiDensityMode +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.detectUiDensityModeFromMaxHeight import net.primal.android.core.compose.foundation.keyboardVisibilityAsState import net.primal.android.core.compose.icons.PrimalIcons @@ -397,11 +397,12 @@ private fun ProfileDetailsColumn( verticalArrangement = Arrangement.Bottom, ) { if (!(uiMode.isCompactOrLower() && keyboardVisible)) { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = profileDetails.avatarCdnImage, avatarSize = 100.dp, hasBorder = profileDetails.avatarCdnImage != null, - borderColor = Color.White, + fallbackBorderColor = Color.White, + legendaryCustomization = profileDetails.legendaryCustomization, backgroundColor = defaultOnboardingAvatarBackground, defaultAvatar = { DefaultOnboardingAvatar() }, ) diff --git a/app/src/main/kotlin/net/primal/android/auth/onboarding/account/ui/OnboardingProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/auth/onboarding/account/ui/OnboardingProfileFollowsScreen.kt index 69a408cbf..764c2b8f7 100644 --- a/app/src/main/kotlin/net/primal/android/auth/onboarding/account/ui/OnboardingProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/auth/onboarding/account/ui/OnboardingProfileFollowsScreen.kt @@ -54,9 +54,9 @@ import net.primal.android.auth.onboarding.account.OnboardingContract import net.primal.android.auth.onboarding.account.OnboardingStep import net.primal.android.auth.onboarding.account.ui.model.FollowGroup import net.primal.android.auth.onboarding.account.ui.model.FollowGroupMember -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalFilledButton import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack @@ -373,7 +373,7 @@ private fun FollowGroupMemberListItem(member: FollowGroupMember, onClick: () -> containerColor = Color.White, ), leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = member.metadata?.picture?.let { CdnImage(sourceUrl = it) }, ) }, diff --git a/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailListItemImage.kt b/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailListItemImage.kt index c1326bd45..42af14585 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailListItemImage.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailListItemImage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon @@ -27,56 +28,46 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.AvatarDefault import net.primal.android.core.images.AvatarCoilImageLoader +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.theme.AppTheme @Composable -fun AvatarThumbnail( +fun UniversalAvatarThumbnail( modifier: Modifier = Modifier, avatarCdnImage: CdnImage? = null, avatarSize: Dp = 48.dp, - hasBorder: Boolean = false, - borderColor: Color = AppTheme.colorScheme.primary, - borderSize: Dp = 2.dp, - backgroundColor: Color = AppTheme.extraColorScheme.surfaceVariantAlt1, - onClick: (() -> Unit)? = null, - defaultAvatar: @Composable () -> Unit = { DefaultAvatarThumbnailPlaceholderListItemImage() }, -) { - AvatarThumbnailCustomBorder( - modifier = modifier, - avatarCdnImage = avatarCdnImage, - avatarSize = avatarSize, - hasBorder = hasBorder, - borderBrush = Brush.linearGradient(listOf(borderColor, borderColor)), - borderSize = borderSize, - backgroundColor = backgroundColor, - onClick = onClick, - defaultAvatar = defaultAvatar, - ) -} - -@Composable -fun AvatarThumbnailCustomBorder( - modifier: Modifier = Modifier, - avatarCdnImage: CdnImage? = null, - avatarSize: Dp = 48.dp, - hasBorder: Boolean = false, - borderBrush: Brush = Brush.linearGradient( - listOf(AppTheme.colorScheme.primary, AppTheme.colorScheme.primary), - ), - borderSize: Dp = 2.dp, + hasBorder: Boolean = true, + legendaryCustomization: LegendaryCustomization? = null, + fallbackBorderColor: Color = Color.Transparent, + borderSizeOverride: Dp? = null, backgroundColor: Color = AppTheme.extraColorScheme.surfaceVariantAlt1, onClick: (() -> Unit)? = null, defaultAvatar: @Composable () -> Unit = { DefaultAvatarThumbnailPlaceholderListItemImage() }, ) { val variant = avatarCdnImage?.variants?.minByOrNull { it.width } val imageSource = variant?.mediaUrl ?: avatarCdnImage?.sourceUrl + + val borderBrush = if (legendaryCustomization?.avatarGlow == true && + legendaryCustomization.legendaryStyle != LegendaryStyle.NO_CUSTOMIZATION + ) { + legendaryCustomization.legendaryStyle?.brush + } else { + null + } + AvatarThumbnailListItemImage( modifier = modifier, avatarSize = avatarSize, source = imageSource, hasBorder = hasBorder, - borderBrush = borderBrush, - borderSize = borderSize, + borderBrush = borderBrush ?: Brush.linearGradient( + colors = listOf( + fallbackBorderColor, + fallbackBorderColor, + ), + ), + borderSize = borderSizeOverride ?: avatarSize.mapAvatarSizeToBorderSize(), backgroundColor = backgroundColor, onClick = onClick, defaultAvatar = defaultAvatar, @@ -137,20 +128,39 @@ fun Modifier.adjustAvatarBackground( ): Modifier { return if (hasBorder) { this - .size(size + borderSize) + .size(size + borderSize * 2) .border( width = borderSize, brush = borderBrush, shape = CircleShape, ) + .padding(borderSize) .clip(CircleShape) } else { this - .size(size) + .size(size + borderSize * 2) + .border( + width = borderSize, + brush = Brush.linearGradient( + listOf(Color.Transparent, Color.Transparent), + ), + shape = CircleShape, + ) + .padding(borderSize) .clip(CircleShape) } } +@Suppress("MagicNumber") +private fun Dp.mapAvatarSizeToBorderSize(): Dp = + when { + this >= 112.dp -> 4.dp + this >= 80.dp -> 3.dp + this >= 32.dp -> 2.dp + this >= 24.dp -> (1.5).dp + else -> 1.dp + } + @Composable fun DefaultAvatarThumbnailPlaceholderListItemImage( backgroundColor: Color = AppTheme.extraColorScheme.surfaceVariantAlt1, diff --git a/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailsRow.kt b/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailsRow.kt index 7e8872ccb..2cc3e4f9c 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailsRow.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/AvatarThumbnailsRow.kt @@ -19,12 +19,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme @Composable fun AvatarThumbnailsRow( modifier: Modifier = Modifier, avatarCdnImages: List, + avatarLegendaryCustomizations: List, avatarOverlap: AvatarOverlap = AvatarOverlap.End, hasAvatarBorder: Boolean = true, avatarBorderSize: Dp = 2.dp, @@ -70,12 +72,13 @@ fun AvatarThumbnailsRow( } AvatarSpacer(width = (layoutIndex * avatarVisibleWidth.value).dp) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.size(avatarSize), avatarCdnImage = imageCdnImage, hasBorder = hasAvatarBorder, - borderColor = avatarBorderColor, - borderSize = avatarBorderSize, + legendaryCustomization = runCatching { avatarLegendaryCustomizations[layoutIndex] }.getOrNull(), + fallbackBorderColor = avatarBorderColor, + borderSizeOverride = avatarBorderSize, onClick = { onClick(layoutIndex) }, ) } diff --git a/app/src/main/kotlin/net/primal/android/core/compose/PrimalTopAppBar.kt b/app/src/main/kotlin/net/primal/android/core/compose/PrimalTopAppBar.kt index 19f92905e..53e41b73e 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/PrimalTopAppBar.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/PrimalTopAppBar.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -35,7 +34,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.attachments.domain.CdnImage -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme @OptIn(ExperimentalFoundationApi::class) @@ -53,7 +52,7 @@ fun PrimalTopAppBar( onNavigationIconClick: (() -> Unit)? = null, autoCloseKeyboardOnNavigationIconClick: Boolean = true, avatarCdnImage: CdnImage? = null, - avatarLegendaryStyle: LegendaryStyle? = null, + legendaryCustomization: LegendaryCustomization? = null, actions: (@Composable RowScope.() -> Unit)? = null, showDivider: Boolean = true, scrollBehavior: TopAppBarScrollBehavior? = null, @@ -77,15 +76,11 @@ fun PrimalTopAppBar( .padding(horizontal = 8.dp) .clip(CircleShape), ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarCdnImage = avatarCdnImage, modifier = Modifier.size(32.dp), onClick = onNavigationIconClick, - hasBorder = avatarLegendaryStyle != null, - borderBrush = avatarLegendaryStyle?.brush - ?: Brush.linearGradient( - listOf(AppTheme.colorScheme.primary, AppTheme.colorScheme.primary), - ), + legendaryCustomization = legendaryCustomization, ) } } else if (navigationIcon != null) { diff --git a/app/src/main/kotlin/net/primal/android/core/compose/profile/model/ProfileDetailsUi.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/model/ProfileDetailsUi.kt index af3bc2bbd..2532665d7 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/profile/model/ProfileDetailsUi.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/model/ProfileDetailsUi.kt @@ -3,7 +3,8 @@ package net.primal.android.core.compose.profile.model import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.core.utils.usernameUiFriendly -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.db.ProfileData data class ProfileDetailsUi( @@ -20,9 +21,7 @@ data class ProfileDetailsUi( val website: String? = null, val primalName: String? = null, val lnUrlDecoded: String? = null, - val avatarGlow: Boolean = false, - val customBadge: Boolean = false, - val legendaryStyle: LegendaryStyle? = null, + val legendaryCustomization: LegendaryCustomization? = null, ) fun ProfileData.asProfileDetailsUi() = @@ -40,7 +39,5 @@ fun ProfileData.asProfileDetailsUi() = website = this.website, primalName = this.primalName, lnUrlDecoded = this.lnUrlDecoded, - avatarGlow = this.primalLegendProfile?.avatarGlow == true, - customBadge = this.primalLegendProfile?.customBadge == true, - legendaryStyle = LegendaryStyle.valueById(this.primalLegendProfile?.styleId), + legendaryCustomization = this.primalLegendProfile?.asLegendaryCustomization(), ) diff --git a/app/src/main/kotlin/net/primal/android/core/compose/profile/model/UserProfileItemUi.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/model/UserProfileItemUi.kt index fde2beb8e..7929bd7fb 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/profile/model/UserProfileItemUi.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/model/UserProfileItemUi.kt @@ -3,6 +3,8 @@ package net.primal.android.core.compose.profile.model import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.explore.domain.UserProfileSearchItem +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.db.ProfileData data class UserProfileItemUi( @@ -13,6 +15,7 @@ data class UserProfileItemUi( val followersCount: Int? = null, val score: Float? = null, val isFollowed: Boolean? = null, + val legendaryCustomization: LegendaryCustomization? = null, ) fun UserProfileSearchItem.mapAsUserProfileUi() = @@ -23,6 +26,7 @@ fun UserProfileSearchItem.mapAsUserProfileUi() = avatarCdnImage = this.metadata.avatarCdnImage, followersCount = this.followersCount, score = this.score, + legendaryCustomization = this.metadata.primalLegendProfile?.asLegendaryCustomization(), ) fun ProfileData.asUserProfileItemUi() = @@ -34,4 +38,5 @@ fun ProfileData.asUserProfileItemUi() = followersCount = null, score = null, isFollowed = null, + legendaryCustomization = this.primalLegendProfile?.asLegendaryCustomization(), ) diff --git a/app/src/main/kotlin/net/primal/android/core/compose/zaps/ArticleTopZapsSection.kt b/app/src/main/kotlin/net/primal/android/core/compose/zaps/ArticleTopZapsSection.kt index 08a14f333..bc32f079f 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/zaps/ArticleTopZapsSection.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/zaps/ArticleTopZapsSection.kt @@ -24,11 +24,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import java.text.NumberFormat import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.IconText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.NavWalletBoltFilled import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.stats.ui.EventZapUiModel import net.primal.android.theme.AppTheme @@ -86,11 +88,12 @@ private fun ArticleTopNoteZapRow(noteZap: EventZapUiModel, onClick: () -> Unit) .clickable { onClick() }, verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(start = 2.dp), avatarCdnImage = noteZap.zapperAvatarCdnImage, avatarSize = 28.dp, onClick = onClick, + legendaryCustomization = noteZap.zapperLegendaryCustomization, ) IconText( @@ -134,11 +137,12 @@ private fun ArticleNoteZapListItem(noteZap: EventZapUiModel, onClick: () -> Unit .clickable { onClick() }, verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(start = 2.dp), avatarCdnImage = noteZap.zapperAvatarCdnImage, avatarSize = 24.dp, onClick = onClick, + legendaryCustomization = noteZap.zapperLegendaryCustomization, ) Text( @@ -202,6 +206,11 @@ private fun PreviewArticleTopZapsSection() { zappedAt = 0, message = "Top zap message!!!", amountInSats = 21_21_21.toULong(), + zapperLegendaryCustomization = LegendaryCustomization( + avatarGlow = true, + customBadge = true, + legendaryStyle = LegendaryStyle.SUN_FIRE, + ), ), EventZapUiModel( id = "id", diff --git a/app/src/main/kotlin/net/primal/android/core/compose/zaps/EventZapItem.kt b/app/src/main/kotlin/net/primal/android/core/compose/zaps/EventZapItem.kt index 122e9e7ac..e38867d3a 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/zaps/EventZapItem.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/zaps/EventZapItem.kt @@ -15,8 +15,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import java.text.NumberFormat -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.IconText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.FeedNewZapFilled import net.primal.android.stats.ui.EventZapUiModel @@ -44,11 +44,12 @@ fun EventZapItem( ), verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(start = 2.dp), avatarCdnImage = noteZap.zapperAvatarCdnImage, avatarSize = 24.dp, onClick = onClick, + legendaryCustomization = noteZap.zapperLegendaryCustomization, ) IconText( diff --git a/app/src/main/kotlin/net/primal/android/core/compose/zaps/ZappersAvatarThumbnailRow.kt b/app/src/main/kotlin/net/primal/android/core/compose/zaps/ZappersAvatarThumbnailRow.kt index 3028b139c..2f8bf9a3f 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/zaps/ZappersAvatarThumbnailRow.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/zaps/ZappersAvatarThumbnailRow.kt @@ -14,8 +14,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times import java.util.* import kotlinx.datetime.Clock -import net.primal.android.core.compose.AvatarThumbnail +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.stats.ui.EventZapUiModel import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme @@ -30,13 +32,14 @@ fun ZappersAvatarThumbnailRow(zaps: List, onClick: (() -> Unit) repeat(times = zaps.size) { index -> val zap = reversed[index] key(zap.id) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(end = index.times(18.dp)), avatarSize = 24.dp, avatarCdnImage = zap.zapperAvatarCdnImage, hasBorder = true, - borderSize = 1.dp, - borderColor = AppTheme.colorScheme.surface, + borderSizeOverride = 1.dp, + fallbackBorderColor = AppTheme.colorScheme.surface, + legendaryCustomization = zap.zapperLegendaryCustomization, onClick = onClick, ) } @@ -62,6 +65,10 @@ private fun PreviewZappersAvatarThumbnailRow() { zapperHandle = "zapper", zapperId = "zapperId", zapperName = "Zapper", + zapperLegendaryCustomization = LegendaryCustomization( + avatarGlow = true, + legendaryStyle = LegendaryStyle.SUN_FIRE, + ), ), EventZapUiModel( id = UUID.randomUUID().toString(), diff --git a/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt b/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt index 4eec9ed5a..01b98a26e 100644 --- a/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt +++ b/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt @@ -90,7 +90,7 @@ import net.primal.android.wallet.db.WalletTransactionData ArticleCommentCrossRef::class, ArticleFeedCrossRef::class, ], - version = 46, + version = 47, exportSchema = true, ) @TypeConverters( diff --git a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawer.kt b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawer.kt index 41e9cd1f1..7855fab06 100644 --- a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawer.kt +++ b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawer.kt @@ -30,8 +30,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -48,8 +46,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import java.text.NumberFormat import kotlinx.coroutines.launch import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.NostrUserText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.DarkMode import net.primal.android.core.compose.icons.primaliconpack.DrawerBookmarks @@ -62,7 +60,7 @@ import net.primal.android.core.compose.icons.primaliconpack.LightMode import net.primal.android.core.compose.icons.primaliconpack.QrCode import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.utils.formatNip05Identifier -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme import net.primal.android.user.domain.Badges @@ -119,10 +117,8 @@ fun PrimalDrawer( ) { DrawerHeader( userAccount = state.activeUserAccount, - customBadge = state.customBadge, - avatarGlow = state.avatarGlow, - legendaryStyle = state.legendaryStyle, onQrCodeClick = onQrCodeClick, + legendaryCustomization = state.legendaryCustomization, ) DrawerMenu( @@ -151,9 +147,7 @@ fun PrimalDrawer( @Composable private fun DrawerHeader( userAccount: UserAccount?, - customBadge: Boolean, - avatarGlow: Boolean, - legendaryStyle: LegendaryStyle?, + legendaryCustomization: LegendaryCustomization?, onQrCodeClick: () -> Unit, ) { val numberFormat = remember { NumberFormat.getNumberInstance() } @@ -163,18 +157,14 @@ private fun DrawerHeader( val startGuideline = createGuidelineFromStart(24.dp) val (avatarRef, usernameRef, iconRef, identifierRef, statsRef) = createRefs() - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( modifier = Modifier.constrainAs(avatarRef) { start.linkTo(startGuideline) top.linkTo(parent.top, margin = 16.dp) }, avatarSize = 52.dp, avatarCdnImage = userAccount?.avatarCdnImage, - hasBorder = avatarGlow && legendaryStyle != null, - borderBrush = when { - legendaryStyle != null -> legendaryStyle.brush - else -> Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, + legendaryCustomization = legendaryCustomization, ) NostrUserText( @@ -186,7 +176,11 @@ private fun DrawerHeader( top.linkTo(avatarRef.bottom, margin = 16.dp) width = Dimension.preferredValue(220.dp) }, - customBadgeStyle = if (customBadge) legendaryStyle else null, + customBadgeStyle = if (legendaryCustomization?.customBadge == true) { + legendaryCustomization.legendaryStyle + } else { + null + }, ) IconButton( diff --git a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerContract.kt b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerContract.kt index 50aa083ee..904fadd4a 100644 --- a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerContract.kt +++ b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerContract.kt @@ -1,6 +1,6 @@ package net.primal.android.drawer -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges import net.primal.android.user.domain.UserAccount @@ -12,9 +12,7 @@ interface PrimalDrawerContract { val activeUserAccount: UserAccount? = null, val badges: Badges = Badges(), val showPremiumBadge: Boolean = false, - val customBadge: Boolean = false, - val avatarGlow: Boolean = false, - val legendaryStyle: LegendaryStyle? = null, + val legendaryCustomization: LegendaryCustomization? = null, ) sealed class UiEvent { diff --git a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerViewModel.kt b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerViewModel.kt index e19e570b7..a128a5152 100644 --- a/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/drawer/PrimalDrawerViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.repository.ProfileRepository import net.primal.android.theme.active.ActiveThemeStore import net.primal.android.theme.domain.PrimalTheme @@ -82,9 +82,7 @@ class PrimalDrawerViewModel @Inject constructor( profileRepository.observeProfile(profileId = activeAccountStore.activeUserId()).collect { setState { copy( - avatarGlow = it.metadata?.primalLegendProfile?.avatarGlow == true, - customBadge = it.metadata?.primalLegendProfile?.customBadge == true, - legendaryStyle = LegendaryStyle.valueById(it.metadata?.primalLegendProfile?.styleId), + legendaryCustomization = it.metadata?.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt index 5560e38d2..704faa06c 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt @@ -9,6 +9,7 @@ import net.primal.android.core.compose.profile.model.UserProfileItemUi import net.primal.android.editor.domain.NoteAttachment import net.primal.android.editor.domain.NoteTaggedUser import net.primal.android.notes.feed.model.FeedPostUi +import net.primal.android.premium.legend.LegendaryCustomization interface NoteEditorContract { @@ -19,6 +20,7 @@ interface NoteEditorContract { val publishing: Boolean = false, val error: NoteEditorError? = null, val activeAccountAvatarCdnImage: CdnImage? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val uploadingAttachments: Boolean = false, val attachments: List = emptyList(), val taggedUsers: List = emptyList(), diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt index 0a8429d68..b6aa784b9 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt @@ -51,6 +51,7 @@ import net.primal.android.nostr.utils.Naddr import net.primal.android.nostr.utils.Nip19TLV import net.primal.android.notes.feed.model.asFeedPostUi import net.primal.android.notes.repository.FeedRepository +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.repository.ProfileRepository import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.accounts.active.ActiveUserAccountState @@ -169,6 +170,8 @@ class NoteEditorViewModel @AssistedInject constructor( setState { copy( activeAccountAvatarCdnImage = it.data.avatarCdnImage, + activeAccountLegendaryCustomization = + it.data.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt index 21a0705f5..0fa7f4257 100644 --- a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt +++ b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt @@ -62,13 +62,13 @@ import androidx.compose.ui.unit.sp import java.util.* import net.primal.android.R import net.primal.android.articles.feed.ui.FeedArticleListItem -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ImportPhotosIconButton import net.primal.android.core.compose.PrimalDefaults import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopAppBar import net.primal.android.core.compose.ReplyingToText import net.primal.android.core.compose.TakePhotoIconButton +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalLoadingButton import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ImportPhotoFromCamera @@ -310,7 +310,7 @@ private fun NoteEditor( } Row { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier .drawWithCache { onDrawBehind { @@ -335,6 +335,7 @@ private fun NoteEditor( .padding(top = 8.dp), avatarSize = avatarSizeDp, avatarCdnImage = state.activeAccountAvatarCdnImage, + legendaryCustomization = state.activeAccountLegendaryCustomization, ) NoteOutlinedTextField( @@ -477,6 +478,7 @@ private fun ReplyToNote(replyToNote: FeedPostUi, connectionLineColor: Color) { postTimestamp = replyToNote.timestamp, authorAvatarCdnImage = replyToNote.authorAvatarCdnImage, authorInternetIdentifier = replyToNote.authorInternetIdentifier, + authorLegendaryCustomization = replyToNote.authorLegendaryCustomization, onAuthorAvatarClick = {}, ) diff --git a/app/src/main/kotlin/net/primal/android/explore/asearch/AdvancedSearchScreen.kt b/app/src/main/kotlin/net/primal/android/explore/asearch/AdvancedSearchScreen.kt index b1a3cec7c..d9747532c 100644 --- a/app/src/main/kotlin/net/primal/android/explore/asearch/AdvancedSearchScreen.kt +++ b/app/src/main/kotlin/net/primal/android/explore/asearch/AdvancedSearchScreen.kt @@ -437,6 +437,7 @@ private fun MultipleUserPickerOptionListItem( } else { AvatarThumbnailsRow( avatarCdnImages = selectedUsers.map { it.avatarCdnImage }, + avatarLegendaryCustomizations = selectedUsers.map { it.legendaryCustomization }, onClick = {}, avatarOverlap = AvatarOverlap.None, maxAvatarsToShow = 4, diff --git a/app/src/main/kotlin/net/primal/android/explore/asearch/ui/MultipleUserPicker.kt b/app/src/main/kotlin/net/primal/android/explore/asearch/ui/MultipleUserPicker.kt index 1ecd8331e..d5f9b2c01 100644 --- a/app/src/main/kotlin/net/primal/android/explore/asearch/ui/MultipleUserPicker.kt +++ b/app/src/main/kotlin/net/primal/android/explore/asearch/ui/MultipleUserPicker.kt @@ -46,9 +46,9 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.launch import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalIconTextField +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalLoadingButton import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.Search @@ -260,11 +260,12 @@ private fun SelectedUsersIndicator( modifier = Modifier.size(54.dp), contentAlignment = Alignment.BottomEnd, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.offset(x = (-6).dp, y = (-6).dp), avatarSize = 48.dp, avatarCdnImage = user.avatarCdnImage, onClick = { onUserClick(user) }, + legendaryCustomization = user.legendaryCustomization, ) Icon( modifier = Modifier diff --git a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeContract.kt b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeContract.kt index a75fbd7eb..3afefb523 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeContract.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeContract.kt @@ -1,13 +1,13 @@ package net.primal.android.explore.home import net.primal.android.attachments.domain.CdnImage -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges interface ExploreHomeContract { data class UiState( val activeAccountAvatarCdnImage: CdnImage? = null, - val activeAccountLegendaryStyle: LegendaryStyle? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val activeAccountPubkey: String? = null, val badges: Badges = Badges(), ) diff --git a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt index 55eb490b1..638cc1b33 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext @@ -52,11 +51,11 @@ import net.primal.android.LocalContentDisplaySettings import net.primal.android.R import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.AppBarIcon -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.IconText import net.primal.android.core.compose.InvisibleAppBarIcon import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopLevelDestination +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.AdvancedSearch import net.primal.android.core.compose.icons.primaliconpack.AvatarDefault @@ -79,7 +78,7 @@ import net.primal.android.explore.home.zaps.ExploreZaps import net.primal.android.feeds.domain.exploreMediaFeedSpec import net.primal.android.notes.feed.grid.MediaFeedGrid import net.primal.android.notes.feed.note.ui.events.NoteCallbacks -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme @@ -143,7 +142,7 @@ private fun ExploreHomeScreen( pagerState = pagerState, actionIcon = PrimalIcons.AdvancedSearch, avatarCdnImage = state.activeAccountAvatarCdnImage, - avatarLegendaryStyle = state.activeAccountLegendaryStyle, + avatarLegendaryCustomization = state.activeAccountLegendaryCustomization, navigationIcon = PrimalIcons.AvatarDefault, onNavigationIconClick = { uiScope.launch { drawerState.open() } @@ -234,7 +233,7 @@ fun ExploreTopAppBar( onNavigationIconClick: () -> Unit, onActionIconClick: () -> Unit, onSearchClick: () -> Unit, - avatarLegendaryStyle: LegendaryStyle? = null, + avatarLegendaryCustomization: LegendaryCustomization? = null, navigationIconTintColor: Color = LocalContentColor.current, scrollBehavior: TopAppBarScrollBehavior? = null, ) { @@ -258,15 +257,11 @@ fun ExploreTopAppBar( .padding(horizontal = 8.dp) .clip(CircleShape), ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarCdnImage = avatarCdnImage, modifier = Modifier.size(32.dp), onClick = onNavigationIconClick, - hasBorder = avatarLegendaryStyle != null, - borderBrush = avatarLegendaryStyle?.brush - ?: Brush.linearGradient( - listOf(AppTheme.colorScheme.primary, AppTheme.colorScheme.primary), - ), + legendaryCustomization = avatarLegendaryCustomization, ) } } else if (navigationIcon != null) { diff --git a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeViewModel.kt index 1c734ba64..eccff7ce1 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import net.primal.android.explore.home.ExploreHomeContract.UiState +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.subscriptions.SubscriptionsManager @@ -34,7 +35,7 @@ class ExploreHomeViewModel @Inject constructor( copy( activeAccountPubkey = it.pubkey, activeAccountAvatarCdnImage = it.avatarCdnImage, - activeAccountLegendaryStyle = if (it.avatarRing) it.legendaryStyle else null, + activeAccountLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt index 2622f79fe..a6d3e650a 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt @@ -36,10 +36,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.HeightAdjustableLoadingLazyListPlaceholder import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.FollowUnfollowButton import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog @@ -184,11 +184,12 @@ private fun ExplorePersonListItem( modifier = Modifier.padding(horizontal = 12.dp), verticalArrangement = Arrangement.Top, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(bottom = 24.dp), avatarSize = 64.dp, avatarCdnImage = person.profile.avatarCdnImage, onClick = onItemClick, + legendaryCustomization = person.profile.legendaryCustomization, ) } @@ -198,6 +199,11 @@ private fun ExplorePersonListItem( NostrUserText( displayName = person.profile.userDisplayName, internetIdentifier = person.profile.internetIdentifier, + customBadgeStyle = if (person.profile.legendaryCustomization?.customBadge == true) { + person.profile.legendaryCustomization.legendaryStyle + } else { + null + }, ) person.profile.internetIdentifier?.let { Text( @@ -233,13 +239,7 @@ private fun ExplorePersonListItem( fontSize = 12.sp, fontWeight = FontWeight.SemiBold, ), - onClick = { - if (isFollowed) { - onUnfollowClick() - } else { - onFollowClick() - } - }, + onClick = { if (isFollowed) onUnfollowClick() else onFollowClick() }, ) FollowersIndicator( diff --git a/app/src/main/kotlin/net/primal/android/explore/home/zaps/ExploreZaps.kt b/app/src/main/kotlin/net/primal/android/explore/home/zaps/ExploreZaps.kt index 5188bc67e..da98b76aa 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/zaps/ExploreZaps.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/zaps/ExploreZaps.kt @@ -34,9 +34,9 @@ import java.text.NumberFormat import java.time.Instant import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.HeightAdjustableLoadingLazyListPlaceholder import net.primal.android.core.compose.ListNoContent +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.asBeforeNowFormat import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.LightningBoltFilled @@ -44,6 +44,7 @@ import net.primal.android.explore.home.zaps.ui.ExploreZapNoteUi import net.primal.android.notes.feed.model.NoteContentUi import net.primal.android.notes.feed.note.ui.NoteContent import net.primal.android.notes.feed.note.ui.events.NoteCallbacks +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme @Composable @@ -134,6 +135,7 @@ fun ZapListItem( senderCdnImage = zapData.sender?.avatarCdnImage, amountSats = zapData.amountSats, message = zapData.zapMessage, + senderLegendaryCustomization = zapData.sender?.legendaryCustomization, ) NoteSummary( noteContent = zapData.noteContentUi, @@ -143,6 +145,7 @@ fun ZapListItem( receiverCdnResource = zapData.receiver?.avatarCdnImage, receiverDisplayName = zapData.receiver?.authorDisplayName, onReceiverAvatarClick = { zapData.receiver?.pubkey?.let { noteCallbacks.onProfileClick?.invoke(it) } }, + receiverLegendaryCustomization = zapData.receiver?.legendaryCustomization, ) } } @@ -152,6 +155,7 @@ private fun NoteSummary( receiverDisplayName: String?, noteContent: NoteContentUi, receiverCdnResource: CdnImage?, + receiverLegendaryCustomization: LegendaryCustomization?, noteTimestamp: Instant, noteCallbacks: NoteCallbacks, onReceiverAvatarClick: () -> Unit, @@ -163,10 +167,11 @@ private fun NoteSummary( .padding(start = 2.dp, end = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = receiverCdnResource, avatarSize = 38.dp, onClick = onReceiverAvatarClick, + legendaryCustomization = receiverLegendaryCustomization, ) Column( verticalArrangement = Arrangement.spacedBy(4.dp), @@ -213,6 +218,7 @@ private fun NoteSummary( private fun ZapHeader( onSenderAvatarClick: () -> Unit, senderCdnImage: CdnImage?, + senderLegendaryCustomization: LegendaryCustomization?, amountSats: ULong, message: String?, ) { @@ -226,10 +232,11 @@ private fun ZapHeader( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = senderCdnImage, avatarSize = 38.dp, onClick = onSenderAvatarClick, + legendaryCustomization = senderLegendaryCustomization, ) val numberFormat = NumberFormat.getNumberInstance() Row( diff --git a/app/src/main/kotlin/net/primal/android/explore/search/ui/UserProfileListItem.kt b/app/src/main/kotlin/net/primal/android/explore/search/ui/UserProfileListItem.kt index b3e2fd1d0..3d3a1c270 100644 --- a/app/src/main/kotlin/net/primal/android/explore/search/ui/UserProfileListItem.kt +++ b/app/src/main/kotlin/net/primal/android/explore/search/ui/UserProfileListItem.kt @@ -23,12 +23,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.FollowUnfollowButton import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.compose.profile.model.UserProfileItemUi import net.primal.android.core.utils.formatNip05Identifier import net.primal.android.core.utils.shortened +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.theme.AppTheme @Composable @@ -46,9 +48,10 @@ fun UserProfileListItem( modifier = Modifier.clickable { onClick(data) }, colors = colors, leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = data.avatarCdnImage, onClick = { onClick(data) }, + legendaryCustomization = data.legendaryCustomization, ) }, headlineContent = { @@ -108,6 +111,56 @@ fun UserProfileListItem( ) } +@Preview +@Composable +fun PreviewLegendaryUserProfileListItemWithFollow() { + PrimalPreview(primalTheme = net.primal.android.theme.domain.PrimalTheme.Sunset) { + Surface { + UserProfileListItem( + data = UserProfileItemUi( + profileId = "b10b0d5e5fae9c6c48a8c77f7e5abd42a79e9480e25a4094051d4ba4ce14456b", + displayName = "alex", + internetIdentifier = "alex@primal.net", + followersCount = 12345, + legendaryCustomization = LegendaryCustomization( + avatarGlow = true, + customBadge = true, + legendaryStyle = LegendaryStyle.GOLD, + ), + ), + followUnfollowVisibility = FollowUnfollowVisibility.Visible, + isFollowed = false, + onClick = {}, + ) + } + } +} + +@Preview +@Composable +fun PreviewLegendaryUserProfileListItemWithUnfollow() { + PrimalPreview(primalTheme = net.primal.android.theme.domain.PrimalTheme.Sunset) { + Surface { + UserProfileListItem( + data = UserProfileItemUi( + profileId = "b10b0d5e5fae9c6c48a8c77f7e5abd42a79e9480e25a4094051d4ba4ce14456b", + displayName = "alex", + internetIdentifier = "alex@primal.net", + followersCount = 12345, + legendaryCustomization = LegendaryCustomization( + avatarGlow = true, + customBadge = true, + legendaryStyle = LegendaryStyle.SUN_FIRE, + ), + ), + followUnfollowVisibility = FollowUnfollowVisibility.Visible, + isFollowed = true, + onClick = {}, + ) + } + } +} + @Preview @Composable fun PreviewUserProfileListItemWithFollow() { diff --git a/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedListItem.kt b/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedListItem.kt index 7ebaed73b..96774fca0 100644 --- a/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedListItem.kt +++ b/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedListItem.kt @@ -43,8 +43,8 @@ import java.text.NumberFormat import net.primal.android.R import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.AvatarOverlap -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.AvatarThumbnailsRow +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.FeedLikes import net.primal.android.core.compose.icons.primaliconpack.FeedLikesFilled @@ -247,6 +247,7 @@ private fun DvmFeedListItem( height = profileAvatarSize, ), avatarCdnImages = dvmFeed.actionUserAvatars, + avatarLegendaryCustomizations = dvmFeed.actionUserLegendaryCustomizations, onClick = {}, maxAvatarsToShow = MaxAvatarsToShow, displayAvatarOverflowIndicator = false, @@ -276,7 +277,7 @@ fun DvmFeedThumbnail( modifier = Modifier.size(avatarSize), contentAlignment = Alignment.BottomEnd, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = avatarCdnImage, avatarSize = avatarSize, ) diff --git a/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedUi.kt b/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedUi.kt index 9d3a3cb76..10b0be374 100644 --- a/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedUi.kt +++ b/app/src/main/kotlin/net/primal/android/feeds/dvm/ui/DvmFeedUi.kt @@ -2,6 +2,7 @@ package net.primal.android.feeds.dvm.ui import net.primal.android.attachments.domain.CdnImage import net.primal.android.feeds.domain.DvmFeed +import net.primal.android.premium.legend.LegendaryCustomization data class DvmFeedUi( val data: DvmFeed, @@ -10,4 +11,5 @@ data class DvmFeedUi( val totalLikes: Long? = null, val totalSatsZapped: Long? = null, val actionUserAvatars: List = emptyList(), + val actionUserLegendaryCustomizations: List = emptyList(), ) diff --git a/app/src/main/kotlin/net/primal/android/feeds/repository/DvmFeedListHandler.kt b/app/src/main/kotlin/net/primal/android/feeds/repository/DvmFeedListHandler.kt index 2cfdfc2bb..072e2b1f1 100644 --- a/app/src/main/kotlin/net/primal/android/feeds/repository/DvmFeedListHandler.kt +++ b/app/src/main/kotlin/net/primal/android/feeds/repository/DvmFeedListHandler.kt @@ -8,6 +8,7 @@ import net.primal.android.core.ext.asMapByKey import net.primal.android.feeds.domain.FeedSpecKind import net.primal.android.feeds.dvm.ui.DvmFeedUi import net.primal.android.networking.primal.retryNetworkCall +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.repository.ProfileRepository import net.primal.android.stats.repository.EventRepository @@ -33,15 +34,19 @@ class DvmFeedListHandler @Inject constructor( .asMapByKey { it.eventId } var feeds = dvmFeeds.map { dvmFeed -> + val actionUsers = profileRepository.findProfilesData(profileIds = dvmFeed.actionUserIds) + val avatarLegendaryCustomizationPair = actionUsers + .filter { it.avatarCdnImage != null } + .map { Pair(it.avatarCdnImage, it.primalLegendProfile?.asLegendaryCustomization()) } + DvmFeedUi( data = dvmFeed, userLiked = userStats[dvmFeed.eventId]?.liked, userZapped = userStats[dvmFeed.eventId]?.zapped, totalLikes = stats[dvmFeed.eventId]?.likes, totalSatsZapped = stats[dvmFeed.eventId]?.satsZapped, - actionUserAvatars = profileRepository - .findProfilesData(profileIds = dvmFeed.actionUserIds) - .mapNotNull { it.avatarCdnImage }, + actionUserAvatars = avatarLegendaryCustomizationPair.mapNotNull { it.first }, + actionUserLegendaryCustomizations = avatarLegendaryCustomizationPair.map { it.second }, ) } update(feeds) diff --git a/app/src/main/kotlin/net/primal/android/messages/chat/ChatScreen.kt b/app/src/main/kotlin/net/primal/android/messages/chat/ChatScreen.kt index 4336f453b..06b87b1c5 100644 --- a/app/src/main/kotlin/net/primal/android/messages/chat/ChatScreen.kt +++ b/app/src/main/kotlin/net/primal/android/messages/chat/ChatScreen.kt @@ -54,11 +54,11 @@ import java.time.Instant import kotlin.time.Duration.Companion.minutes import net.primal.android.R import net.primal.android.core.compose.AppBarIcon -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.PrimalDefaults import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.asBeforeNowFormat import net.primal.android.core.compose.foundation.rememberLazyListStatePagingWorkaround import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholder @@ -138,10 +138,11 @@ fun ChatScreen( .padding(horizontal = 8.dp) .clip(CircleShape), ) { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = state.participantProfile?.avatarCdnImage, modifier = Modifier.size(32.dp), onClick = { noteCallbacks.onProfileClick?.invoke(state.participantId) }, + legendaryCustomization = state.participantProfile?.legendaryCustomization, ) } }, diff --git a/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListScreen.kt b/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListScreen.kt index 7e0ddf4c7..3d18bd4b2 100644 --- a/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListScreen.kt +++ b/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListScreen.kt @@ -60,11 +60,11 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.asBeforeNowFormat import net.primal.android.core.compose.foundation.rememberLazyListStatePagingWorkaround import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholder @@ -280,9 +280,10 @@ private fun ConversationListItem( }, colors = ListItemDefaults.colors(containerColor = AppTheme.colorScheme.surfaceVariant), leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = conversation.participantAvatarCdnImage, onClick = { onProfileClick(conversation.participantId) }, + legendaryCustomization = conversation.participantLegendaryCustomization, ) }, headlineContent = { @@ -311,6 +312,7 @@ private fun ConversationListItem( displayName = conversation.participantUsername, internetIdentifier = conversation.participantInternetIdentifier, annotatedStringSuffixBuilder = { append(suffixText) }, + customBadgeStyle = conversation.participantLegendaryCustomization?.legendaryStyle, style = AppTheme.typography.bodyMedium, ) } diff --git a/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListViewModel.kt b/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListViewModel.kt index dbc7e0927..29122713b 100644 --- a/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/messages/conversation/MessageConversationListViewModel.kt @@ -26,6 +26,7 @@ import net.primal.android.messages.domain.ConversationRelation import net.primal.android.messages.repository.MessageRepository import net.primal.android.networking.sockets.errors.WssException import net.primal.android.notes.feed.model.asNoteNostrUriUi +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.subscriptions.SubscriptionsManager import timber.log.Timber @@ -139,6 +140,7 @@ class MessageConversationListViewModel @Inject constructor( isLastMessageFromUser = this.lastMessage?.senderId == activeUserId, participantInternetIdentifier = this.participant?.internetIdentifier, participantAvatarCdnImage = this.participant?.avatarCdnImage, + participantLegendaryCustomization = this.participant?.primalLegendProfile?.asLegendaryCustomization(), unreadMessagesCount = this.data.unreadMessagesCount, ) } diff --git a/app/src/main/kotlin/net/primal/android/messages/conversation/model/MessageConversationUi.kt b/app/src/main/kotlin/net/primal/android/messages/conversation/model/MessageConversationUi.kt index 701d6687f..86a424ee8 100644 --- a/app/src/main/kotlin/net/primal/android/messages/conversation/model/MessageConversationUi.kt +++ b/app/src/main/kotlin/net/primal/android/messages/conversation/model/MessageConversationUi.kt @@ -4,6 +4,7 @@ import java.time.Instant import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.attachment.model.NoteAttachmentUi import net.primal.android.notes.feed.model.NoteNostrUriUi +import net.primal.android.premium.legend.LegendaryCustomization data class MessageConversationUi( val participantId: String, @@ -16,5 +17,6 @@ data class MessageConversationUi( val isLastMessageFromUser: Boolean, val participantInternetIdentifier: String? = null, val participantAvatarCdnImage: CdnImage? = null, + val participantLegendaryCustomization: LegendaryCustomization? = null, val unreadMessagesCount: Int = 0, ) diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedList.kt b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedList.kt index 7452eb53f..ff6aff7e7 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedList.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedList.kt @@ -329,6 +329,7 @@ private fun NewPostsButton(syncStats: FeedPostsSyncStats, onClick: () -> Unit) { AvatarThumbnailsRow( modifier = Modifier.padding(start = 6.dp), avatarCdnImages = syncStats.latestAvatarCdnImages, + avatarLegendaryCustomizations = syncStats.latestLegendaryCustomizations, onClick = { onClick() }, ) diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt index c9902666b..88c675498 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt @@ -39,6 +39,7 @@ import net.primal.android.notes.feed.list.NoteFeedContract.UiState import net.primal.android.notes.feed.model.FeedPostsSyncStats import net.primal.android.notes.feed.model.asFeedPostUi import net.primal.android.notes.repository.FeedRepository +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.premium.utils.hasPremiumMembership import net.primal.android.user.accounts.active.ActiveAccountStore import timber.log.Timber @@ -182,15 +183,23 @@ class NoteFeedViewModel @AssistedInject constructor( primalUserNames = primalUserNames, primalLegendProfiles = primalLegendProfiles, ) - val avatarCdnImages = allNotes - .mapNotNull { note -> profiles.find { it.ownerId == note.pubKey }?.avatarCdnImage } + val avatarCdnImagesAndLegendaryCustomizations = allNotes + .mapNotNull { note -> profiles.find { it.ownerId == note.pubKey } } + .filter { profileData -> profileData.avatarCdnImage != null } + .map { profileData -> + Pair( + profileData.avatarCdnImage, + profileData.primalLegendProfile?.asLegendaryCustomization(), + ) + } .distinct() - val limit = if (avatarCdnImages.count() <= MAX_AVATARS) avatarCdnImages.count() else MAX_AVATARS + val limit = avatarCdnImagesAndLegendaryCustomizations.count().coerceAtMost(MAX_AVATARS) val newSyncStats = FeedPostsSyncStats( latestNoteIds = allNotes.map { it.id }, - latestAvatarCdnImages = avatarCdnImages.take(limit), + latestAvatarCdnImages = avatarCdnImagesAndLegendaryCustomizations.mapNotNull { it.first }.take(limit), + latestLegendaryCustomizations = avatarCdnImagesAndLegendaryCustomizations.map { it.second }.take(limit), ) if (newSyncStats.isTopVisibleNoteTheLatestNote()) { diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostUi.kt b/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostUi.kt index 0bda3cbf3..3a31fb3d3 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostUi.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostUi.kt @@ -9,7 +9,8 @@ import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.core.utils.formatNip05Identifier import net.primal.android.core.utils.usernameUiFriendly import net.primal.android.notes.db.FeedPost -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.stats.ui.EventZapUiModel import net.primal.android.stats.ui.asEventZapUiModel @@ -33,9 +34,7 @@ data class FeedPostUi( val replyToAuthorHandle: String? = null, val isBookmarked: Boolean = false, val eventZaps: List = emptyList(), - val authorLegendAvatarGlow: Boolean = false, - val authorLegendCustomBadge: Boolean = false, - val authorLegendaryStyle: LegendaryStyle? = null, + val authorLegendaryCustomization: LegendaryCustomization? = null, ) fun FeedPost.asFeedPostUi(): FeedPostUi { @@ -61,8 +60,6 @@ fun FeedPost.asFeedPostUi(): FeedPostUi { eventZaps = this.eventZaps .map { it.asEventZapUiModel() } .sortedWith(EventZapUiModel.DefaultComparator), - authorLegendAvatarGlow = this.author?.primalLegendProfile?.avatarGlow == true, - authorLegendCustomBadge = this.author?.primalLegendProfile?.customBadge == true, - authorLegendaryStyle = LegendaryStyle.valueById(this.author?.primalLegendProfile?.styleId), + authorLegendaryCustomization = this.author?.primalLegendProfile?.asLegendaryCustomization(), ) } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostsSyncStats.kt b/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostsSyncStats.kt index 005ce9105..5bfd3018c 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostsSyncStats.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/model/FeedPostsSyncStats.kt @@ -1,8 +1,10 @@ package net.primal.android.notes.feed.model import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization data class FeedPostsSyncStats( val latestNoteIds: List = emptyList(), val latestAvatarCdnImages: List = emptyList(), + val latestLegendaryCustomizations: List = emptyList(), ) diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt index c78390596..62b75c496 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt @@ -28,8 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler @@ -44,8 +42,8 @@ import java.time.temporal.ChronoUnit import kotlinx.coroutines.launch import net.primal.android.LocalContentDisplaySettings import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.PrimalDivider +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.compose.profile.approvals.ApproveBookmarkAlertDialog import net.primal.android.core.errors.UiError @@ -413,20 +411,16 @@ private fun FeedNote( Row { if (!fullWidthContent) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( modifier = Modifier.padding(avatarPaddingValues), avatarSize = avatarSizeDp, avatarCdnImage = data.authorAvatarCdnImage, + legendaryCustomization = data.authorLegendaryCustomization, onClick = if (noteCallbacks.onProfileClick != null) { { noteCallbacks.onProfileClick.invoke(data.authorId) } } else { null }, - hasBorder = data.authorLegendAvatarGlow && data.authorLegendaryStyle != null, - borderBrush = when { - data.authorLegendaryStyle != null -> data.authorLegendaryStyle.brush - else -> Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, ) } @@ -445,9 +439,7 @@ private fun FeedNote( authorDisplayName = data.authorName, authorAvatarCdnImage = data.authorAvatarCdnImage, authorInternetIdentifier = data.authorInternetIdentifier, - authorLegendAvatarGlow = data.authorLegendAvatarGlow, - authorLegendCustomBadge = data.authorLegendCustomBadge, - authorLegendaryStyle = data.authorLegendaryStyle, + authorLegendaryCustomization = data.authorLegendaryCustomization, replyToAuthor = if (showReplyTo) data.replyToAuthorHandle else null, onAuthorAvatarClick = if (noteCallbacks.onProfileClick != null) { { noteCallbacks.onProfileClick.invoke(data.authorId) } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/NoteHeader.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/NoteHeader.kt index 6f1a52054..b071b079a 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/NoteHeader.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/NoteHeader.kt @@ -11,8 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -27,13 +25,13 @@ import java.time.Instant import kotlin.time.Duration.Companion.seconds import net.primal.android.LocalContentDisplaySettings import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.ReplyingToText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.WrappedContentWithSuffix import net.primal.android.core.compose.asBeforeNowFormat import net.primal.android.core.utils.formatNip05Identifier -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme import net.primal.android.theme.PrimalTheme import net.primal.android.theme.domain.PrimalTheme @@ -49,9 +47,7 @@ fun FeedNoteHeader( authorAvatarVisible: Boolean = true, authorAvatarCdnImage: CdnImage? = null, authorInternetIdentifier: String? = null, - authorLegendAvatarGlow: Boolean = false, - authorLegendCustomBadge: Boolean = false, - authorLegendaryStyle: LegendaryStyle? = null, + authorLegendaryCustomization: LegendaryCustomization? = null, replyToAuthor: String? = null, label: String? = authorInternetIdentifier, labelStyle: TextStyle? = null, @@ -68,15 +64,11 @@ fun FeedNoteHeader( verticalAlignment = Alignment.CenterVertically, ) { if (authorAvatarVisible) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarCdnImage = authorAvatarCdnImage, avatarSize = authorAvatarSize, onClick = onAuthorAvatarClick, - hasBorder = authorLegendAvatarGlow && authorLegendaryStyle != null, - borderBrush = when { - authorLegendaryStyle != null -> authorLegendaryStyle.brush - else -> Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, + legendaryCustomization = authorLegendaryCustomization, ) } @@ -96,32 +88,14 @@ fun FeedNoteHeader( ) } - WrappedContentWithSuffix( - wrappedContent = { - NostrUserText( - displayName = authorDisplayName, - internetIdentifier = authorInternetIdentifier, - annotatedStringSuffixBuilder = { - append(suffixText) - }, - style = topRowTextStyle, - internetIdentifierBadgeSize = topRowTextStyle.fontSize.value.dp, - overflow = TextOverflow.Ellipsis, - customBadgeStyle = if (authorLegendCustomBadge) authorLegendaryStyle else null, - ) - }, - suffixFixedContent = { - if (postTimestamp != null) { - Text( - text = " • ${postTimestamp.asBeforeNowFormat()}", - textAlign = TextAlign.Center, - maxLines = 1, - style = topRowTextStyle, - fontSize = (displaySettings.contentAppearance.noteUsernameSize.value).sp, - color = AppTheme.extraColorScheme.onSurfaceVariantAlt2, - ) - } - }, + NoteAuthorBadgeAndTimestampSection( + authorDisplayName = authorDisplayName, + authorInternetIdentifier = authorInternetIdentifier, + suffixText = suffixText, + topRowTextStyle = topRowTextStyle, + authorLegendaryCustomization = authorLegendaryCustomization, + postTimestamp = postTimestamp, + displaySettings = displaySettings, ) if (!label.isNullOrEmpty() && !singleLine) { @@ -146,6 +120,49 @@ fun FeedNoteHeader( } } +@Composable +private fun NoteAuthorBadgeAndTimestampSection( + authorDisplayName: String, + authorInternetIdentifier: String?, + suffixText: AnnotatedString, + topRowTextStyle: TextStyle, + authorLegendaryCustomization: LegendaryCustomization?, + postTimestamp: Instant?, + displaySettings: ContentDisplaySettings, +) { + WrappedContentWithSuffix( + wrappedContent = { + NostrUserText( + displayName = authorDisplayName, + internetIdentifier = authorInternetIdentifier, + annotatedStringSuffixBuilder = { + append(suffixText) + }, + style = topRowTextStyle, + internetIdentifierBadgeSize = topRowTextStyle.fontSize.value.dp, + overflow = TextOverflow.Ellipsis, + customBadgeStyle = if (authorLegendaryCustomization?.customBadge == true) { + authorLegendaryCustomization.legendaryStyle + } else { + null + }, + ) + }, + suffixFixedContent = { + if (postTimestamp != null) { + Text( + text = " • ${postTimestamp.asBeforeNowFormat()}", + textAlign = TextAlign.Center, + maxLines = 1, + style = topRowTextStyle, + fontSize = (displaySettings.contentAppearance.noteUsernameSize.value).sp, + color = AppTheme.extraColorScheme.onSurfaceVariantAlt2, + ) + } + }, + ) +} + @Preview @Composable fun PreviewLightNoteHeader() { diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedNoteCard.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedNoteCard.kt index e6aa52f0c..7e22ba00c 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedNoteCard.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedNoteCard.kt @@ -55,6 +55,7 @@ fun ReferencedNoteCard( singleLine = true, authorAvatarSize = 30.dp, authorAvatarCdnImage = data.authorAvatarCdnImage, + authorLegendaryCustomization = data.authorLegendaryCustomization, authorInternetIdentifier = data.authorInternetIdentifier, onAuthorAvatarClick = { noteCallbacks.onProfileClick?.invoke(data.authorId) }, ) diff --git a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedContract.kt b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedContract.kt index 0b8ff105f..2978816e2 100644 --- a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedContract.kt +++ b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedContract.kt @@ -2,7 +2,7 @@ package net.primal.android.notes.home import net.primal.android.attachments.domain.CdnImage import net.primal.android.feeds.list.ui.model.FeedUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges interface HomeFeedContract { @@ -10,7 +10,7 @@ interface HomeFeedContract { data class UiState( val feeds: List = emptyList(), val activeAccountAvatarCdnImage: CdnImage? = null, - val activeAccountLegendaryStyle: LegendaryStyle? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val badges: Badges = Badges(), val loading: Boolean = true, ) diff --git a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedScreen.kt b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedScreen.kt index 02bfbfe7a..eaae05a84 100644 --- a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedScreen.kt +++ b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedScreen.kt @@ -67,7 +67,7 @@ import net.primal.android.feeds.list.ui.model.FeedUi import net.primal.android.notes.feed.list.NoteFeedList import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.notes.home.HomeFeedContract.UiEvent -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme @Composable @@ -166,7 +166,7 @@ fun HomeFeedScreen( title = activeFeed?.title ?: "", activeFeed = activeFeed, avatarCdnImage = state.activeAccountAvatarCdnImage, - avatarLegendaryStyle = state.activeAccountLegendaryStyle, + avatarLegendaryCustomization = state.activeAccountLegendaryCustomization, onAvatarClick = { uiScope.launch { drawerState.open() } }, onSearchClick = onSearchClick, onFeedChanged = { feed -> @@ -255,7 +255,7 @@ private fun NoteFeedTopAppBar( onSearchClick: () -> Unit, activeFeed: FeedUi?, onFeedChanged: (FeedUi) -> Unit, - avatarLegendaryStyle: LegendaryStyle? = null, + avatarLegendaryCustomization: LegendaryCustomization? = null, scrollBehavior: TopAppBarScrollBehavior? = null, onGoToWallet: (() -> Unit)? = null, ) { @@ -284,7 +284,7 @@ private fun NoteFeedTopAppBar( } }, avatarCdnImage = avatarCdnImage, - avatarLegendaryStyle = avatarLegendaryStyle, + legendaryCustomization = avatarLegendaryCustomization, navigationIcon = PrimalIcons.AvatarDefault, onNavigationIconClick = onAvatarClick, actions = { diff --git a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedViewModel.kt index 516554f60..4404dd9e0 100644 --- a/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/notes/home/HomeFeedViewModel.kt @@ -20,6 +20,7 @@ import net.primal.android.networking.primal.retryNetworkCall import net.primal.android.networking.sockets.errors.WssException import net.primal.android.notes.home.HomeFeedContract.UiEvent import net.primal.android.notes.home.HomeFeedContract.UiState +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.subscriptions.SubscriptionsManager import net.primal.android.user.updater.UserDataUpdater @@ -116,7 +117,7 @@ class HomeFeedViewModel @Inject constructor( setState { copy( activeAccountAvatarCdnImage = it.avatarCdnImage, - activeAccountLegendaryStyle = if (it.avatarRing) it.legendaryStyle else null, + activeAccountLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsContract.kt b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsContract.kt index 84258a270..aae820747 100644 --- a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsContract.kt +++ b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsContract.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingData import kotlinx.coroutines.flow.Flow import net.primal.android.attachments.domain.CdnImage import net.primal.android.notifications.list.ui.NotificationUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges interface NotificationsContract { @@ -13,7 +13,7 @@ interface NotificationsContract { val seenNotifications: Flow>, val unseenNotifications: List> = emptyList(), val activeAccountAvatarCdnImage: CdnImage? = null, - val activeAccountLegendaryStyle: LegendaryStyle? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val badges: Badges = Badges(), ) diff --git a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsScreen.kt b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsScreen.kt index 54c7bb899..5ef6a8239 100644 --- a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsScreen.kt @@ -158,7 +158,7 @@ fun NotificationsScreen( PrimalTopAppBar( title = stringResource(id = R.string.notifications_title), avatarCdnImage = state.activeAccountAvatarCdnImage, - avatarLegendaryStyle = state.activeAccountLegendaryStyle, + legendaryCustomization = state.activeAccountLegendaryCustomization, navigationIcon = PrimalIcons.AvatarDefault, onNavigationIconClick = { uiScope.launch { drawerState.open() } diff --git a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsViewModel.kt b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsViewModel.kt index 76c9ecd69..5608eb4bd 100644 --- a/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/notifications/list/NotificationsViewModel.kt @@ -30,6 +30,7 @@ import net.primal.android.notifications.list.NotificationsContract.UiEvent.Notif import net.primal.android.notifications.list.NotificationsContract.UiState import net.primal.android.notifications.list.ui.NotificationUi import net.primal.android.notifications.repository.NotificationRepository +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.subscriptions.SubscriptionsManager import timber.log.Timber @@ -77,7 +78,7 @@ class NotificationsViewModel @Inject constructor( setState { copy( activeAccountAvatarCdnImage = it.avatarCdnImage, - activeAccountLegendaryStyle = if (it.avatarRing) it.legendaryStyle else null, + activeAccountLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } @@ -152,6 +153,7 @@ class NotificationsViewModel @Inject constructor( ?: this.data.actionUserId?.asEllipsizedNpub(), actionUserInternetIdentifier = this.actionByUser?.internetIdentifier, actionUserAvatarCdnImage = this.actionByUser?.avatarCdnImage, + actionUserLegendaryCustomization = this.actionByUser?.primalLegendProfile?.asLegendaryCustomization(), actionUserSatsZapped = this.data.satsZapped, actionPost = this.extractFeedPostUi(), ) diff --git a/app/src/main/kotlin/net/primal/android/notifications/list/ui/NotificationListItem.kt b/app/src/main/kotlin/net/primal/android/notifications/list/ui/NotificationListItem.kt index 139600fb2..26436deaa 100644 --- a/app/src/main/kotlin/net/primal/android/notifications/list/ui/NotificationListItem.kt +++ b/app/src/main/kotlin/net/primal/android/notifications/list/ui/NotificationListItem.kt @@ -48,6 +48,8 @@ import net.primal.android.notes.feed.note.ui.FeedNoteActionsRow import net.primal.android.notes.feed.note.ui.NoteContent import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.notifications.domain.NotificationType +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme.Sunset @@ -331,6 +333,7 @@ private fun NotificationsGroupHeader( AvatarThumbnailsRow( modifier = Modifier.fillMaxWidth(), avatarCdnImages = notifications.map { it.actionUserAvatarCdnImage }, + avatarLegendaryCustomizations = notifications.map { it.actionUserLegendaryCustomization }, avatarOverlap = AvatarOverlap.None, hasAvatarBorder = false, onClick = { index -> @@ -385,6 +388,7 @@ private fun HeaderContent( if (showAvatars) { AvatarThumbnailsRow( avatarCdnImages = notifications.map { it.actionUserAvatarCdnImage }, + avatarLegendaryCustomizations = notifications.map { it.actionUserLegendaryCustomization }, avatarOverlap = AvatarOverlap.None, hasAvatarBorder = false, onClick = { index -> @@ -559,6 +563,10 @@ private class NotificationsParameterProvider : PreviewParameterProvider Unit, ) { val snackbarHostState = remember { SnackbarHostState() } - var customBadge by remember(state.customBadge) { mutableStateOf(state.customBadge) } - var avatarGlow by remember(state.avatarGlow) { mutableStateOf(state.avatarGlow) } - var selectedStyle by remember(state.legendaryStyle) { mutableStateOf(state.legendaryStyle) } + var customBadge by remember(state.avatarLegendaryCustomization?.customBadge) { + mutableStateOf(state.avatarLegendaryCustomization?.customBadge) + } + var avatarGlow by remember(state.avatarLegendaryCustomization?.avatarGlow) { + mutableStateOf(state.avatarLegendaryCustomization?.avatarGlow) + } + var selectedStyle by remember(state.avatarLegendaryCustomization?.legendaryStyle) { + mutableStateOf(state.avatarLegendaryCustomization?.legendaryStyle) + } Scaffold( topBar = { @@ -98,9 +104,9 @@ fun LegendaryProfileCustomizationScreen( onClick = { eventPublisher( LegendaryProfileCustomizationContract.UiEvent.ApplyCustomization( - customBadge = customBadge, - avatarGlow = avatarGlow, - style = selectedStyle, + customBadge = customBadge ?: false, + avatarGlow = avatarGlow ?: false, + style = selectedStyle ?: LegendaryStyle.NO_CUSTOMIZATION, ), ) }, @@ -123,15 +129,14 @@ fun LegendaryProfileCustomizationScreen( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarCdnImage = state.avatarCdnImage, avatarSize = 80.dp, - hasBorder = true, - borderBrush = if (avatarGlow) { - selectedStyle.brush - } else { - Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, + legendaryCustomization = LegendaryCustomization( + avatarGlow = avatarGlow == true, + customBadge = customBadge == true, + legendaryStyle = selectedStyle, + ), ) Spacer(modifier = Modifier.height(16.dp)) val primalName = state.membership?.premiumName ?: "" @@ -140,7 +145,7 @@ fun LegendaryProfileCustomizationScreen( displayName = primalName, internetIdentifier = "$primalName@primal.net", internetIdentifierBadgeSize = 24.dp, - customBadgeStyle = if (customBadge) selectedStyle else null, + customBadgeStyle = if (customBadge == true) selectedStyle else null, fontSize = 20.sp, ) } @@ -150,7 +155,7 @@ fun LegendaryProfileCustomizationScreen( firstCohort = state.membership.cohort1, secondCohort = state.membership.cohort2, membershipExpired = state.membership.isExpired(), - legendaryStyle = selectedStyle, + legendaryStyle = selectedStyle ?: LegendaryStyle.NO_CUSTOMIZATION, ) PrimalDivider(modifier = Modifier.padding(top = 16.dp)) @@ -159,7 +164,7 @@ fun LegendaryProfileCustomizationScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = 32.dp, vertical = 8.dp), - activeLegendaryStyle = selectedStyle, + activeLegendaryStyle = selectedStyle ?: LegendaryStyle.NO_CUSTOMIZATION, onStyleChanged = { selectedStyle = it }, ) @@ -167,9 +172,9 @@ fun LegendaryProfileCustomizationScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - avatarRing = avatarGlow, + avatarRing = avatarGlow == true, onAvatarRingChanged = { avatarGlow = it }, - customBadge = customBadge, + customBadge = customBadge == true, onCustomBadgeChanged = { customBadge = it }, ) diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/custimization/LegendaryProfileCustomizationViewModel.kt b/app/src/main/kotlin/net/primal/android/premium/legend/custimization/LegendaryProfileCustomizationViewModel.kt index d006a84bc..ad6fd38bc 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/custimization/LegendaryProfileCustomizationViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/custimization/LegendaryProfileCustomizationViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import net.primal.android.networking.sockets.errors.WssException -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.premium.legend.custimization.LegendaryProfileCustomizationContract.SideEffect import net.primal.android.premium.legend.custimization.LegendaryProfileCustomizationContract.UiEvent import net.primal.android.premium.legend.custimization.LegendaryProfileCustomizationContract.UiState @@ -99,10 +99,7 @@ class LegendaryProfileCustomizationViewModel @Inject constructor( profileRepository.observeProfile(profileId = activeAccountStore.activeUserId()).collect { setState { copy( - avatarGlow = it.metadata?.primalLegendProfile?.avatarGlow == true, - customBadge = it.metadata?.primalLegendProfile?.customBadge == true, - legendaryStyle = LegendaryStyle.valueById(it.metadata?.primalLegendProfile?.styleId) - ?: LegendaryStyle.NO_CUSTOMIZATION, + avatarLegendaryCustomization = it.metadata?.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/ConfirmNameChangeStage.kt b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/ConfirmNameChangeStage.kt index 761da5cb8..de8bc843f 100644 --- a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/ConfirmNameChangeStage.kt +++ b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/ConfirmNameChangeStage.kt @@ -15,8 +15,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.NostrUserText +import net.primal.android.core.compose.UniversalAvatarThumbnail +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.premium.ui.PremiumPrimalNameTable import net.primal.android.theme.AppTheme @@ -25,6 +26,7 @@ fun ConfirmNameChangeStage( modifier: Modifier = Modifier, primalName: String, profileAvatarCdnImage: CdnImage?, + profileLegendaryCustomization: LegendaryCustomization?, contentPadding: PaddingValues, ) { Column( @@ -34,15 +36,21 @@ fun ConfirmNameChangeStage( ) { Spacer(modifier = Modifier.height(48.dp)) - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = profileAvatarCdnImage, avatarSize = 80.dp, + legendaryCustomization = profileLegendaryCustomization, ) NostrUserText( displayName = primalName, internetIdentifier = "$primalName@primal.net", internetIdentifierBadgeSize = 24.dp, fontSize = 20.sp, + customBadgeStyle = if (profileLegendaryCustomization?.customBadge == true) { + profileLegendaryCustomization.legendaryStyle + } else { + null + }, ) Text( modifier = Modifier.padding(horizontal = 12.dp), diff --git a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameContract.kt b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameContract.kt index 5a5ae23fb..78a853777 100644 --- a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameContract.kt +++ b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameContract.kt @@ -1,12 +1,14 @@ package net.primal.android.premium.manage.nameChange import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization interface PremiumChangePrimalNameContract { data class UiState( val stage: ChangePrimalNameStage = ChangePrimalNameStage.PickNew, val primalName: String? = null, val profileAvatarCdnImage: CdnImage? = null, + val profileLegendaryCustomization: LegendaryCustomization? = null, val changingName: Boolean = false, val error: NameChangeError? = null, ) diff --git a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameScreen.kt b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameScreen.kt index 4212ac6ad..527b6c855 100644 --- a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameScreen.kt +++ b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameScreen.kt @@ -166,6 +166,7 @@ private fun ChangePremiumNameConfirmationStage( contentPadding = paddingValues, primalName = state.primalName, profileAvatarCdnImage = state.profileAvatarCdnImage, + profileLegendaryCustomization = state.profileLegendaryCustomization, ) } } diff --git a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameViewModel.kt b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameViewModel.kt index 1f5f1f162..b2b013373 100644 --- a/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/premium/manage/nameChange/PremiumChangePrimalNameViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import net.primal.android.networking.sockets.errors.WssException +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.premium.manage.nameChange.PremiumChangePrimalNameContract.ChangePrimalNameStage import net.primal.android.premium.manage.nameChange.PremiumChangePrimalNameContract.SideEffect import net.primal.android.premium.manage.nameChange.PremiumChangePrimalNameContract.UiEvent @@ -91,6 +92,7 @@ class PremiumChangePrimalNameViewModel @Inject constructor( copy( primalName = it.premiumMembership?.premiumName, profileAvatarCdnImage = it.avatarCdnImage, + profileLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt index db60d283f..8334ce174 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt @@ -139,8 +139,8 @@ private fun ProfileHeaderDetails( displayName = state.profileDetails?.authorDisplayName ?: state.profileId.asEllipsizedNpub(), internetIdentifier = state.profileDetails?.internetIdentifier, isProfileFollowingMe = state.isProfileFollowingMe, - customBadge = state.profileDetails?.customBadge == true, - legendaryStyle = state.profileDetails?.legendaryStyle, + customBadge = state.profileDetails?.legendaryCustomization?.customBadge == true, + legendaryStyle = state.profileDetails?.legendaryCustomization?.legendaryStyle, ) if (state.profileDetails?.internetIdentifier?.isNotEmpty() == true) { @@ -205,6 +205,7 @@ private fun UserFollowedByIndicator( AvatarThumbnailsRow( avatarBorderColor = AppTheme.colorScheme.background, avatarCdnImages = profiles.map { it.avatarCdnImage }, + avatarLegendaryCustomizations = profiles.map { it.legendaryCustomization }, onClick = { onProfileClick(profiles[it].pubkey) }, diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileTopCoverBar.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileTopCoverBar.kt index bbc16d297..4ea90967f 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileTopCoverBar.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileTopCoverBar.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity @@ -37,14 +36,13 @@ import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import net.primal.android.R import net.primal.android.attachments.domain.findNearestOrNull -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalDivider +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack import net.primal.android.core.compose.icons.primaliconpack.Search import net.primal.android.core.utils.asEllipsizedNpub -import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.profile.details.ProfileDetailsContract import net.primal.android.theme.AppTheme @@ -118,7 +116,7 @@ fun ProfileTopCoverBar( .offset(y = avatarValues.avatarOffsetY, x = avatarValues.avatarOffsetX) .padding(horizontal = 16.dp), ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( modifier = Modifier .size(avatarValues.avatarSize) .padding( @@ -129,21 +127,8 @@ fun ProfileTopCoverBar( ), onClick = { state.profileDetails?.avatarCdnImage?.sourceUrl?.let { onMediaItemClick(it) } }, avatarCdnImage = state.profileDetails?.avatarCdnImage, - hasBorder = true, - borderBrush = if (state.profileDetails?.legendaryStyle != null && state.profileDetails.avatarGlow) { - when (state.profileDetails.legendaryStyle) { - LegendaryStyle.NO_CUSTOMIZATION -> Brush.linearGradient( - listOf( - Color.White, - Color.White, - ), - ) - - else -> state.profileDetails.legendaryStyle.brush - } - } else { - Brush.linearGradient(listOf(Color.White, Color.White)) - }, + fallbackBorderColor = Color.White, + legendaryCustomization = state.profileDetails?.legendaryCustomization, ) } } @@ -196,8 +181,8 @@ private fun ProfileTopAppBar( internetIdentifier = state.profileDetails?.internetIdentifier, internetIdentifierBadgeSize = 20.dp, internetIdentifierBadgeAlign = PlaceholderVerticalAlign.Center, - customBadgeStyle = if (state.profileDetails?.customBadge == true) { - state.profileDetails.legendaryStyle + customBadgeStyle = if (state.profileDetails?.legendaryCustomization?.customBadge == true) { + state.profileDetails.legendaryCustomization.legendaryStyle } else { null }, diff --git a/app/src/main/kotlin/net/primal/android/profile/qr/ui/ProfileQrCodeViewer.kt b/app/src/main/kotlin/net/primal/android/profile/qr/ui/ProfileQrCodeViewer.kt index 5d7a33f64..542b36748 100644 --- a/app/src/main/kotlin/net/primal/android/profile/qr/ui/ProfileQrCodeViewer.kt +++ b/app/src/main/kotlin/net/primal/android/profile/qr/ui/ProfileQrCodeViewer.kt @@ -61,9 +61,9 @@ import com.wajahatkarim.flippable.Flippable import com.wajahatkarim.flippable.FlippableState import com.wajahatkarim.flippable.rememberFlipController import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.DefaultAvatarThumbnailPlaceholderListItemImage import net.primal.android.core.compose.PrimalLoadingSpinner +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.Copy import net.primal.android.core.compose.profile.model.ProfileDetailsUi @@ -86,11 +86,12 @@ fun ProfileQrCodeViewer( .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally, ) { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.size(108.dp), avatarCdnImage = profileDetails?.avatarCdnImage, hasBorder = true, - borderColor = Color.White, + fallbackBorderColor = Color.White, + legendaryCustomization = profileDetails?.legendaryCustomization, defaultAvatar = { DefaultAvatarThumbnailPlaceholderListItemImage( backgroundColor = Color.White, diff --git a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsContract.kt b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsContract.kt index 4d629e673..af8e985ae 100644 --- a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsContract.kt +++ b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsContract.kt @@ -1,11 +1,13 @@ package net.primal.android.settings.keys import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization interface KeysSettingsContract { data class UiState( val avatarCdnImage: CdnImage? = null, val nsec: String = "", val npub: String = "", + val legendaryCustomization: LegendaryCustomization? = null, ) } diff --git a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsScreen.kt b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsScreen.kt index 2b822fe12..7c4505659 100644 --- a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsScreen.kt @@ -39,15 +39,16 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.IconText import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalLoadingButton import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack import net.primal.android.core.compose.icons.primaliconpack.Key import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme @@ -87,6 +88,7 @@ fun KeysSettingsScreen(state: KeysSettingsContract.UiState, onClose: () -> Unit) PublicKeySection( npub = state.npub, avatarCdnImage = state.avatarCdnImage, + legendaryCustomization = state.legendaryCustomization, ) } @@ -105,7 +107,11 @@ fun KeysSettingsScreen(state: KeysSettingsContract.UiState, onClose: () -> Unit) } @Composable -fun PublicKeySection(npub: String, avatarCdnImage: CdnImage?) { +fun PublicKeySection( + npub: String, + avatarCdnImage: CdnImage?, + legendaryCustomization: LegendaryCustomization?, +) { val context = LocalContext.current Text( @@ -127,7 +133,10 @@ fun PublicKeySection(npub: String, avatarCdnImage: CdnImage?) { verticalAlignment = Alignment.CenterVertically, ) { Box(modifier = Modifier.padding(start = 16.dp)) { - AvatarThumbnail(avatarCdnImage = avatarCdnImage) + UniversalAvatarThumbnail( + avatarCdnImage = avatarCdnImage, + legendaryCustomization = legendaryCustomization, + ) } Text( modifier = Modifier.padding(all = 16.dp), diff --git a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsViewModel.kt b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsViewModel.kt index ec7302fce..e66c18931 100644 --- a/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/settings/keys/KeysSettingsViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import net.primal.android.crypto.hexToNpubHrp +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.settings.keys.KeysSettingsContract.UiState import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.credentials.CredentialsStore @@ -36,6 +37,7 @@ class KeysSettingsViewModel @Inject constructor( avatarCdnImage = it.avatarCdnImage, nsec = credential.nsec, npub = credential.npub, + legendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), ) } } diff --git a/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsScreen.kt b/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsScreen.kt index 7efb90500..f5c793f75 100644 --- a/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsScreen.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalFilledButton import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack @@ -119,9 +119,10 @@ fun MutedUserListItem( containerColor = AppTheme.colorScheme.surfaceVariant, ), leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = item.avatarCdnImage, onClick = { onProfileClick(item.userId) }, + legendaryCustomization = item.legendaryCustomization, ) }, headlineContent = { diff --git a/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsViewModel.kt b/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsViewModel.kt index bff4823d0..fa72dc4f1 100644 --- a/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/settings/muted/list/MutedSettingsViewModel.kt @@ -15,6 +15,7 @@ import net.primal.android.core.utils.asEllipsizedNpub import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.networking.sockets.errors.WssException +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.settings.muted.db.MutedUser import net.primal.android.settings.muted.list.MutedSettingsContract.UiEvent import net.primal.android.settings.muted.list.MutedSettingsContract.UiState @@ -101,5 +102,6 @@ class MutedSettingsViewModel @Inject constructor( userId = this.mutedAccount.userId, avatarCdnImage = this.profileData?.avatarCdnImage, internetIdentifier = this.profileData?.internetIdentifier, + legendaryCustomization = this.profileData?.primalLegendProfile?.asLegendaryCustomization(), ) } diff --git a/app/src/main/kotlin/net/primal/android/settings/muted/list/model/MutedUserUi.kt b/app/src/main/kotlin/net/primal/android/settings/muted/list/model/MutedUserUi.kt index 569c34e83..3728af7ab 100644 --- a/app/src/main/kotlin/net/primal/android/settings/muted/list/model/MutedUserUi.kt +++ b/app/src/main/kotlin/net/primal/android/settings/muted/list/model/MutedUserUi.kt @@ -1,10 +1,12 @@ package net.primal.android.settings.muted.list.model import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization data class MutedUserUi( val userId: String, val displayName: String, val avatarCdnImage: CdnImage? = null, val internetIdentifier: String? = null, + val legendaryCustomization: LegendaryCustomization? = null, ) diff --git a/app/src/main/kotlin/net/primal/android/stats/db/EventZap.kt b/app/src/main/kotlin/net/primal/android/stats/db/EventZap.kt index 513764fa6..ffff39cb4 100644 --- a/app/src/main/kotlin/net/primal/android/stats/db/EventZap.kt +++ b/app/src/main/kotlin/net/primal/android/stats/db/EventZap.kt @@ -2,6 +2,7 @@ package net.primal.android.stats.db import androidx.room.Entity import net.primal.android.attachments.domain.CdnImage +import net.primal.android.profile.domain.PrimalLegendProfile @Entity( primaryKeys = [ @@ -22,4 +23,5 @@ data class EventZap( val zapSenderHandle: String? = null, val zapSenderInternetIdentifier: String? = null, val zapSenderAvatarCdnImage: CdnImage? = null, + val zapSenderPrimalLegendProfile: PrimalLegendProfile? = null, ) diff --git a/app/src/main/kotlin/net/primal/android/stats/reactions/ui/GenericReactionsLazyColumn.kt b/app/src/main/kotlin/net/primal/android/stats/reactions/ui/GenericReactionsLazyColumn.kt index 5ec05fe52..474593fd4 100644 --- a/app/src/main/kotlin/net/primal/android/stats/reactions/ui/GenericReactionsLazyColumn.kt +++ b/app/src/main/kotlin/net/primal/android/stats/reactions/ui/GenericReactionsLazyColumn.kt @@ -16,10 +16,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalDivider +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholder import net.primal.android.stats.reactions.EventActionUi import net.primal.android.theme.AppTheme @@ -75,10 +75,11 @@ private fun GenericReactionListItem( containerColor = AppTheme.colorScheme.surfaceVariant, ), leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = item.profile.avatarCdnImage, avatarSize = 42.dp, onClick = { onProfileClick(item.profile.pubkey) }, + legendaryCustomization = item.profile.legendaryCustomization, ) }, headlineContent = { diff --git a/app/src/main/kotlin/net/primal/android/stats/reactions/ui/ReactionsZapsLazyColumn.kt b/app/src/main/kotlin/net/primal/android/stats/reactions/ui/ReactionsZapsLazyColumn.kt index 9adb607d1..50efa1960 100644 --- a/app/src/main/kotlin/net/primal/android/stats/reactions/ui/ReactionsZapsLazyColumn.kt +++ b/app/src/main/kotlin/net/primal/android/stats/reactions/ui/ReactionsZapsLazyColumn.kt @@ -20,10 +20,10 @@ import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalDivider +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.foundation.rememberLazyListStatePagingWorkaround import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.FeedZaps @@ -81,10 +81,11 @@ private fun NoteZapListItem(data: EventZapUiModel, onProfileClick: (profileId: S containerColor = AppTheme.colorScheme.surfaceVariant, ), leadingContent = { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = data.zapperAvatarCdnImage, avatarSize = 42.dp, onClick = { onProfileClick(data.zapperId) }, + legendaryCustomization = data.zapperLegendaryCustomization, ) }, headlineContent = { diff --git a/app/src/main/kotlin/net/primal/android/stats/ui/EventZapUiModel.kt b/app/src/main/kotlin/net/primal/android/stats/ui/EventZapUiModel.kt index 3928d0c9f..91679b32f 100644 --- a/app/src/main/kotlin/net/primal/android/stats/ui/EventZapUiModel.kt +++ b/app/src/main/kotlin/net/primal/android/stats/ui/EventZapUiModel.kt @@ -4,6 +4,8 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.core.utils.formatNip05Identifier import net.primal.android.core.utils.usernameUiFriendly +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.stats.db.EventZap import net.primal.android.wallet.utils.CurrencyConversionUtils.toSats @@ -17,6 +19,7 @@ data class EventZapUiModel( val amountInSats: ULong, val zapperInternetIdentifier: String? = null, val zapperAvatarCdnImage: CdnImage? = null, + val zapperLegendaryCustomization: LegendaryCustomization? = null, ) { companion object { val DefaultComparator = compareByDescending { it.amountInSats }.thenBy { it.zappedAt } @@ -34,4 +37,5 @@ fun EventZap.asEventZapUiModel() = zappedAt = this.zapRequestAt, message = this.message, amountInSats = this.amountInBtc.toBigDecimal().toSats(), + zapperLegendaryCustomization = this.zapSenderPrimalLegendProfile?.asLegendaryCustomization(), ) diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt index 60983f246..a276669c7 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt @@ -447,9 +447,7 @@ private fun ArticleContentWithComments( authorCdnImage = state.article.authorAvatarCdnImage, authorDisplayName = state.article.authorDisplayName, authorInternetIdentifier = state.article.authorInternetIdentifier, - authorLegendAvatarGlow = state.article.authorLegendAvatarGlow, - authorLegendCustomBadge = state.article.authorLegendCustomBadge, - authorLegendaryStyle = state.article.authorLegendaryStyle, + authorLegendaryCustomization = state.article.authorLegendaryCustomization, onAuthorAvatarClick = { noteCallbacks.onProfileClick?.invoke(state.article.authorId) }, onFollowUnfollowClick = onFollowUnfollowClick, ) diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleAuthorRow.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleAuthorRow.kt index 9778ff614..22c48f380 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleAuthorRow.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleAuthorRow.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -30,10 +29,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.NostrUserText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalFilledButton import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.theme.AppTheme @@ -44,9 +44,7 @@ fun ArticleAuthorRow( authorDisplayName: String, authorCdnImage: CdnImage? = null, authorInternetIdentifier: String? = null, - authorLegendAvatarGlow: Boolean = false, - authorLegendCustomBadge: Boolean = false, - authorLegendaryStyle: LegendaryStyle? = null, + authorLegendaryCustomization: LegendaryCustomization? = null, onAuthorAvatarClick: (() -> Unit)? = null, onFollowUnfollowClick: (() -> Unit)? = null, ) { @@ -54,15 +52,11 @@ fun ArticleAuthorRow( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - AvatarThumbnailCustomBorder( + UniversalAvatarThumbnail( avatarSize = 42.dp, avatarCdnImage = authorCdnImage, onClick = onAuthorAvatarClick, - hasBorder = authorLegendAvatarGlow && authorLegendaryStyle != null, - borderBrush = when { - authorLegendaryStyle != null -> authorLegendaryStyle.brush - else -> Brush.linearGradient(listOf(Color.Transparent, Color.Transparent)) - }, + legendaryCustomization = authorLegendaryCustomization, ) Column( @@ -78,7 +72,11 @@ fun ArticleAuthorRow( fontSize = 16.sp, lineHeight = 16.sp, ), - customBadgeStyle = if (authorLegendCustomBadge) authorLegendaryStyle else null, + customBadgeStyle = if (authorLegendaryCustomization?.customBadge == true) { + authorLegendaryCustomization.legendaryStyle + } else { + null + }, ) if (!authorInternetIdentifier.isNullOrBlank()) { @@ -206,3 +204,23 @@ fun PreviewArticleAuthorRow() { } } } + +@Preview +@Composable +fun PreviewArticleLegendaryAuthorRow() { + PrimalPreview(primalTheme = net.primal.android.theme.domain.PrimalTheme.Sunset) { + Surface(modifier = Modifier.fillMaxWidth()) { + ArticleAuthorRow( + modifier = Modifier.fillMaxWidth(), + authorFollowed = true, + authorDisplayName = "miljan", + authorInternetIdentifier = "miljan@primal.net", + authorLegendaryCustomization = LegendaryCustomization( + avatarGlow = true, + customBadge = true, + legendaryStyle = LegendaryStyle.SUN_FIRE, + ), + ) + } + } +} diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleDetailsUi.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleDetailsUi.kt index f4db6efc4..7130dd08e 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleDetailsUi.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/ArticleDetailsUi.kt @@ -6,7 +6,8 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.utils.asEllipsizedNpub import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.notes.feed.model.EventStatsUi -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization +import net.primal.android.premium.legend.asLegendaryCustomization data class ArticleDetailsUi( val aTag: String, @@ -26,9 +27,7 @@ data class ArticleDetailsUi( val hashtags: List = emptyList(), val isBookmarked: Boolean = false, val eventStatsUi: EventStatsUi = EventStatsUi(), - val authorLegendAvatarGlow: Boolean = false, - val authorLegendCustomBadge: Boolean = false, - val authorLegendaryStyle: LegendaryStyle? = null, + val authorLegendaryCustomization: LegendaryCustomization? = null, ) fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi { @@ -50,8 +49,6 @@ fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi { hashtags = this.data.hashtags, isBookmarked = this.bookmark != null, eventStatsUi = EventStatsUi.from(eventStats = this.eventStats, userStats = this.userEventStats), - authorLegendAvatarGlow = this.author?.primalLegendProfile?.avatarGlow == true, - authorLegendCustomBadge = this.author?.primalLegendProfile?.customBadge == true, - authorLegendaryStyle = LegendaryStyle.valueById(this.author?.primalLegendProfile?.styleId), + authorLegendaryCustomization = this.author?.primalLegendProfile?.asLegendaryCustomization(), ) } diff --git a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountExt.kt b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountExt.kt index 6a89b7d96..03be4da8f 100644 --- a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountExt.kt +++ b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountExt.kt @@ -19,9 +19,7 @@ fun UserAccount.copyProfileIfNotNull(profile: UserAccount?): UserAccount { avatarCdnImage = profile.avatarCdnImage, internetIdentifier = profile.internetIdentifier, lightningAddress = profile.lightningAddress, - avatarRing = profile.avatarRing, - customBadge = profile.customBadge, - legendaryStyle = profile.legendaryStyle, + primalLegendProfile = profile.primalLegendProfile, ) } else { this diff --git a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt index 25e486cbf..9c460786a 100644 --- a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt +++ b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt @@ -13,7 +13,6 @@ import net.primal.android.nostr.ext.asProfileStatsPO import net.primal.android.nostr.ext.flatMapNotNullAsCdnResource import net.primal.android.nostr.ext.parseAndMapPrimalLegendProfiles import net.primal.android.nostr.ext.parseAndMapPrimalUserNames -import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.user.api.UsersApi import net.primal.android.user.domain.UserAccount import net.primal.android.user.domain.asUserAccountFromFollowListEvent @@ -56,9 +55,7 @@ class UserAccountFetcher @Inject constructor( followingCount = profileStats?.following, notesCount = profileStats?.notesCount, repliesCount = profileStats?.repliesCount, - customBadge = profileData.primalLegendProfile?.customBadge == true, - avatarRing = profileData.primalLegendProfile?.avatarGlow == true, - legendaryStyle = LegendaryStyle.valueById(profileData.primalLegendProfile?.styleId), + primalLegendProfile = profileData.primalLegendProfile, ) } diff --git a/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt b/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt index 2c69338b0..6eee88e91 100644 --- a/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt +++ b/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt @@ -5,7 +5,7 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.utils.asEllipsizedNpub import net.primal.android.nostr.model.primal.content.ContentAppSettings import net.primal.android.premium.domain.PremiumMembership -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.profile.domain.PrimalLegendProfile import net.primal.android.wallet.domain.WalletSettings import net.primal.android.wallet.domain.WalletState @@ -38,9 +38,7 @@ data class UserAccount( val cachingProxyEnabled: Boolean = false, val premiumMembership: PremiumMembership? = null, val lastBuyPremiumTimestampInMillis: Long? = null, - val avatarRing: Boolean = false, - val customBadge: Boolean = false, - val legendaryStyle: LegendaryStyle? = null, + val primalLegendProfile: PrimalLegendProfile? = null, ) { companion object { val EMPTY = UserAccount( diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardContract.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardContract.kt index ddf315e80..5b1e065d5 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardContract.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardContract.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingData import java.math.BigDecimal import kotlinx.coroutines.flow.Flow import net.primal.android.attachments.domain.CdnImage -import net.primal.android.premium.legend.LegendaryStyle +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.user.domain.Badges import net.primal.android.user.domain.PrimalWallet import net.primal.android.user.domain.WalletPreference @@ -14,7 +14,7 @@ interface WalletDashboardContract { data class UiState( val transactions: Flow>, val activeAccountAvatarCdnImage: CdnImage? = null, - val activeAccountLegendaryStyle: LegendaryStyle? = null, + val activeAccountLegendaryCustomization: LegendaryCustomization? = null, val badges: Badges = Badges(), val primalWallet: PrimalWallet? = null, val walletPreference: WalletPreference = WalletPreference.Undefined, diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt index 4f06d6203..d71962814 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt @@ -180,7 +180,7 @@ fun WalletDashboardScreen( modifier = Modifier.onSizeChanged { topBarHeight = it.height }, title = stringResource(id = R.string.wallet_title), avatarCdnImage = state.activeAccountAvatarCdnImage, - avatarLegendaryStyle = state.activeAccountLegendaryStyle, + legendaryCustomization = state.activeAccountLegendaryCustomization, navigationIcon = PrimalIcons.AvatarDefault, onNavigationIconClick = { uiScope.launch { drawerState.open() } diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt index 3faed9f57..487b4aab3 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.networking.sockets.errors.NostrNoticeException import net.primal.android.networking.sockets.errors.WssException +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.domain.WalletPreference import net.primal.android.user.repository.UserRepository @@ -79,7 +80,7 @@ class WalletDashboardViewModel @Inject constructor( setState { copy( activeAccountAvatarCdnImage = it.avatarCdnImage, - activeAccountLegendaryStyle = if (it.avatarRing) it.legendaryStyle else null, + activeAccountLegendaryCustomization = it.primalLegendProfile?.asLegendaryCustomization(), primalWallet = it.primalWallet, walletPreference = it.walletPreference, walletBalance = it.primalWalletState.balanceInBtc?.toBigDecimal(), @@ -162,6 +163,7 @@ class WalletDashboardViewModel @Inject constructor( otherUserId = this.data.otherUserId, otherUserAvatarCdnImage = this.otherProfileData?.avatarCdnImage, otherUserDisplayName = this.otherProfileData?.authorNameUiFriendly(), + otherUserLegendaryCustomization = this.otherProfileData?.primalLegendProfile?.asLegendaryCustomization(), isZap = this.data.isZap, isStorePurchase = this.data.isStorePurchase, isOnChainPayment = this.data.onChainAddress != null, diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt index 641b855ac..2c42eb825 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt @@ -2,6 +2,7 @@ package net.primal.android.wallet.transactions.details import java.time.Instant import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.wallet.domain.TxState import net.primal.android.wallet.domain.TxType @@ -25,4 +26,5 @@ data class TransactionDetailDataUi( val otherUserInternetIdentifier: String? = null, val otherUserDisplayName: String? = null, val otherUserLightningAddress: String? = null, + val otherUserLegendaryCustomization: LegendaryCustomization? = null, ) diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt index 187cc7882..a89309b7f 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt @@ -135,8 +135,6 @@ fun TransactionDetailsScreen( onClose: () -> Unit, noteCallbacks: NoteCallbacks, ) { - val context = LocalContext.current - val uiScope = rememberCoroutineScope() val scrollState = rememberScrollState() val showTopBarDivider by remember { derivedStateOf { scrollState.value > 0 } } val snackbarHostState = remember { SnackbarHostState() } @@ -397,6 +395,7 @@ private fun TransactionCard(txData: TransactionDetailDataUi, onProfileClick: (St authorDisplayName = txData.otherUserDisplayName, authorAvatarCdnImage = txData.otherUserAvatarCdnImage, authorInternetIdentifier = txData.otherUserInternetIdentifier, + authorLegendaryCustomization = txData.otherUserLegendaryCustomization, onAuthorAvatarClick = { txData.otherUserId?.let(onProfileClick) }, label = txData.otherUserLightningAddress, labelStyle = AppTheme.typography.bodyMedium, diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt index f25929706..4b2875504 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt @@ -21,6 +21,7 @@ import net.primal.android.navigation.transactionIdOrThrow import net.primal.android.networking.sockets.errors.WssException import net.primal.android.notes.feed.model.asFeedPostUi import net.primal.android.notes.repository.FeedRepository +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.wallet.db.WalletTransaction import net.primal.android.wallet.repository.WalletRepository import net.primal.android.wallet.transactions.details.TransactionDetailsContract.UiState @@ -128,6 +129,7 @@ class TransactionDetailsViewModel @Inject constructor( otherUserDisplayName = this.otherProfileData?.authorNameUiFriendly(), otherUserInternetIdentifier = this.otherProfileData?.internetIdentifier, otherUserLightningAddress = this.data.otherLightningAddress, + otherUserLegendaryCustomization = this.otherProfileData?.primalLegendProfile?.asLegendaryCustomization(), isZap = this.data.isZap, isStorePurchase = this.data.isStorePurchase, ) diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt index 51cd8f08c..f53f16b7b 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt @@ -45,8 +45,8 @@ import java.time.format.DateTimeFormatter import kotlin.time.Duration.Companion.seconds import net.primal.android.R import net.primal.android.attachments.domain.CdnImage -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.IconText +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.WrappedContentWithSuffix import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.WalletBitcoinPayment @@ -54,6 +54,7 @@ import net.primal.android.core.compose.icons.primaliconpack.WalletLightningPayme import net.primal.android.core.compose.icons.primaliconpack.WalletPay import net.primal.android.core.compose.icons.primaliconpack.WalletReceive import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.theme.AppTheme import net.primal.android.wallet.domain.TxState import net.primal.android.wallet.domain.TxType @@ -97,6 +98,7 @@ fun TransactionListItem( isPending = data.txState.isPending(), otherUserId = data.otherUserId, otherUserAvatarCdnImage = data.otherUserAvatarCdnImage, + otherUserLegendaryCustomization = data.otherUserLegendaryCustomization, onAvatarClick = onAvatarClick, ) }, @@ -133,13 +135,15 @@ private fun TransactionLeadingContent( isPending: Boolean, otherUserId: String?, otherUserAvatarCdnImage: CdnImage?, + otherUserLegendaryCustomization: LegendaryCustomization?, onAvatarClick: (String) -> Unit, ) { when { otherUserId != null -> { - AvatarThumbnail( + UniversalAvatarThumbnail( avatarCdnImage = otherUserAvatarCdnImage, onClick = { onAvatarClick(otherUserId) }, + legendaryCustomization = otherUserLegendaryCustomization, ) } diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItemDataUi.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItemDataUi.kt index eb7738984..5e87086ed 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItemDataUi.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItemDataUi.kt @@ -2,6 +2,7 @@ package net.primal.android.wallet.transactions.list import java.time.Instant import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.wallet.domain.TxState import net.primal.android.wallet.domain.TxType @@ -20,4 +21,5 @@ data class TransactionListItemDataUi( val otherUserId: String? = null, val otherUserAvatarCdnImage: CdnImage? = null, val otherUserDisplayName: String? = null, + val otherUserLegendaryCustomization: LegendaryCustomization? = null, ) diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionContract.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionContract.kt index 2730f2a70..554c47c91 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionContract.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionContract.kt @@ -1,6 +1,7 @@ package net.primal.android.wallet.transactions.send.create import net.primal.android.attachments.domain.CdnImage +import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.wallet.domain.DraftTx import net.primal.android.wallet.transactions.send.create.ui.model.MiningFeeUi @@ -16,6 +17,7 @@ interface CreateTransactionContract { val profileAvatarCdnImage: CdnImage? = null, val profileDisplayName: String? = null, val profileLightningAddress: String? = null, + val profileLegendaryCustomization: LegendaryCustomization? = null, ) { fun isNotInvoice() = transaction.lnInvoice == null && transaction.onChainInvoice == null } diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionViewModel.kt index f8c212ab1..720599441 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionViewModel.kt @@ -16,6 +16,7 @@ import net.primal.android.core.utils.authorNameUiFriendly import net.primal.android.navigation.draftTransaction import net.primal.android.navigation.lnbc import net.primal.android.networking.sockets.errors.WssException +import net.primal.android.premium.legend.asLegendaryCustomization import net.primal.android.profile.db.ProfileData import net.primal.android.profile.repository.ProfileRepository import net.primal.android.scanner.analysis.WalletTextParser @@ -191,6 +192,8 @@ class CreateTransactionViewModel @Inject constructor( setState { copy( profileAvatarCdnImage = this@updateStateWithProfileData.avatarCdnImage, + profileLegendaryCustomization = this@updateStateWithProfileData + .primalLegendProfile?.asLegendaryCustomization(), profileDisplayName = this@updateStateWithProfileData.authorNameUiFriendly(), profileLightningAddress = transaction.targetLud16 ?: this@updateStateWithProfileData.lightningAddress, transaction = if (transaction.targetLud16 == null && diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/ui/TransactionEditor.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/ui/TransactionEditor.kt index 44afe715c..d89b99034 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/ui/TransactionEditor.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/ui/TransactionEditor.kt @@ -52,10 +52,10 @@ import androidx.compose.ui.unit.sp import java.math.BigDecimal import java.text.NumberFormat import net.primal.android.R -import net.primal.android.core.compose.AvatarThumbnail import net.primal.android.core.compose.PrimalDefaults import net.primal.android.core.compose.PrimalLoadingSpinner import net.primal.android.core.compose.UiDensityMode +import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalLoadingButton import net.primal.android.core.compose.detectUiDensityModeFromMaxHeight import net.primal.android.core.compose.foundation.keyboardVisibilityAsState @@ -388,10 +388,11 @@ private fun TransactionHeaderColumn( ) } } else { - AvatarThumbnail( + UniversalAvatarThumbnail( modifier = Modifier.padding(vertical = verticalPadding.value), avatarCdnImage = state.profileAvatarCdnImage, avatarSize = avatarSize.value, + legendaryCustomization = state.profileLegendaryCustomization, ) }