From add451005e6984b651ab9f8b7a1c89127192ce89 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Sat, 7 Dec 2024 11:48:42 +0100 Subject: [PATCH 1/7] Save highlights to local database --- .../48.json | 1851 +++++++++++++++++ .../net/primal/android/articles/db/Article.kt | 4 + .../net/primal/android/db/PrimalDatabase.kt | 7 +- .../android/highlights/db/HighlightDao.kt | 10 + .../android/highlights/db/HighlightData.kt | 17 + .../android/highlights/model/HighlightUi.kt | 26 + .../android/nostr/ext/HighlightEvents.kt | 23 + .../net/primal/android/nostr/ext/Tags.kt | 6 + .../notes/repository/FeedResponseProcessor.kt | 3 + .../articles/details/ui/ArticleDetailsUi.kt | 4 + 10 files changed, 1950 insertions(+), 1 deletion(-) create mode 100644 app/schemas/net.primal.android.db.PrimalDatabase/48.json create mode 100644 app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt create mode 100644 app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt create mode 100644 app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt create mode 100644 app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt diff --git a/app/schemas/net.primal.android.db.PrimalDatabase/48.json b/app/schemas/net.primal.android.db.PrimalDatabase/48.json new file mode 100644 index 000000000..8f9b73fc6 --- /dev/null +++ b/app/schemas/net.primal.android.db.PrimalDatabase/48.json @@ -0,0 +1,1851 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "06f5bd9c678486ea2bbe40b4979a6961", + "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": [] + }, + { + "tableName": "HighlightData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`highlightId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `content` TEXT NOT NULL, `context` TEXT, `alt` TEXT, `highlightEventId` TEXT, `highlightEventAuthorId` TEXT, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`highlightId`))", + "fields": [ + { + "fieldPath": "highlightId", + "columnName": "highlightId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "context", + "columnName": "context", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "alt", + "columnName": "alt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "highlightEventId", + "columnName": "highlightEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "highlightEventAuthorId", + "columnName": "highlightEventAuthorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "highlightId" + ] + }, + "indices": [], + "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, '06f5bd9c678486ea2bbe40b4979a6961')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/net/primal/android/articles/db/Article.kt b/app/src/main/kotlin/net/primal/android/articles/db/Article.kt index 23f52bea5..84530b9ca 100644 --- a/app/src/main/kotlin/net/primal/android/articles/db/Article.kt +++ b/app/src/main/kotlin/net/primal/android/articles/db/Article.kt @@ -3,6 +3,7 @@ package net.primal.android.articles.db import androidx.room.Embedded import androidx.room.Relation import net.primal.android.bookmarks.db.PublicBookmark +import net.primal.android.highlights.db.HighlightData import net.primal.android.profile.db.ProfileData import net.primal.android.stats.db.EventStats import net.primal.android.stats.db.EventUserStats @@ -26,4 +27,7 @@ data class Article( @Relation(entityColumn = "tagValue", parentColumn = "aTag") val bookmark: PublicBookmark? = null, + + @Relation(entityColumn = "highlightEventId", parentColumn = "aTag") + val highlights: List = emptyList(), ) 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 01b98a26e..d5a163579 100644 --- a/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt +++ b/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt @@ -21,6 +21,8 @@ import net.primal.android.explore.db.TrendingTopic import net.primal.android.explore.db.TrendingTopicDao import net.primal.android.feeds.db.Feed import net.primal.android.feeds.db.FeedDao +import net.primal.android.highlights.db.HighlightDao +import net.primal.android.highlights.db.HighlightData import net.primal.android.messages.db.DirectMessageDao import net.primal.android.messages.db.DirectMessageData import net.primal.android.messages.db.MessageConversationDao @@ -89,8 +91,9 @@ import net.primal.android.wallet.db.WalletTransactionData ArticleData::class, ArticleCommentCrossRef::class, ArticleFeedCrossRef::class, + HighlightData::class, ], - version = 47, + version = 48, exportSchema = true, ) @TypeConverters( @@ -150,4 +153,6 @@ abstract class PrimalDatabase : RoomDatabase() { abstract fun articles(): ArticleDao abstract fun articleFeedsConnections(): ArticleFeedCrossRefDao + + abstract fun highlights(): HighlightDao } diff --git a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt new file mode 100644 index 000000000..db9a4ef54 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt @@ -0,0 +1,10 @@ +package net.primal.android.highlights.db + +import androidx.room.Dao +import androidx.room.Upsert + +@Dao +interface HighlightDao { + @Upsert + fun upsertAll(data: List) +} diff --git a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt new file mode 100644 index 000000000..2ad134d52 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt @@ -0,0 +1,17 @@ +package net.primal.android.highlights.db + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class HighlightData( + @PrimaryKey + val highlightId: String, + val authorId: String, + val content: String, + val context: String?, + val alt: String?, + val highlightEventId: String?, + val highlightEventAuthorId: String?, + val createdAt: Long, +) diff --git a/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt b/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt new file mode 100644 index 000000000..655f0d03d --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt @@ -0,0 +1,26 @@ +package net.primal.android.highlights.model + +import net.primal.android.highlights.db.HighlightData + +data class HighlightUi( + val highlightId: String, + val authorId: String, + val content: String, + val context: String?, + val alt: String?, + val highlightEventId: String?, + val highlightEventAuthorId: String?, + val createdAt: Long, +) + +fun HighlightData.asHighlightUi() = + HighlightUi( + highlightId = highlightId, + authorId = authorId, + content = content, + context = context, + alt = alt, + highlightEventId = highlightEventId, + highlightEventAuthorId = highlightEventAuthorId, + createdAt = createdAt, + ) diff --git a/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt b/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt new file mode 100644 index 000000000..c90ef48cc --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt @@ -0,0 +1,23 @@ +package net.primal.android.nostr.ext + +import net.primal.android.highlights.db.HighlightData +import net.primal.android.nostr.model.NostrEvent +import net.primal.android.nostr.model.NostrEventKind +import net.primal.android.nostr.model.primal.PrimalEvent + +fun List.mapReferencedEventsAsHighlightDataPO() = + this.mapNotNull { it.takeContentOrNull() } + .filter { it.kind == NostrEventKind.Highlight.value } + .map { it.asHighlightData() } + +fun NostrEvent.asHighlightData() = + HighlightData( + highlightId = this.id, + authorId = this.pubKey, + content = this.content, + alt = this.tags.findFirstAltDescription(), + context = this.tags.findFirstContextTag(), + highlightEventId = this.tags.findFirstReplaceableEventId(), + highlightEventAuthorId = this.tags.findFirstProfileId()?.extractProfileId(), + createdAt = this.createdAt, + ) diff --git a/app/src/main/kotlin/net/primal/android/nostr/ext/Tags.kt b/app/src/main/kotlin/net/primal/android/nostr/ext/Tags.kt index c7da7b3c6..5f307c4e3 100644 --- a/app/src/main/kotlin/net/primal/android/nostr/ext/Tags.kt +++ b/app/src/main/kotlin/net/primal/android/nostr/ext/Tags.kt @@ -30,6 +30,12 @@ fun List.findFirstIdentifier() = firstOrNull { it.isIdentifierTag() } fun List.findFirstAltDescription() = firstOrNull { it.isAltTag() }?.getTagValueOrNull() +fun List.findFirstReplaceableEventId() = firstOrNull { it.isATag() }?.getTagValueOrNull() + +fun List.findFirstContextTag() = firstOrNull { it.isContextTag() }?.getTagValueOrNull() + +fun JsonArray.isContextTag() = getOrNull(0)?.jsonPrimitive?.content == "context" + fun JsonArray.isBolt11Tag() = getOrNull(0)?.jsonPrimitive?.content == "bolt11" fun JsonArray.isDescriptionTag() = getOrNull(0)?.jsonPrimitive?.content == "description" diff --git a/app/src/main/kotlin/net/primal/android/notes/repository/FeedResponseProcessor.kt b/app/src/main/kotlin/net/primal/android/notes/repository/FeedResponseProcessor.kt index 694620eea..d30f8ed45 100644 --- a/app/src/main/kotlin/net/primal/android/notes/repository/FeedResponseProcessor.kt +++ b/app/src/main/kotlin/net/primal/android/notes/repository/FeedResponseProcessor.kt @@ -21,6 +21,7 @@ import net.primal.android.nostr.ext.mapNotNullAsEventUserStatsPO import net.primal.android.nostr.ext.mapNotNullAsPostDataPO import net.primal.android.nostr.ext.mapNotNullAsRepostDataPO import net.primal.android.nostr.ext.mapReferencedEventsAsArticleDataPO +import net.primal.android.nostr.ext.mapReferencedEventsAsHighlightDataPO import net.primal.android.nostr.ext.parseAndMapPrimalLegendProfiles import net.primal.android.nostr.ext.parseAndMapPrimalUserNames import net.primal.android.nostr.model.NostrEvent @@ -42,6 +43,7 @@ suspend fun FeedResponse.persistToDatabaseAsTransaction(userId: String, database val articles = this.articles.mapNotNullAsArticleDataPO(cdnResources = cdnResources) val referencedArticles = this.referencedEvents.mapReferencedEventsAsArticleDataPO(cdnResources = cdnResources) + val referencedHighlights = this.referencedEvents.mapReferencedEventsAsHighlightDataPO() val allArticles = articles + referencedArticles val primalUserNames = this.primalUserNames.parseAndMapPrimalUserNames() @@ -92,6 +94,7 @@ suspend fun FeedResponse.persistToDatabaseAsTransaction(userId: String, database database.eventStats().upsertAll(data = postStats) database.eventUserStats().upsertAll(data = userPostStats) database.articles().upsertAll(list = allArticles) + database.highlights().upsertAll(data = referencedHighlights) val eventHintsDao = database.eventHints() val hintsMap = eventHints.associateBy { it.eventId } 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 7130dd08e..ee651a37c 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 @@ -5,6 +5,8 @@ import net.primal.android.articles.db.Article 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.highlights.model.HighlightUi +import net.primal.android.highlights.model.asHighlightUi import net.primal.android.notes.feed.model.EventStatsUi import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.premium.legend.asLegendaryCustomization @@ -28,6 +30,7 @@ data class ArticleDetailsUi( val isBookmarked: Boolean = false, val eventStatsUi: EventStatsUi = EventStatsUi(), val authorLegendaryCustomization: LegendaryCustomization? = null, + val highlights: List = emptyList(), ) fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi { @@ -50,5 +53,6 @@ fun Article.mapAsArticleDetailsUi(): ArticleDetailsUi { isBookmarked = this.bookmark != null, eventStatsUi = EventStatsUi.from(eventStats = this.eventStats, userStats = this.userEventStats), authorLegendaryCustomization = this.author?.primalLegendProfile?.asLegendaryCustomization(), + highlights = this.highlights.map { it.asHighlightUi() }, ) } From 76a619dc19902af17abe4213eec680f7b589da25 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 16:20:37 +0100 Subject: [PATCH 2/7] Migrate to compose-richtext:1.0.0-alpha02 with a custom block composer of AstParagraph nodes --- .../articles/details/ArticleDetailsScreen.kt | 1 + .../ui/rendering/CustomBlockNodeComposer.kt | 26 +++ .../details/ui/rendering/MarkdownRenderer.kt | 21 +- .../details/ui/rendering/MarkdownStyles.kt | 7 +- .../PrimalMarkdownUriHandlerProvider.kt | 18 ++ .../details/ui/richtext/MarkdownRichText.kt | 182 ++++++++++++++++++ .../details/ui/richtext/RemoteImage.kt | 85 ++++++++ .../details/ui/richtext/TraverseUtils.kt | 53 +++++ gradle/libs.versions.toml | 2 +- 9 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt create mode 100644 app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt create mode 100644 app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt create mode 100644 app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt create mode 100644 app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt 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 a276669c7..377fbf8cf 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 @@ -524,6 +524,7 @@ private fun ArticleContentWithComments( .fillMaxWidth() .padding(all = 16.dp), markdown = part.markdown, + highlights = state.article?.highlights ?: emptyList(), onProfileClick = noteCallbacks.onProfileClick, onNoteClick = noteCallbacks.onNoteClick, onArticleClick = noteCallbacks.onArticleClick, diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt new file mode 100644 index 000000000..94ab08433 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt @@ -0,0 +1,26 @@ +package net.primal.android.thread.articles.details.ui.rendering + +import androidx.compose.runtime.Composable +import com.halilibo.richtext.markdown.AstBlockNodeComposer +import com.halilibo.richtext.markdown.node.AstBlockNodeType +import com.halilibo.richtext.markdown.node.AstNode +import com.halilibo.richtext.markdown.node.AstParagraph +import com.halilibo.richtext.ui.RichTextScope +import net.primal.android.highlights.model.HighlightUi +import net.primal.android.thread.articles.details.ui.richtext.MarkdownRichText + + +fun customBlockNodeComposer(@Suppress("UnusedParameter") highlights: List) = + object : AstBlockNodeComposer { + override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean = + when (astBlockNodeType) { + AstParagraph -> true + else -> false + } + + @Composable + override fun RichTextScope.Compose(astNode: AstNode, visitChildren: @Composable (AstNode) -> Unit) { + require(astNode.type == AstParagraph) + MarkdownRichText(astNode = astNode) + } + } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt index d6f89d502..c7566f051 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt @@ -4,10 +4,11 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser -import com.halilibo.richtext.commonmark.MarkdownParseOptions import com.halilibo.richtext.markdown.BasicMarkdown import com.halilibo.richtext.ui.material3.RichText +import net.primal.android.highlights.model.HighlightUi import net.primal.android.theme.AppTheme import net.primal.android.thread.articles.details.ui.handleArticleLinkClick @@ -15,6 +16,7 @@ import net.primal.android.thread.articles.details.ui.handleArticleLinkClick fun MarkdownRenderer( markdown: String, modifier: Modifier = Modifier, + highlights: List = emptyList(), onProfileClick: ((profileId: String) -> Unit)? = null, onNoteClick: ((noteId: String) -> Unit)? = null, onArticleClick: ((naddr: String) -> Unit)? = null, @@ -26,14 +28,12 @@ fun MarkdownRenderer( codeBlockContent = AppTheme.colorScheme.onSurface, outlineColor = AppTheme.colorScheme.outline, ) - val parser = remember(markdown) { CommonmarkAstNodeParser(MarkdownParseOptions.Default) } + val parser = remember(markdown) { CommonmarkAstNodeParser(CommonMarkdownParseOptions.Default) } val astNode = remember(parser) { parser.parse(markdown) } SelectionContainer { PrimalMarkdownStylesProvider { - RichText( - modifier = modifier, - style = richTextStyle, + PrimalMarkdownUriHandlerProvider( linkClickHandler = { url -> url.handleArticleLinkClick( onProfileClick = onProfileClick, @@ -43,8 +43,17 @@ fun MarkdownRenderer( ) }, ) { - BasicMarkdown(astNode = astNode) + RichText( + modifier = modifier, + style = richTextStyle, + ) { + BasicMarkdown( + astNode = astNode, + astBlockNodeComposer = customBlockNodeComposer(highlights = highlights), + ) + } } + } } } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt index ced4a21d3..a0edac643 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -106,8 +107,10 @@ fun buildPrimalRichTextStyle( color = codeBlockContent, background = codeBlockBackground, ), - linkStyle = primalMarkdownBodyTextStyle.toSpanStyle().copy( - color = highlightColor, + linkStyle = TextLinkStyles( + style = primalMarkdownBodyTextStyle.toSpanStyle().copy( + color = highlightColor, + ) ), ), paragraphSpacing = 20.sp, diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt new file mode 100644 index 000000000..c5b138b3f --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt @@ -0,0 +1,18 @@ +package net.primal.android.thread.articles.details.ui.rendering + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler + + +@Composable +fun PrimalMarkdownUriHandlerProvider(linkClickHandler: (uri: String) -> Unit, content: @Composable () -> Unit) { + val uriHandler = remember { + object : UriHandler { + override fun openUri(uri: String) = linkClickHandler(uri) + } + } + CompositionLocalProvider(LocalUriHandler provides uriHandler, content) +} diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt new file mode 100644 index 000000000..fb2b97b32 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt @@ -0,0 +1,182 @@ +package net.primal.android.thread.articles.details.ui.richtext + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.halilibo.richtext.markdown.node.AstBlockQuote +import com.halilibo.richtext.markdown.node.AstCode +import com.halilibo.richtext.markdown.node.AstEmphasis +import com.halilibo.richtext.markdown.node.AstFencedCodeBlock +import com.halilibo.richtext.markdown.node.AstHardLineBreak +import com.halilibo.richtext.markdown.node.AstHeading +import com.halilibo.richtext.markdown.node.AstImage +import com.halilibo.richtext.markdown.node.AstIndentedCodeBlock +import com.halilibo.richtext.markdown.node.AstLink +import com.halilibo.richtext.markdown.node.AstLinkReferenceDefinition +import com.halilibo.richtext.markdown.node.AstListItem +import com.halilibo.richtext.markdown.node.AstNode +import com.halilibo.richtext.markdown.node.AstParagraph +import com.halilibo.richtext.markdown.node.AstSoftLineBreak +import com.halilibo.richtext.markdown.node.AstStrikethrough +import com.halilibo.richtext.markdown.node.AstStrongEmphasis +import com.halilibo.richtext.markdown.node.AstText +import com.halilibo.richtext.ui.BlockQuote +import com.halilibo.richtext.ui.FormattedList +import com.halilibo.richtext.ui.RichTextScope +import com.halilibo.richtext.ui.string.InlineContent +import com.halilibo.richtext.ui.string.RichTextString +import com.halilibo.richtext.ui.string.Text +import com.halilibo.richtext.ui.string.withFormat +import timber.log.Timber + + +/** + * + * NOTE: this component is copied from: https://github.com/halilozercan/compose-richtext + * and later modified for our specific use case + * + * Only render the text content that exists below [astNode]. All the content blocks + * like [AstBlockQuote] or [AstFencedCodeBlock] are ignored. This composable is + * suited for [AstHeading] and [AstParagraph] since they are strictly text blocks. + * + * Some notes about commonmark and in general Markdown parsing. + * + * - Paragraph and Heading are the only RichTextString containers in base implementation. + * - RichTextString is build by traversing the children of Heading or Paragraph. + * - RichTextString can include; + * - Emphasis + * - StrongEmphasis + * - Image + * - Link + * - Code + * - Code blocks should not have any children. Their whole content must reside in + * [AstIndentedCodeBlock.literal] or [AstFencedCodeBlock.literal]. + * - Blocks like [BlockQuote], [FormattedList], [AstListItem] must have an [AstParagraph] + * as a child to include any further RichText. + * - CustomNode and CustomBlock can have their own scope, no idea about that. + * + * @param astNode Root node to accept as Text Content container. + */ +@Composable +internal fun RichTextScope.MarkdownRichText(astNode: AstNode, modifier: Modifier = Modifier) { + // Assume that only RichText nodes reside below this level. + val richText = remember(astNode) { + computeRichTextString(astNode) + } + + Text(text = richText, modifier = modifier) +} + +private fun computeRichTextString(astNode: AstNode): RichTextString { + val richTextStringBuilder = RichTextString.Builder() + + // Modified pre-order traversal with pushFormat, popFormat support. + var iteratorStack = listOf( + AstNodeTraversalEntry( + astNode = astNode, + isVisited = false, + formatIndex = null + ) + ) + + while (iteratorStack.isNotEmpty()) { + val (currentNode, isVisited, formatIndex) = iteratorStack.first().copy() + iteratorStack = iteratorStack.drop(1) + + if (!isVisited) { + val newFormatIndex = when (val currentNodeType = currentNode.type) { + is AstCode -> { + richTextStringBuilder.withFormat(RichTextString.Format.Code) { + append(currentNodeType.literal) + } + null + } + is AstEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Italic) + is AstStrikethrough -> richTextStringBuilder.pushFormat( + RichTextString.Format.Strikethrough + ) + is AstImage -> { + richTextStringBuilder.appendInlineContent( + content = InlineContent( + initialSize = { + IntSize(128.dp.roundToPx(), 128.dp.roundToPx()) + } + ) { + RemoteImage( + url = currentNodeType.destination, + contentDescription = currentNodeType.title, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.Inside + ) + } + ) + null + } + is AstLink -> { + Timber.tag("astLink").i(currentNodeType.toString()) + richTextStringBuilder.pushFormat( + RichTextString.Format.Link( + destination = currentNodeType.destination + )) + } + is AstSoftLineBreak -> { + richTextStringBuilder.append(" ") + null + } + is AstHardLineBreak -> { + richTextStringBuilder.append("\n") + null + } + is AstStrongEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Bold) + is AstText -> { + richTextStringBuilder.append(currentNodeType.literal) + null + } + is AstLinkReferenceDefinition -> richTextStringBuilder.pushFormat( + RichTextString.Format.Link(destination = currentNodeType.destination)) + else -> null + } + + iteratorStack = iteratorStack.addFirst( + AstNodeTraversalEntry( + astNode = currentNode, + isVisited = true, + formatIndex = newFormatIndex + ) + ) + + // Do not visit children of terminals such as Text, Image, etc. + if (!currentNode.isRichTextTerminal()) { + currentNode.childrenSequence(reverse = true).forEach { + iteratorStack = iteratorStack.addFirst( + AstNodeTraversalEntry( + astNode = it, + isVisited = false, + formatIndex = null + ) + ) + } + } + } + + if (formatIndex != null) { + richTextStringBuilder.pop(formatIndex) + } + } + + return richTextStringBuilder.toRichTextString() +} + +private data class AstNodeTraversalEntry( + val astNode: AstNode, + val isVisited: Boolean, + val formatIndex: Int? +) + +private inline fun List.addFirst(item: T): List { + return listOf(item) + this +} diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt new file mode 100644 index 000000000..bd34c0bb3 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt @@ -0,0 +1,85 @@ +package net.primal.android.thread.articles.details.ui.richtext + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.isSpecified +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import coil.size.Size + +/** + * NOTE: this file was copied from: https://github.com/halilozercan/compose-richtext + */ + +private val DEFAULT_IMAGE_SIZE = 64.dp + +/** + * Implementation of RemoteImage by using Coil library for Android. + */ +@Composable +fun RemoteImage( + url: String, + contentDescription: String?, + modifier: Modifier, + contentScale: ContentScale +) { + val painter = rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current) + .data(data = url) + .size(Size.ORIGINAL) + .crossfade(true) + .build() + ) + + val density = LocalDensity.current + + BoxWithConstraints(modifier, contentAlignment = Alignment.Center) { + val sizeModifier by remember(density, painter) { + derivedStateOf { + val painterIntrinsicSize = painter.state.painter?.intrinsicSize + if (painterIntrinsicSize != null && + painterIntrinsicSize.isSpecified && + painterIntrinsicSize.width != Float.POSITIVE_INFINITY && + painterIntrinsicSize.height != Float.POSITIVE_INFINITY + ) { + val width = painterIntrinsicSize.width + val height = painterIntrinsicSize.height + val scale = if (width > constraints.maxWidth) { + constraints.maxWidth.toFloat() / width + } else { + 1f + } + + with(density) { + Modifier.size( + (width * scale).toDp(), + (height * scale).toDp() + ) + } + } else { + // if size is not defined at all, Coil fails to render the image + // here, we give a default size for images until they are loaded. + Modifier.size(DEFAULT_IMAGE_SIZE) + } + } + } + + Image( + painter = painter, + contentDescription = contentDescription, + modifier = sizeModifier, + contentScale = contentScale + ) + } +} diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt new file mode 100644 index 000000000..ccda332b8 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt @@ -0,0 +1,53 @@ +package net.primal.android.thread.articles.details.ui.richtext + +import com.halilibo.richtext.markdown.node.AstCode +import com.halilibo.richtext.markdown.node.AstHardLineBreak +import com.halilibo.richtext.markdown.node.AstImage +import com.halilibo.richtext.markdown.node.AstNode +import com.halilibo.richtext.markdown.node.AstNodeType +import com.halilibo.richtext.markdown.node.AstSoftLineBreak +import com.halilibo.richtext.markdown.node.AstText + +/** + * NOTE: this file was copied from: https://github.com/halilozercan/compose-richtext + */ + +internal fun AstNode.childrenSequence( + reverse: Boolean = false +): Sequence { + return if (!reverse) { + generateSequence(this.links.firstChild) { it.links.next } + } else { + generateSequence(this.links.lastChild) { it.links.previous } + } +} + +/** + * Markdown rendering is susceptible to have assumptions. Hence, some rendering rules + * may force restrictions on children. So, valid children nodes should be selected + * before traversing. This function returns a LinkedList of children which conforms to + * [filter] function. + * + * @param filter A lambda to select valid children. + */ +fun AstNode.filterChildren( + reverse: Boolean = false, + filter: (AstNode) -> Boolean +): Sequence { + return childrenSequence(reverse).filter(filter) +} + +internal inline fun AstNode.filterChildrenType(): Sequence { + return filterChildren { it.type is T } +} + +/** + * These ASTNode types should never have any children. If any exists, ignore them. + */ +internal fun AstNode.isRichTextTerminal(): Boolean { + return type is AstText + || type is AstCode + || type is AstImage + || type is AstSoftLineBreak + || type is AstHardLineBreak +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8baf10fbf..906a8d2b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ camera = "1.4.0" compose-bom = "2024.11.00" compose-lottie = "6.4.0" compose-navigation = "2.8.4" -compose-richtext = "1.0.0-alpha01" +compose-richtext = "1.0.0-alpha02" constraintlayout = "2.2.0" constraintlayout-compose = "1.1.0" coil = "2.7.0" From 077e73c3ca5c94a46e5a6c9075bcbc6bf0568385 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 16:22:32 +0100 Subject: [PATCH 3/7] Update detekt baseline --- app/detekt-baseline.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index c29699e2b..f9c126aeb 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -3,7 +3,7 @@ ComplexCondition:NostrResources.kt$isNote() || isNoteUri() || isNEventUri() || isNEvent() - ComplexCondition:PrimalDrawer.kt$(item is DrawerScreenDestination.Messages && badges.unreadMessagesCount > 0) || (item is DrawerScreenDestination.Premium && showPremiumBadge) + ComplexCondition:RemoteImage.kt$painterIntrinsicSize != null && painterIntrinsicSize.isSpecified && painterIntrinsicSize.width != Float.POSITIVE_INFINITY && painterIntrinsicSize.height != Float.POSITIVE_INFINITY CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List<ArticlePartRender>, listState: LazyListState = rememberLazyListState(), paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -> Unit, onArticleHashtagClick: (hashtag: String) -> Unit, onZapOptionsClick: () -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongPressAction: ((FeedPostAction) -> Unit)? = null, onFollowUnfollowClick: (() -> Unit)? = null, onUiError: ((UiError) -> Unit)? = null, ) CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArticleDetailsScreen( detailsState: ArticleDetailsContract.UiState, articleState: ArticleContract.UiState, detailsEventPublisher: (UiEvent) -> Unit, articleEventPublisher: (ArticleContract.UiEvent) -> Unit, onArticleHashtagClick: (hashtag: String) -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onClose: () -> Unit, ) CyclomaticComplexMethod:ChatScreen.kt$@Composable private fun ChatMessageListItem( chatMessage: ChatMessageUi, previousMessage: ChatMessageUi? = null, nextMessage: ChatMessageUi? = null, onUrlClick: (String) -> Unit, noteCallbacks: NoteCallbacks, ) @@ -13,6 +13,7 @@ CyclomaticComplexMethod:FeedListViewModel.kt$FeedListViewModel$private fun observeEvents() CyclomaticComplexMethod:FeedNoteCard.kt$@Composable private fun FeedNote( data: FeedPostUi, fullWidthContent: Boolean, avatarSizeDp: Dp, avatarPaddingValues: PaddingValues, notePaddingValues: PaddingValues, enableTweetsMode: Boolean, headerSingleLine: Boolean, showReplyTo: Boolean, forceContentIndent: Boolean, expanded: Boolean, textSelectable: Boolean, showNoteStatCounts: Boolean, noteCallbacks: NoteCallbacks, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongClickAction: ((FeedPostAction) -> Unit)? = null, contentFooter: @Composable () -> Unit = {}, ) CyclomaticComplexMethod:FeedNoteCard.kt$@ExperimentalMaterial3Api @Composable private fun FeedNoteCard( data: FeedPostUi, state: NoteContract.UiState, eventPublisher: (UiEvent) -> Unit, modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape, colors: CardColors = noteCardColors(), cardPadding: PaddingValues = PaddingValues(all = 0.dp), enableTweetsMode: Boolean = false, headerSingleLine: Boolean = true, fullWidthContent: Boolean = false, forceContentIndent: Boolean = false, drawLineAboveAvatar: Boolean = false, drawLineBelowAvatar: Boolean = false, expanded: Boolean = false, textSelectable: Boolean = false, showReplyTo: Boolean = true, noteOptionsMenuEnabled: Boolean = true, showNoteStatCounts: Boolean = true, noteCallbacks: NoteCallbacks = NoteCallbacks(), onGoToWallet: (() -> Unit)? = null, contentFooter: @Composable () -> Unit = {}, ) + CyclomaticComplexMethod:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString CyclomaticComplexMethod:MessagesRemoteMediator.kt$MessagesRemoteMediator$override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessage>): MediatorResult CyclomaticComplexMethod:NostrUserText.kt$@Composable fun NostrUserText( displayName: String, internetIdentifier: String?, modifier: Modifier = Modifier, displayNameColor: Color = AppTheme.colorScheme.onSurface, fontSize: TextUnit = TextUnit.Unspecified, style: TextStyle = LocalTextStyle.current, overflow: TextOverflow = TextOverflow.Ellipsis, maxLines: Int = 1, internetIdentifierBadgeSize: Dp = 14.dp, internetIdentifierBadgeAlign: PlaceholderVerticalAlign = PlaceholderVerticalAlign.Center, customBadgeStyle: LegendaryStyle? = null, annotatedStringPrefixBuilder: (AnnotatedString.Builder.() -> Unit)? = null, annotatedStringSuffixBuilder: (AnnotatedString.Builder.() -> Unit)? = null, ) CyclomaticComplexMethod:NoteActionsRow.kt$@Composable fun FeedNoteActionsRow( modifier: Modifier, eventStats: EventStatsUi, isBookmarked: Boolean, highlightedNote: Boolean = false, showBookmark: Boolean = false, showCounts: Boolean = true, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongPressAction: ((FeedPostAction) -> Unit)? = null, ) @@ -53,6 +54,7 @@ LongMethod:HomeFeedScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeFeedScreen( state: HomeFeedContract.UiState, onTopLevelDestinationChanged: (PrimalTopLevelDestination) -> Unit, onDrawerScreenClick: (DrawerScreenDestination) -> Unit, onDrawerQrCodeClick: () -> Unit, onSearchClick: () -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onNewPostClick: (content: TextFieldValue?) -> Unit, eventPublisher: (UiEvent) -> Unit, ) LongMethod:KeysSettingsScreen.kt$@Composable fun PrivateKeySection(nsec: String) LongMethod:LegendaryProfileCustomizationScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun LegendaryProfileCustomizationScreen( state: LegendaryProfileCustomizationContract.UiState, eventPublisher: (LegendaryProfileCustomizationContract.UiEvent) -> Unit, onClose: () -> Unit, ) + LongMethod:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString LongMethod:MessageConversationListScreen.kt$@Composable private fun ConversationListItem( conversation: MessageConversationUi, onConversationClick: (String) -> Unit, onProfileClick: (profileId: String) -> Unit, ) LongMethod:MessageConversationListScreen.kt$@Composable private fun MessagesTabs( relation: ConversationRelation, onFollowsTabClick: () -> Unit, onOtherTabClick: () -> Unit, onMarkAllRead: () -> Unit, ) LongMethod:MultipleUserPicker.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MultipleUserPicker( modifier: Modifier = Modifier, sheetTitle: String, placeholderText: String, onDismissRequest: () -> Unit, onUsersSelected: (Set<UserProfileItemUi>) -> Unit, sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), startingSelectedUsers: Set<UserProfileItemUi>, ) @@ -78,7 +80,6 @@ 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, 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, ) @@ -112,6 +113,7 @@ MagicNumber:PrimalDrawer.kt$0.5f MagicNumber:PrimalDrawerScaffold.kt$0.5f MatchingDeclarationName:__PrimalIcons.kt$PrimalIcons + NestedBlockDepth:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString ReturnCount:LnInvoiceUtils.kt$LnInvoiceUtils$private fun getAmount(invoice: String): BigDecimal ReturnCount:MessagesRemoteMediator.kt$MessagesRemoteMediator$override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessage>): MediatorResult ReturnCount:NostrResources.kt$private fun String.nostrUriToBytes(): ByteArray? From cf3cf09ce5d5c2ab51297bc5843e67f7a1a9b751 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 16:22:49 +0100 Subject: [PATCH 4/7] Auto-format code --- .../ui/rendering/CustomBlockNodeComposer.kt | 1 - .../details/ui/rendering/MarkdownRenderer.kt | 1 - .../details/ui/rendering/MarkdownStyles.kt | 2 +- .../PrimalMarkdownUriHandlerProvider.kt | 1 - .../details/ui/richtext/MarkdownRichText.kt | 31 ++++++++++--------- .../details/ui/richtext/RemoteImage.kt | 8 ++--- .../details/ui/richtext/TraverseUtils.kt | 19 +++++------- 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt index 94ab08433..cc3d62bdb 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/CustomBlockNodeComposer.kt @@ -9,7 +9,6 @@ import com.halilibo.richtext.ui.RichTextScope import net.primal.android.highlights.model.HighlightUi import net.primal.android.thread.articles.details.ui.richtext.MarkdownRichText - fun customBlockNodeComposer(@Suppress("UnusedParameter") highlights: List) = object : AstBlockNodeComposer { override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean = diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt index c7566f051..e40fabbc5 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownRenderer.kt @@ -53,7 +53,6 @@ fun MarkdownRenderer( ) } } - } } } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt index a0edac643..9f364d977 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/MarkdownStyles.kt @@ -110,7 +110,7 @@ fun buildPrimalRichTextStyle( linkStyle = TextLinkStyles( style = primalMarkdownBodyTextStyle.toSpanStyle().copy( color = highlightColor, - ) + ), ), ), paragraphSpacing = 20.sp, diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt index c5b138b3f..710748d19 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/rendering/PrimalMarkdownUriHandlerProvider.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler - @Composable fun PrimalMarkdownUriHandlerProvider(linkClickHandler: (uri: String) -> Unit, content: @Composable () -> Unit) { val uriHandler = remember { diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt index fb2b97b32..3a475e499 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt @@ -33,7 +33,6 @@ import com.halilibo.richtext.ui.string.Text import com.halilibo.richtext.ui.string.withFormat import timber.log.Timber - /** * * NOTE: this component is copied from: https://github.com/halilozercan/compose-richtext @@ -79,8 +78,8 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { AstNodeTraversalEntry( astNode = astNode, isVisited = false, - formatIndex = null - ) + formatIndex = null, + ), ) while (iteratorStack.isNotEmpty()) { @@ -97,22 +96,22 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { } is AstEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Italic) is AstStrikethrough -> richTextStringBuilder.pushFormat( - RichTextString.Format.Strikethrough + RichTextString.Format.Strikethrough, ) is AstImage -> { richTextStringBuilder.appendInlineContent( content = InlineContent( initialSize = { IntSize(128.dp.roundToPx(), 128.dp.roundToPx()) - } + }, ) { RemoteImage( url = currentNodeType.destination, contentDescription = currentNodeType.title, modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.Inside + contentScale = ContentScale.Inside, ) - } + }, ) null } @@ -120,8 +119,9 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { Timber.tag("astLink").i(currentNodeType.toString()) richTextStringBuilder.pushFormat( RichTextString.Format.Link( - destination = currentNodeType.destination - )) + destination = currentNodeType.destination, + ), + ) } is AstSoftLineBreak -> { richTextStringBuilder.append(" ") @@ -137,7 +137,8 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { null } is AstLinkReferenceDefinition -> richTextStringBuilder.pushFormat( - RichTextString.Format.Link(destination = currentNodeType.destination)) + RichTextString.Format.Link(destination = currentNodeType.destination), + ) else -> null } @@ -145,8 +146,8 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { AstNodeTraversalEntry( astNode = currentNode, isVisited = true, - formatIndex = newFormatIndex - ) + formatIndex = newFormatIndex, + ), ) // Do not visit children of terminals such as Text, Image, etc. @@ -156,8 +157,8 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { AstNodeTraversalEntry( astNode = it, isVisited = false, - formatIndex = null - ) + formatIndex = null, + ), ) } } @@ -174,7 +175,7 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { private data class AstNodeTraversalEntry( val astNode: AstNode, val isVisited: Boolean, - val formatIndex: Int? + val formatIndex: Int?, ) private inline fun List.addFirst(item: T): List { diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt index bd34c0bb3..777398d07 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt @@ -32,14 +32,14 @@ fun RemoteImage( url: String, contentDescription: String?, modifier: Modifier, - contentScale: ContentScale + contentScale: ContentScale, ) { val painter = rememberAsyncImagePainter( ImageRequest.Builder(LocalContext.current) .data(data = url) .size(Size.ORIGINAL) .crossfade(true) - .build() + .build(), ) val density = LocalDensity.current @@ -64,7 +64,7 @@ fun RemoteImage( with(density) { Modifier.size( (width * scale).toDp(), - (height * scale).toDp() + (height * scale).toDp(), ) } } else { @@ -79,7 +79,7 @@ fun RemoteImage( painter = painter, contentDescription = contentDescription, modifier = sizeModifier, - contentScale = contentScale + contentScale = contentScale, ) } } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt index ccda332b8..ade49e1ec 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/TraverseUtils.kt @@ -12,9 +12,7 @@ import com.halilibo.richtext.markdown.node.AstText * NOTE: this file was copied from: https://github.com/halilozercan/compose-richtext */ -internal fun AstNode.childrenSequence( - reverse: Boolean = false -): Sequence { +internal fun AstNode.childrenSequence(reverse: Boolean = false): Sequence { return if (!reverse) { generateSequence(this.links.firstChild) { it.links.next } } else { @@ -30,10 +28,7 @@ internal fun AstNode.childrenSequence( * * @param filter A lambda to select valid children. */ -fun AstNode.filterChildren( - reverse: Boolean = false, - filter: (AstNode) -> Boolean -): Sequence { +fun AstNode.filterChildren(reverse: Boolean = false, filter: (AstNode) -> Boolean): Sequence { return childrenSequence(reverse).filter(filter) } @@ -45,9 +40,9 @@ internal inline fun AstNode.filterChildrenType(): Sequ * These ASTNode types should never have any children. If any exists, ignore them. */ internal fun AstNode.isRichTextTerminal(): Boolean { - return type is AstText - || type is AstCode - || type is AstImage - || type is AstSoftLineBreak - || type is AstHardLineBreak + return type is AstText || + type is AstCode || + type is AstImage || + type is AstSoftLineBreak || + type is AstHardLineBreak } From a6aab152cf432c4359836dd0ed0c3fba9cd4d69d Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 16:43:07 +0100 Subject: [PATCH 5/7] Checkout db v48 schema json --- .../48.json | 62 ------------------- 1 file changed, 62 deletions(-) diff --git a/app/schemas/net.primal.android.db.PrimalDatabase/48.json b/app/schemas/net.primal.android.db.PrimalDatabase/48.json index 24965f661..ca057c268 100644 --- a/app/schemas/net.primal.android.db.PrimalDatabase/48.json +++ b/app/schemas/net.primal.android.db.PrimalDatabase/48.json @@ -1778,68 +1778,6 @@ } ], "foreignKeys": [] - }, - { - "tableName": "HighlightData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`highlightId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `content` TEXT NOT NULL, `context` TEXT, `alt` TEXT, `highlightEventId` TEXT, `highlightEventAuthorId` TEXT, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`highlightId`))", - "fields": [ - { - "fieldPath": "highlightId", - "columnName": "highlightId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authorId", - "columnName": "authorId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "context", - "columnName": "context", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "alt", - "columnName": "alt", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "highlightEventId", - "columnName": "highlightEventId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "highlightEventAuthorId", - "columnName": "highlightEventAuthorId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "highlightId" - ] - }, - "indices": [], - "foreignKeys": [] } ], "views": [], From 5b086ea8ff1cb7620f27994ca02677b76471d9fd Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 16:43:42 +0100 Subject: [PATCH 6/7] Rename some fields inside HighlightData --- .../net.primal.android.db.PrimalDatabase/50.json | 14 +++++++------- .../net/primal/android/articles/db/Article.kt | 2 +- .../primal/android/highlights/db/HighlightData.kt | 4 ++-- .../primal/android/highlights/model/HighlightUi.kt | 4 ++-- .../primal/android/nostr/ext/HighlightEvents.kt | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/schemas/net.primal.android.db.PrimalDatabase/50.json b/app/schemas/net.primal.android.db.PrimalDatabase/50.json index 3e6dbf4e6..ab710c225 100644 --- a/app/schemas/net.primal.android.db.PrimalDatabase/50.json +++ b/app/schemas/net.primal.android.db.PrimalDatabase/50.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 50, - "identityHash": "2019cbd364cd7e55d42d279786ac99e7", + "identityHash": "d3760b2268d561e1a4951fc2ce14e17b", "entities": [ { "tableName": "PostData", @@ -1793,7 +1793,7 @@ }, { "tableName": "HighlightData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`highlightId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `content` TEXT NOT NULL, `context` TEXT, `alt` TEXT, `highlightEventId` TEXT, `highlightEventAuthorId` TEXT, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`highlightId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`highlightId` TEXT NOT NULL, `authorId` TEXT NOT NULL, `content` TEXT NOT NULL, `context` TEXT, `alt` TEXT, `referencedEventId` TEXT, `referencedEventAuthorId` TEXT, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`highlightId`))", "fields": [ { "fieldPath": "highlightId", @@ -1826,14 +1826,14 @@ "notNull": false }, { - "fieldPath": "highlightEventId", - "columnName": "highlightEventId", + "fieldPath": "referencedEventId", + "columnName": "referencedEventId", "affinity": "TEXT", "notNull": false }, { - "fieldPath": "highlightEventAuthorId", - "columnName": "highlightEventAuthorId", + "fieldPath": "referencedEventAuthorId", + "columnName": "referencedEventAuthorId", "affinity": "TEXT", "notNull": false }, @@ -1857,7 +1857,7 @@ "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, '2019cbd364cd7e55d42d279786ac99e7')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd3760b2268d561e1a4951fc2ce14e17b')" ] } } \ No newline at end of file diff --git a/app/src/main/kotlin/net/primal/android/articles/db/Article.kt b/app/src/main/kotlin/net/primal/android/articles/db/Article.kt index 84530b9ca..ca8a30d52 100644 --- a/app/src/main/kotlin/net/primal/android/articles/db/Article.kt +++ b/app/src/main/kotlin/net/primal/android/articles/db/Article.kt @@ -28,6 +28,6 @@ data class Article( @Relation(entityColumn = "tagValue", parentColumn = "aTag") val bookmark: PublicBookmark? = null, - @Relation(entityColumn = "highlightEventId", parentColumn = "aTag") + @Relation(entityColumn = "referencedEventId", parentColumn = "aTag") val highlights: List = emptyList(), ) diff --git a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt index 2ad134d52..b41621675 100644 --- a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt +++ b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightData.kt @@ -11,7 +11,7 @@ data class HighlightData( val content: String, val context: String?, val alt: String?, - val highlightEventId: String?, - val highlightEventAuthorId: String?, + val referencedEventId: String?, + val referencedEventAuthorId: String?, val createdAt: Long, ) diff --git a/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt b/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt index 655f0d03d..c9cd1c608 100644 --- a/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt +++ b/app/src/main/kotlin/net/primal/android/highlights/model/HighlightUi.kt @@ -20,7 +20,7 @@ fun HighlightData.asHighlightUi() = content = content, context = context, alt = alt, - highlightEventId = highlightEventId, - highlightEventAuthorId = highlightEventAuthorId, + highlightEventId = referencedEventId, + highlightEventAuthorId = referencedEventAuthorId, createdAt = createdAt, ) diff --git a/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt b/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt index c90ef48cc..181943a97 100644 --- a/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt +++ b/app/src/main/kotlin/net/primal/android/nostr/ext/HighlightEvents.kt @@ -17,7 +17,7 @@ fun NostrEvent.asHighlightData() = content = this.content, alt = this.tags.findFirstAltDescription(), context = this.tags.findFirstContextTag(), - highlightEventId = this.tags.findFirstReplaceableEventId(), - highlightEventAuthorId = this.tags.findFirstProfileId()?.extractProfileId(), + referencedEventId = this.tags.findFirstReplaceableEventId(), + referencedEventAuthorId = this.tags.findFirstProfileId()?.extractProfileId(), createdAt = this.createdAt, ) From 6106cc9fb6f4034355aa9449782229fc5356ab64 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 10 Dec 2024 17:18:52 +0100 Subject: [PATCH 7/7] Suppress detekt warnings in files instead of baseline --- app/detekt-baseline.xml | 4 ---- .../articles/details/ui/richtext/MarkdownRichText.kt | 11 +++++++++-- .../articles/details/ui/richtext/RemoteImage.kt | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 10141ed3d..ae14bacfa 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -3,7 +3,6 @@ ComplexCondition:NostrResources.kt$isNote() || isNoteUri() || isNEventUri() || isNEvent() - ComplexCondition:RemoteImage.kt$painterIntrinsicSize != null && painterIntrinsicSize.isSpecified && painterIntrinsicSize.width != Float.POSITIVE_INFINITY && painterIntrinsicSize.height != Float.POSITIVE_INFINITY CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalLayoutApi::class) @Composable private fun ArticleContentWithComments( state: ArticleDetailsContract.UiState, articleParts: List<ArticlePartRender>, listState: LazyListState = rememberLazyListState(), paddingValues: PaddingValues, onArticleCommentClick: (naddr: String) -> Unit, onArticleHashtagClick: (hashtag: String) -> Unit, onZapOptionsClick: () -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongPressAction: ((FeedPostAction) -> Unit)? = null, onFollowUnfollowClick: (() -> Unit)? = null, onUiError: ((UiError) -> Unit)? = null, ) CyclomaticComplexMethod:ArticleDetailsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArticleDetailsScreen( detailsState: ArticleDetailsContract.UiState, articleState: ArticleContract.UiState, detailsEventPublisher: (UiEvent) -> Unit, articleEventPublisher: (ArticleContract.UiEvent) -> Unit, onArticleHashtagClick: (hashtag: String) -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onClose: () -> Unit, ) CyclomaticComplexMethod:ChatScreen.kt$@Composable private fun ChatMessageListItem( chatMessage: ChatMessageUi, previousMessage: ChatMessageUi? = null, nextMessage: ChatMessageUi? = null, onUrlClick: (String) -> Unit, noteCallbacks: NoteCallbacks, ) @@ -13,7 +12,6 @@ CyclomaticComplexMethod:FeedListViewModel.kt$FeedListViewModel$private fun observeEvents() CyclomaticComplexMethod:FeedNoteCard.kt$@Composable private fun FeedNote( data: FeedPostUi, fullWidthContent: Boolean, avatarSizeDp: Dp, avatarPaddingValues: PaddingValues, notePaddingValues: PaddingValues, enableTweetsMode: Boolean, headerSingleLine: Boolean, showReplyTo: Boolean, forceContentIndent: Boolean, expanded: Boolean, textSelectable: Boolean, showNoteStatCounts: Boolean, noteCallbacks: NoteCallbacks, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongClickAction: ((FeedPostAction) -> Unit)? = null, contentFooter: @Composable () -> Unit = {}, ) CyclomaticComplexMethod:FeedNoteCard.kt$@ExperimentalMaterial3Api @Composable private fun FeedNoteCard( data: FeedPostUi, state: NoteContract.UiState, eventPublisher: (UiEvent) -> Unit, modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape, colors: CardColors = noteCardColors(), cardPadding: PaddingValues = PaddingValues(all = 0.dp), enableTweetsMode: Boolean = false, headerSingleLine: Boolean = true, fullWidthContent: Boolean = false, forceContentIndent: Boolean = false, drawLineAboveAvatar: Boolean = false, drawLineBelowAvatar: Boolean = false, expanded: Boolean = false, textSelectable: Boolean = false, showReplyTo: Boolean = true, noteOptionsMenuEnabled: Boolean = true, showNoteStatCounts: Boolean = true, noteCallbacks: NoteCallbacks = NoteCallbacks(), onGoToWallet: (() -> Unit)? = null, contentFooter: @Composable () -> Unit = {}, ) - CyclomaticComplexMethod:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString CyclomaticComplexMethod:MessagesRemoteMediator.kt$MessagesRemoteMediator$override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessage>): MediatorResult CyclomaticComplexMethod:NostrUserText.kt$@Composable fun NostrUserText( displayName: String, internetIdentifier: String?, modifier: Modifier = Modifier, displayNameColor: Color = AppTheme.colorScheme.onSurface, fontSize: TextUnit = TextUnit.Unspecified, style: TextStyle = LocalTextStyle.current, overflow: TextOverflow = TextOverflow.Ellipsis, maxLines: Int = 1, internetIdentifierBadgeSize: Dp = 14.dp, internetIdentifierBadgeAlign: PlaceholderVerticalAlign = PlaceholderVerticalAlign.Center, customBadgeStyle: LegendaryStyle? = null, annotatedStringPrefixBuilder: (AnnotatedString.Builder.() -> Unit)? = null, annotatedStringSuffixBuilder: (AnnotatedString.Builder.() -> Unit)? = null, ) CyclomaticComplexMethod:NoteActionsRow.kt$@Composable fun FeedNoteActionsRow( modifier: Modifier, eventStats: EventStatsUi, isBookmarked: Boolean, highlightedNote: Boolean = false, showBookmark: Boolean = false, showCounts: Boolean = true, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongPressAction: ((FeedPostAction) -> Unit)? = null, ) @@ -54,7 +52,6 @@ LongMethod:HomeFeedScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeFeedScreen( state: HomeFeedContract.UiState, onTopLevelDestinationChanged: (PrimalTopLevelDestination) -> Unit, onDrawerScreenClick: (DrawerScreenDestination) -> Unit, onDrawerQrCodeClick: () -> Unit, onSearchClick: () -> Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, onNewPostClick: (content: TextFieldValue?) -> Unit, eventPublisher: (UiEvent) -> Unit, ) LongMethod:KeysSettingsScreen.kt$@Composable fun PrivateKeySection(nsec: String) LongMethod:LegendaryProfileCustomizationScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun LegendaryProfileCustomizationScreen( state: LegendaryProfileCustomizationContract.UiState, eventPublisher: (LegendaryProfileCustomizationContract.UiEvent) -> Unit, onClose: () -> Unit, ) - LongMethod:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString LongMethod:MessageConversationListScreen.kt$@Composable private fun ConversationListItem( conversation: MessageConversationUi, onConversationClick: (String) -> Unit, onProfileClick: (profileId: String) -> Unit, ) LongMethod:MessageConversationListScreen.kt$@Composable private fun MessagesTabs( relation: ConversationRelation, onFollowsTabClick: () -> Unit, onOtherTabClick: () -> Unit, onMarkAllRead: () -> Unit, ) LongMethod:MultipleUserPicker.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MultipleUserPicker( modifier: Modifier = Modifier, sheetTitle: String, placeholderText: String, onDismissRequest: () -> Unit, onUsersSelected: (Set<UserProfileItemUi>) -> Unit, sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), startingSelectedUsers: Set<UserProfileItemUi>, ) @@ -113,7 +110,6 @@ MagicNumber:PrimalDrawer.kt$0.5f MagicNumber:PrimalDrawerScaffold.kt$0.5f MatchingDeclarationName:__PrimalIcons.kt$PrimalIcons - NestedBlockDepth:MarkdownRichText.kt$private fun computeRichTextString(astNode: AstNode): RichTextString ReturnCount:LnInvoiceUtils.kt$LnInvoiceUtils$private fun getAmount(invoice: String): BigDecimal ReturnCount:MessagesRemoteMediator.kt$MessagesRemoteMediator$override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessage>): MediatorResult ReturnCount:NostrResources.kt$private fun String.nostrUriToBytes(): ByteArray? diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt index 3a475e499..c65acf02c 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/MarkdownRichText.kt @@ -1,3 +1,4 @@ +@file:Suppress("detekt:all") package net.primal.android.thread.articles.details.ui.richtext import androidx.compose.foundation.layout.fillMaxWidth @@ -31,7 +32,6 @@ import com.halilibo.richtext.ui.string.InlineContent import com.halilibo.richtext.ui.string.RichTextString import com.halilibo.richtext.ui.string.Text import com.halilibo.richtext.ui.string.withFormat -import timber.log.Timber /** * @@ -94,10 +94,12 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { } null } + is AstEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Italic) is AstStrikethrough -> richTextStringBuilder.pushFormat( RichTextString.Format.Strikethrough, ) + is AstImage -> { richTextStringBuilder.appendInlineContent( content = InlineContent( @@ -115,30 +117,35 @@ private fun computeRichTextString(astNode: AstNode): RichTextString { ) null } + is AstLink -> { - Timber.tag("astLink").i(currentNodeType.toString()) richTextStringBuilder.pushFormat( RichTextString.Format.Link( destination = currentNodeType.destination, ), ) } + is AstSoftLineBreak -> { richTextStringBuilder.append(" ") null } + is AstHardLineBreak -> { richTextStringBuilder.append("\n") null } + is AstStrongEmphasis -> richTextStringBuilder.pushFormat(RichTextString.Format.Bold) is AstText -> { richTextStringBuilder.append(currentNodeType.literal) null } + is AstLinkReferenceDefinition -> richTextStringBuilder.pushFormat( RichTextString.Format.Link(destination = currentNodeType.destination), ) + else -> null } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt index 777398d07..0085d1860 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/richtext/RemoteImage.kt @@ -1,3 +1,4 @@ +@file:Suppress("detekt:all") package net.primal.android.thread.articles.details.ui.richtext import androidx.compose.foundation.Image