diff --git a/README.md b/README.md
index 1cdb47da9..0cb257b32 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,8 @@
+
+
diff --git a/app/schemas/net.primal.android.db.PrimalDatabase/15.json b/app/schemas/net.primal.android.db.PrimalDatabase/15.json
new file mode 100644
index 000000000..dbbfc6611
--- /dev/null
+++ b/app/schemas/net.primal.android.db.PrimalDatabase/15.json
@@ -0,0 +1,1172 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 15,
+ "identityHash": "13dcdb83e37a441d942b1bc40bf9c1aa",
+ "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, 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
+ }
+ ],
+ "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, 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
+ }
+ ],
+ "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": "PostStats",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`postId` 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(`postId`))",
+ "fields": [
+ {
+ "fieldPath": "postId",
+ "columnName": "postId",
+ "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": [
+ "postId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "NoteNostrUri",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`postId` TEXT NOT NULL, `uri` TEXT NOT NULL, `refPost_postId` TEXT, `refPost_createdAt` INTEGER, `refPost_content` TEXT, `refPost_authorId` TEXT, `refPost_authorName` TEXT, `refPost_authorAvatarCdnImage` TEXT, `refPost_authorInternetIdentifier` TEXT, `refPost_authorLightningAddress` TEXT, `refPost_attachments` TEXT, `refPost_nostrUris` TEXT, `refUser_userId` TEXT, `refUser_handle` TEXT, PRIMARY KEY(`postId`, `uri`))",
+ "fields": [
+ {
+ "fieldPath": "postId",
+ "columnName": "postId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "referencedPost.postId",
+ "columnName": "refPost_postId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.createdAt",
+ "columnName": "refPost_createdAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.content",
+ "columnName": "refPost_content",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.authorId",
+ "columnName": "refPost_authorId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.authorName",
+ "columnName": "refPost_authorName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.authorAvatarCdnImage",
+ "columnName": "refPost_authorAvatarCdnImage",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.authorInternetIdentifier",
+ "columnName": "refPost_authorInternetIdentifier",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.authorLightningAddress",
+ "columnName": "refPost_authorLightningAddress",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.attachments",
+ "columnName": "refPost_attachments",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "referencedPost.nostrUris",
+ "columnName": "refPost_nostrUris",
+ "affinity": "TEXT",
+ "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": [
+ "postId",
+ "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}` (`directive` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`directive`))",
+ "fields": [
+ {
+ "fieldPath": "directive",
+ "columnName": "directive",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "directive"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "FeedPostDataCrossRef",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedDirective` TEXT NOT NULL, `eventId` TEXT NOT NULL, PRIMARY KEY(`feedDirective`, `eventId`))",
+ "fields": [
+ {
+ "fieldPath": "feedDirective",
+ "columnName": "feedDirective",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "feedDirective",
+ "eventId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_FeedPostDataCrossRef_feedDirective",
+ "unique": false,
+ "columnNames": [
+ "feedDirective"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeedPostDataCrossRef_feedDirective` ON `${TABLE_NAME}` (`feedDirective`)"
+ },
+ {
+ "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": "FeedPostSync",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `feedDirective` TEXT NOT NULL, `count` INTEGER NOT NULL, `postIds` TEXT NOT NULL, PRIMARY KEY(`timestamp`, `feedDirective`))",
+ "fields": [
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "feedDirective",
+ "columnName": "feedDirective",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "count",
+ "columnName": "count",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "postIds",
+ "columnName": "postIds",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "timestamp",
+ "feedDirective"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ThreadConversationCrossRef",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`postId` TEXT NOT NULL, `replyPostId` TEXT NOT NULL, PRIMARY KEY(`postId`, `replyPostId`))",
+ "fields": [
+ {
+ "fieldPath": "postId",
+ "columnName": "postId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyPostId",
+ "columnName": "replyPostId",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "postId",
+ "replyPostId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ThreadConversationCrossRef_postId",
+ "unique": false,
+ "columnNames": [
+ "postId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ThreadConversationCrossRef_postId` ON `${TABLE_NAME}` (`postId`)"
+ },
+ {
+ "name": "index_ThreadConversationCrossRef_replyPostId",
+ "unique": false,
+ "columnNames": [
+ "replyPostId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ThreadConversationCrossRef_replyPostId` ON `${TABLE_NAME}` (`replyPostId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PostUserStats",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`postId` 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(`postId`, `userId`))",
+ "fields": [
+ {
+ "fieldPath": "postId",
+ "columnName": "postId",
+ "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": [
+ "postId",
+ "userId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_PostUserStats_postId",
+ "unique": false,
+ "columnNames": [
+ "postId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PostUserStats_postId` ON `${TABLE_NAME}` (`postId`)"
+ },
+ {
+ "name": "index_PostUserStats_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PostUserStats_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ProfileStats",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `following` INTEGER NOT NULL, `followers` INTEGER NOT NULL, `notes` INTEGER NOT NULL, PRIMARY KEY(`profileId`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profileId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "following",
+ "columnName": "following",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "followers",
+ "columnName": "followers",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notes",
+ "columnName": "notes",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "profileId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "TrendingHashtag",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hashtag` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`hashtag`))",
+ "fields": [
+ {
+ "fieldPath": "hashtag",
+ "columnName": "hashtag",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "score",
+ "columnName": "score",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "hashtag"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "NotificationData",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ownerId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `type` TEXT NOT NULL, `seenGloballyAt` INTEGER, `actionUserId` TEXT, `actionPostId` TEXT, `satsZapped` INTEGER, PRIMARY KEY(`ownerId`, `createdAt`, `type`))",
+ "fields": [
+ {
+ "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": [
+ "ownerId",
+ "createdAt",
+ "type"
+ ]
+ },
+ "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` TEXT NOT NULL, `amountInUsd` TEXT, `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, `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": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amountInUsd",
+ "columnName": "amountInUsd",
+ "affinity": "TEXT",
+ "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": "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": []
+ }
+ ],
+ "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, '13dcdb83e37a441d942b1bc40bf9c1aa')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/FeedNoteCard.kt b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/FeedNoteCard.kt
index a2b371893..1ceb2b974 100644
--- a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/FeedNoteCard.kt
+++ b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/FeedNoteCard.kt
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -44,7 +45,9 @@ import net.primal.android.theme.domain.PrimalTheme
@Composable
fun FeedNoteCard(
data: FeedPostUi,
+ modifier: Modifier = Modifier,
shape: Shape = CardDefaults.shape,
+ colors: CardColors = CardDefaults.cardColors(),
cardPadding: PaddingValues = PaddingValues(all = 0.dp),
fullWidthNote: Boolean = false,
headerSingleLine: Boolean = true,
@@ -55,8 +58,8 @@ fun FeedNoteCard(
expanded: Boolean = false,
onPostClick: (String) -> Unit,
onProfileClick: (String) -> Unit,
- onPostAction: (FeedPostAction) -> Unit,
- onPostLongClickAction: (FeedPostAction) -> Unit,
+ onPostAction: ((FeedPostAction) -> Unit)? = null,
+ onPostLongClickAction: ((FeedPostAction) -> Unit)? = null,
onHashtagClick: (String) -> Unit,
onMediaClick: (String, String) -> Unit,
onMuteUserClick: () -> Unit,
@@ -71,7 +74,7 @@ fun FeedNoteCard(
val overflowIconSizeDp = 40.dp
NoteSurfaceCard(
- modifier = Modifier
+ modifier = modifier
.wrapContentHeight()
.padding(cardPadding)
.clickable(
@@ -80,6 +83,7 @@ fun FeedNoteCard(
onClick = { onPostClick(data.postId) },
),
shape = shape,
+ colors = colors,
drawLineAboveAvatar = drawLineAboveAvatar,
drawLineBelowAvatar = drawLineBelowAvatar,
lineOffsetX = (avatarSizeDp / 2) + avatarPaddingDp + notePaddingDp,
diff --git a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteHeader.kt b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteHeader.kt
index 03e756794..a69796f94 100644
--- a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteHeader.kt
+++ b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteHeader.kt
@@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -40,6 +41,8 @@ fun FeedNoteHeader(
authorAvatarVisible: Boolean = true,
authorAvatarCdnImage: CdnImage? = null,
authorInternetIdentifier: String? = null,
+ label: String? = authorInternetIdentifier,
+ labelStyle: TextStyle? = null,
onAuthorAvatarClick: (() -> Unit)? = null,
) {
Row(
@@ -84,22 +87,24 @@ fun FeedNoteHeader(
)
},
suffixFixedContent = {
- Text(
- text = " • ${postTimestamp?.asBeforeNowFormat().orEmpty()}",
- textAlign = TextAlign.Center,
- maxLines = 1,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
- )
+ if (postTimestamp != null) {
+ Text(
+ text = " • ${postTimestamp.asBeforeNowFormat()}",
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+ }
},
)
- if (!authorInternetIdentifier.isNullOrEmpty() && !singleLine) {
+ if (!label.isNullOrEmpty() && !singleLine) {
Text(
- text = authorInternetIdentifier,
+ text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- style = AppTheme.typography.bodySmall,
+ style = labelStyle ?: AppTheme.typography.bodySmall,
color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
)
}
diff --git a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteStatsRow.kt b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteStatsRow.kt
index c3ebdc39c..03acc0c64 100644
--- a/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteStatsRow.kt
+++ b/app/src/main/kotlin/net/primal/android/core/compose/feed/note/NoteStatsRow.kt
@@ -41,8 +41,8 @@ import net.primal.android.theme.AppTheme
fun FeedNoteStatsRow(
modifier: Modifier,
postStats: FeedPostStatsUi,
- onPostAction: (FeedPostAction) -> Unit,
- onPostLongPressAction: (FeedPostAction) -> Unit,
+ onPostAction: ((FeedPostAction) -> Unit)? = null,
+ onPostLongPressAction: ((FeedPostAction) -> Unit)? = null,
) {
Row(
modifier = modifier,
@@ -54,8 +54,12 @@ fun FeedNoteStatsRow(
iconVector = PrimalIcons.FeedReplies,
iconVectorHighlight = PrimalIcons.FeedRepliesFilled,
colorHighlight = AppTheme.extraColorScheme.replied,
- onClick = { onPostAction(FeedPostAction.Reply) },
- onLongClick = { onPostLongPressAction(FeedPostAction.Reply) },
+ onClick = onPostAction?.let {
+ { onPostAction(FeedPostAction.Reply) }
+ },
+ onLongClick = onPostLongPressAction?.let {
+ { onPostLongPressAction(FeedPostAction.Reply) }
+ },
)
SinglePostStat(
@@ -64,8 +68,12 @@ fun FeedNoteStatsRow(
iconVector = PrimalIcons.FeedZaps,
iconVectorHighlight = PrimalIcons.FeedZapsFilled,
colorHighlight = AppTheme.extraColorScheme.zapped,
- onClick = { onPostAction(FeedPostAction.Zap) },
- onLongClick = { onPostLongPressAction(FeedPostAction.Zap) },
+ onClick = onPostAction?.let {
+ { onPostAction(FeedPostAction.Zap) }
+ },
+ onLongClick = onPostLongPressAction?.let {
+ { onPostLongPressAction(FeedPostAction.Zap) }
+ },
)
SinglePostStat(
@@ -74,12 +82,14 @@ fun FeedNoteStatsRow(
iconVector = PrimalIcons.FeedLikes,
iconVectorHighlight = PrimalIcons.FeedLikesFilled,
colorHighlight = AppTheme.extraColorScheme.liked,
- onClick = {
- if (!postStats.userLiked) {
- onPostAction(FeedPostAction.Like)
- }
+ onClick = if (!postStats.userLiked && onPostAction != null) {
+ { onPostAction(FeedPostAction.Like) }
+ } else {
+ null
+ },
+ onLongClick = onPostLongPressAction?.let {
+ { onPostLongPressAction(FeedPostAction.Like) }
},
- onLongClick = { onPostLongPressAction(FeedPostAction.Like) },
)
SinglePostStat(
@@ -88,8 +98,12 @@ fun FeedNoteStatsRow(
iconVector = PrimalIcons.FeedReposts,
iconVectorHighlight = PrimalIcons.FeedRepostsFilled,
colorHighlight = AppTheme.extraColorScheme.reposted,
- onClick = { onPostAction(FeedPostAction.Repost) },
- onLongClick = { onPostLongPressAction(FeedPostAction.Repost) },
+ onClick = onPostAction?.let {
+ { onPostAction(FeedPostAction.Repost) }
+ },
+ onLongClick = onPostLongPressAction?.let {
+ { onPostLongPressAction(FeedPostAction.Repost) }
+ },
)
}
}
@@ -102,8 +116,8 @@ private fun SinglePostStat(
iconVector: ImageVector,
iconVectorHighlight: ImageVector,
colorHighlight: Color,
- onClick: () -> Unit,
- onLongClick: () -> Unit,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null,
) {
val titleText = buildAnnotatedString {
appendInlineContent("icon", "[icon]")
@@ -139,7 +153,8 @@ private fun SinglePostStat(
.clip(CircleShape)
.animateContentSize()
.combinedClickable(
- onClick = onClick,
+ enabled = onClick != null || onLongClick != null,
+ onClick = { onClick?.invoke() },
onLongClick = onLongClick,
),
text = titleText,
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 e117fd22a..c86cd17fd 100644
--- a/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt
+++ b/app/src/main/kotlin/net/primal/android/db/PrimalDatabase.kt
@@ -69,7 +69,7 @@ import net.primal.android.wallet.db.WalletTransactionData
MessageConversationData::class,
WalletTransactionData::class,
],
- version = 14,
+ version = 15,
exportSchema = true,
)
@TypeConverters(
diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt
index 2a9756832..6b340cdcd 100644
--- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt
+++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt
@@ -19,7 +19,9 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import net.primal.android.core.compose.feed.model.asFeedPostUi
+import net.primal.android.core.coroutines.CoroutineDispatcherProvider
import net.primal.android.core.files.FileAnalyser
import net.primal.android.core.files.error.UnsuccessfulFileUpload
import net.primal.android.editor.NoteEditorContract.SideEffect
@@ -36,10 +38,12 @@ import net.primal.android.networking.relays.errors.NostrPublishException
import net.primal.android.networking.sockets.errors.WssException
import net.primal.android.user.accounts.active.ActiveAccountStore
import net.primal.android.user.accounts.active.ActiveUserAccountState
+import timber.log.Timber
@HiltViewModel
class NoteEditorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
+ private val dispatcherProvider: CoroutineDispatcherProvider,
private val fileAnalyser: FileAnalyser,
private val activeAccountStore: ActiveAccountStore,
private val feedRepository: FeedRepository,
@@ -119,9 +123,11 @@ class NoteEditorViewModel @Inject constructor(
private fun fetchRepliesFromNetwork(replyToNoteId: String) =
viewModelScope.launch {
try {
- feedRepository.fetchReplies(postId = replyToNoteId)
+ withContext(dispatcherProvider.io()) {
+ feedRepository.fetchReplies(postId = replyToNoteId)
+ }
} catch (error: WssException) {
- // Ignore
+ Timber.e(error)
}
}
diff --git a/app/src/main/kotlin/net/primal/android/feed/repository/FeedRepository.kt b/app/src/main/kotlin/net/primal/android/feed/repository/FeedRepository.kt
index 0b67da289..097e6f0e6 100644
--- a/app/src/main/kotlin/net/primal/android/feed/repository/FeedRepository.kt
+++ b/app/src/main/kotlin/net/primal/android/feed/repository/FeedRepository.kt
@@ -6,10 +6,8 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.withContext
import net.primal.android.core.ext.isLatestFeed
import net.primal.android.db.PrimalDatabase
import net.primal.android.feed.api.FeedApi
@@ -63,23 +61,22 @@ class FeedRepository @Inject constructor(
userId = activeAccountStore.activeUserId(),
)
- suspend fun fetchReplies(postId: String) =
- withContext(Dispatchers.IO) {
- val userId = activeAccountStore.activeUserId()
- val response = feedApi.getThread(
- ThreadRequestBody(postId = postId, userPubKey = userId, limit = 100),
- )
+ suspend fun fetchReplies(postId: String) {
+ val userId = activeAccountStore.activeUserId()
+ val response = feedApi.getThread(
+ ThreadRequestBody(postId = postId, userPubKey = userId, limit = 100),
+ )
- response.persistToDatabaseAsTransaction(userId = userId, database = database)
- database.conversationConnections().connect(
- data = response.posts.map {
- ThreadConversationCrossRef(
- postId = postId,
- replyPostId = it.id,
- )
- },
- )
- }
+ response.persistToDatabaseAsTransaction(userId = userId, database = database)
+ database.conversationConnections().connect(
+ data = response.posts.map {
+ ThreadConversationCrossRef(
+ postId = postId,
+ replyPostId = it.id,
+ )
+ },
+ )
+ }
@OptIn(ExperimentalPagingApi::class)
private fun createPager(feedDirective: String, pagingSourceFactory: () -> PagingSource) =
@@ -105,6 +102,7 @@ class FeedRepository @Inject constructor(
feedDirective = feedDirective,
userPubkey = activeAccountStore.activeUserId(),
)
+
else -> ExploreFeedQueryBuilder(
feedDirective = feedDirective,
userPubkey = activeAccountStore.activeUserId(),
diff --git a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt
index c008a066f..a464f0827 100644
--- a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt
+++ b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt
@@ -162,12 +162,12 @@ private fun NavController.navigateToWalletSettings(nwcUrl: String? = null) =
else -> navigate(route = "wallet_settings")
}
-private fun NavController.navigateToThread(noteId: String) = navigate(route = "thread/$noteId")
+fun NavController.navigateToThread(noteId: String) = navigate(route = "thread/$noteId")
-private fun NavController.navigateToMediaGallery(noteId: String, mediaUrl: String) =
+fun NavController.navigateToMediaGallery(noteId: String, mediaUrl: String) =
navigate(route = "media/$noteId?$MEDIA_URL=$mediaUrl")
-private fun NavController.navigateToExploreFeed(query: String) = navigate(route = "explore/$query")
+fun NavController.navigateToExploreFeed(query: String) = navigate(route = "explore/$query")
@OptIn(ExperimentalMaterialNavigationApi::class)
@Composable
diff --git a/app/src/main/kotlin/net/primal/android/navigation/WalletNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/WalletNavigation.kt
index b7740fe6e..8d38f991e 100644
--- a/app/src/main/kotlin/net/primal/android/navigation/WalletNavigation.kt
+++ b/app/src/main/kotlin/net/primal/android/navigation/WalletNavigation.kt
@@ -189,5 +189,14 @@ private fun NavGraphBuilder.transactionDetails(
TransactionDetailsScreen(
viewModel = viewModel,
onClose = { navController.navigateUp() },
+ onPostClick = { noteId -> navController.navigateToThread(noteId) },
+ onProfileClick = { profileId -> navController.navigateToProfile(profileId) },
+ onHashtagClick = { hashtag -> navController.navigateToExploreFeed(query = hashtag) },
+ onMediaClick = { noteId, mediaUrl ->
+ navController.navigateToMediaGallery(
+ noteId = noteId,
+ mediaUrl = mediaUrl,
+ )
+ },
)
}
diff --git a/app/src/main/kotlin/net/primal/android/nostr/ext/PrimalWalletEvents.kt b/app/src/main/kotlin/net/primal/android/nostr/ext/PrimalWalletEvents.kt
index eca4e86bc..19479a263 100644
--- a/app/src/main/kotlin/net/primal/android/nostr/ext/PrimalWalletEvents.kt
+++ b/app/src/main/kotlin/net/primal/android/nostr/ext/PrimalWalletEvents.kt
@@ -24,8 +24,10 @@ fun ContentWalletTransaction.asWalletTransactionPO(walletAddress: String): Walle
type = this.type,
state = this.state,
createdAt = this.createdAt,
+ updatedAt = this.updatedAt,
completedAt = this.completedAt,
amountInBtc = this.amountInBtc,
+ amountInUsd = this.amountInUsd,
userId = this.selfPubkey,
userSubWallet = this.selfSubWallet,
userLightningAddress = this.selfLud16,
@@ -37,5 +39,9 @@ fun ContentWalletTransaction.asWalletTransactionPO(walletAddress: String): Walle
zapNoteId = zapEvent?.tags?.findFirstEventId(),
zapNoteAuthorId = zapEvent?.tags?.findFirstProfileId(),
zappedByUserId = zapEvent?.pubKey,
+ invoice = this.invoice,
+ totalFeeInBtc = this.totalFeeInBtc,
+ exchangeRate = this.exchangeRate,
+ onChainAddress = this.onChainAddress,
)
}
diff --git a/app/src/main/kotlin/net/primal/android/nostr/model/primal/content/ContentWalletTransaction.kt b/app/src/main/kotlin/net/primal/android/nostr/model/primal/content/ContentWalletTransaction.kt
index 0f42aed99..e109471de 100644
--- a/app/src/main/kotlin/net/primal/android/nostr/model/primal/content/ContentWalletTransaction.kt
+++ b/app/src/main/kotlin/net/primal/android/nostr/model/primal/content/ContentWalletTransaction.kt
@@ -12,6 +12,7 @@ data class ContentWalletTransaction(
val type: TxType,
val state: TxState,
@SerialName("created_at") val createdAt: Long,
+ @SerialName("updated_at") val updatedAt: Long,
@SerialName("completed_at") val completedAt: Long?,
@SerialName("amount_btc") val amountInBtc: String,
@SerialName("amount_usd") val amountInUsd: String?,
@@ -25,4 +26,8 @@ data class ContentWalletTransaction(
@SerialName("is_zap") val isZap: Boolean,
@SerialName("zap_request") val zapRequestRawJson: String?,
@SerialName("is_in_app_purchase") val isInAppPurchase: Boolean,
+ val invoice: String?,
+ @SerialName("total_fee_btc") val totalFeeInBtc: String?,
+ @SerialName("exchange_rate") val exchangeRate: String?,
+ @SerialName("onchainAddress") val onChainAddress: String?,
)
diff --git a/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt b/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt
index 6b3af1e02..c889cc8ab 100644
--- a/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt
+++ b/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt
@@ -7,7 +7,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,6 +18,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.primal.android.core.compose.feed.model.asFeedPostUi
+import net.primal.android.core.coroutines.CoroutineDispatcherProvider
import net.primal.android.feed.repository.FeedRepository
import net.primal.android.feed.repository.PostRepository
import net.primal.android.navigation.noteIdOrThrow
@@ -36,10 +36,12 @@ import net.primal.android.wallet.zaps.InvalidZapRequestException
import net.primal.android.wallet.zaps.ZapFailureException
import net.primal.android.wallet.zaps.ZapHandler
import net.primal.android.wallet.zaps.hasWallet
+import timber.log.Timber
@HiltViewModel
class ThreadViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
+ private val dispatcherProvider: CoroutineDispatcherProvider,
private val activeAccountStore: ActiveAccountStore,
private val feedRepository: FeedRepository,
private val postRepository: PostRepository,
@@ -110,7 +112,7 @@ class ThreadViewModel @Inject constructor(
}
private suspend fun loadHighlightedPost() {
- val rootPost = withContext(Dispatchers.IO) { feedRepository.findPostById(postId = postId) }
+ val rootPost = withContext(dispatcherProvider.io()) { feedRepository.findPostById(postId = postId) }
if (rootPost != null) {
setState {
copy(
@@ -144,9 +146,11 @@ class ThreadViewModel @Inject constructor(
private fun fetchRepliesFromNetwork() =
viewModelScope.launch {
try {
- feedRepository.fetchReplies(postId = postId)
+ withContext(dispatcherProvider.io()) {
+ feedRepository.fetchReplies(postId = postId)
+ }
} catch (error: WssException) {
- // Ignore
+ Timber.e(error)
}
}
@@ -181,7 +185,7 @@ class ThreadViewModel @Inject constructor(
private fun zapPost(zapAction: UiEvent.ZapAction) =
viewModelScope.launch {
- val postAuthorProfileData = withContext(Dispatchers.IO) {
+ val postAuthorProfileData = withContext(dispatcherProvider.io()) {
profileRepository.findProfileData(profileId = zapAction.postAuthorId)
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt
index 06e2457b4..6fa896527 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -153,7 +154,8 @@ fun WalletDashboardScreen(
}
}
- val dashboardExpanded by remember(listState) { derivedStateOf { listState.firstVisibleItemScrollOffset == 0 } }
+ val isScrolledToTop by remember(listState) { derivedStateOf { listState.firstVisibleItemScrollOffset == 0 } }
+ val dashboardExpanded by rememberSaveable(isScrolledToTop) { mutableStateOf(isScrolledToTop) }
var topBarHeight by remember { mutableIntStateOf(0) }
diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt
index d8a8e0884..38cd9ea79 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/WalletDashboardViewModel.kt
@@ -123,9 +123,9 @@ class WalletDashboardViewModel @Inject constructor(
}
private fun Flow>.mapAsPagingDataOfTransactionUi() =
- map { pagingData -> pagingData.map { it.mapAsTransactionUi() } }
+ map { pagingData -> pagingData.map { it.mapAsTransactionDataUi() } }
- private fun WalletTransaction.mapAsTransactionUi() =
+ private fun WalletTransaction.mapAsTransactionDataUi() =
TransactionDataUi(
txId = this.data.id,
txType = this.data.type,
diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletBalanceText.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/BtcAmountText.kt
similarity index 87%
rename from app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletBalanceText.kt
rename to app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/BtcAmountText.kt
index 8baae1460..cfad2f8a0 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletBalanceText.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/BtcAmountText.kt
@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
@@ -20,10 +21,12 @@ import net.primal.android.theme.AppTheme
import net.primal.android.wallet.utils.CurrencyConversionUtils.toSats
@Composable
-fun AmountText(
+fun BtcAmountText(
modifier: Modifier,
amountInBtc: BigDecimal?,
textSize: TextUnit = 42.sp,
+ amountColor: Color = Color.Unspecified,
+ currencyColor: Color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
) {
val numberFormat = remember { NumberFormat.getNumberInstance() }
@@ -37,6 +40,7 @@ fun AmountText(
textAlign = TextAlign.Center,
style = AppTheme.typography.displayMedium,
fontSize = textSize,
+ color = amountColor,
)
if (amountInBtc != null) {
@@ -46,7 +50,7 @@ fun AmountText(
textAlign = TextAlign.Center,
maxLines = 1,
style = AppTheme.typography.bodyMedium,
- color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
+ color = currencyColor,
)
}
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboard.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboard.kt
index f2310cc4c..ded2b56cb 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboard.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboard.kt
@@ -25,7 +25,7 @@ fun WalletDashboard(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
- AmountText(
+ BtcAmountText(
modifier = Modifier.wrapContentWidth()
.padding(start = if (walletBalance != null) 32.dp else 0.dp)
.padding(bottom = 32.dp),
diff --git a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboardLite.kt b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboardLite.kt
index b0dc77080..ab9461c9d 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboardLite.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/dashboard/ui/WalletDashboardLite.kt
@@ -22,7 +22,7 @@ fun WalletDashboardLite(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
- AmountText(
+ BtcAmountText(
modifier = Modifier.graphicsLayer {
clip = false
translationY = 4.dp.toPx()
diff --git a/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionDao.kt b/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionDao.kt
index 3e57f7161..bc317e0ef 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionDao.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionDao.kt
@@ -22,4 +22,8 @@ interface WalletTransactionDao {
@Query("SELECT * FROM WalletTransactionData ORDER BY createdAt ASC LIMIT 1")
fun last(): WalletTransactionData?
+
+ @Transaction
+ @Query("SELECT * FROM WalletTransactionData WHERE id IS :txId")
+ fun findTransactionById(txId: String): WalletTransaction?
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionData.kt b/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionData.kt
index 904236f86..4f60d1bc5 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionData.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/db/WalletTransactionData.kt
@@ -14,8 +14,10 @@ data class WalletTransactionData(
val type: TxType,
val state: TxState,
val createdAt: Long,
+ val updatedAt: Long,
val completedAt: Long?,
val amountInBtc: String,
+ val amountInUsd: String?,
val isZap: Boolean,
val isStorePurchase: Boolean,
val userId: String,
@@ -24,6 +26,10 @@ data class WalletTransactionData(
val otherUserId: String?,
val otherLightningAddress: String?,
val note: String?,
+ val invoice: String?,
+ val totalFeeInBtc: String?,
+ val exchangeRate: String?,
+ val onChainAddress: String?,
val zapNoteId: String?,
val zapNoteAuthorId: String?,
val zappedByUserId: String?,
diff --git a/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt b/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
index 26c3f3daf..987346055 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/repository/WalletRepository.kt
@@ -37,6 +37,8 @@ class WalletRepository @Inject constructor(
database.walletTransactions().latestTransactionsPaged()
}.flow
+ fun findTransactionById(txId: String) = database.walletTransactions().findTransactionById(txId = txId)
+
suspend fun fetchUserWalletInfoAndUpdateUserAccount(userId: String) {
withContext(dispatcherProvider.io()) {
val response = walletApi.getWalletUserInfo(userId)
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt
new file mode 100644
index 000000000..273980760
--- /dev/null
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailDataUi.kt
@@ -0,0 +1,27 @@
+package net.primal.android.wallet.transactions.details
+
+import java.time.Instant
+import net.primal.android.attachments.domain.CdnImage
+import net.primal.android.wallet.domain.TxState
+import net.primal.android.wallet.domain.TxType
+
+data class TransactionDetailDataUi(
+ val txId: String,
+ val txType: TxType,
+ val txState: TxState,
+ val txInstant: Instant,
+ val txAmountInSats: ULong,
+ val txAmountInUsd: String?,
+ val txNote: String?,
+ val isZap: Boolean,
+ val isStorePurchase: Boolean,
+ val invoice: String?,
+ val totalFeeInSats: ULong?,
+ val exchangeRate: String?,
+ val onChainAddress: String?,
+ val otherUserId: String? = null,
+ val otherUserAvatarCdnImage: CdnImage? = null,
+ val otherUserInternetIdentifier: String? = null,
+ val otherUserDisplayName: String? = null,
+ val otherUserLightningAddress: String? = null,
+)
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsContract.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsContract.kt
index 235dd6f64..0bf583416 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsContract.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsContract.kt
@@ -1,8 +1,11 @@
package net.primal.android.wallet.transactions.details
+import net.primal.android.core.compose.feed.model.FeedPostUi
+
interface TransactionDetailsContract {
data class UiState(
val loading: Boolean = false,
+ val txData: TransactionDetailDataUi? = null,
+ val feedPost: FeedPostUi? = null,
)
- sealed class UiEvent
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt
index bf94ff26f..593ba75a7 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsScreen.kt
@@ -1,75 +1,530 @@
package net.primal.android.wallet.transactions.details
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Spring.StiffnessMediumLow
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import java.text.NumberFormat
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.util.*
+import net.primal.android.LocalPrimalTheme
+import net.primal.android.R
+import net.primal.android.core.compose.IconText
+import net.primal.android.core.compose.PrimalDivider
import net.primal.android.core.compose.PrimalTopAppBar
+import net.primal.android.core.compose.feed.note.FeedNoteCard
+import net.primal.android.core.compose.feed.note.FeedNoteHeader
import net.primal.android.core.compose.icons.PrimalIcons
import net.primal.android.core.compose.icons.primaliconpack.ArrowBack
+import net.primal.android.core.compose.icons.primaliconpack.Copy
+import net.primal.android.core.compose.icons.primaliconpack.WalletLnPayment
+import net.primal.android.core.utils.ellipsizeMiddle
+import net.primal.android.theme.AppTheme
import net.primal.android.theme.PrimalTheme
+import net.primal.android.wallet.dashboard.ui.BtcAmountText
+import net.primal.android.wallet.domain.TxState
+import net.primal.android.wallet.domain.TxType
import net.primal.android.wallet.transactions.details.TransactionDetailsContract.UiState
-import net.primal.android.wallet.transactions.list.TransactionDataUi
+import net.primal.android.wallet.transactions.list.TransactionIcon
+import net.primal.android.wallet.utils.CurrencyConversionUtils.toBtc
+import net.primal.android.wallet.walletDepositColor
+import net.primal.android.wallet.walletWithdrawColor
+import timber.log.Timber
@Composable
-fun TransactionDetailsScreen(viewModel: TransactionDetailsViewModel, onClose: () -> Unit) {
+fun TransactionDetailsScreen(
+ viewModel: TransactionDetailsViewModel,
+ onClose: () -> Unit,
+ onPostClick: (String) -> Unit,
+ onProfileClick: (String) -> Unit,
+ onHashtagClick: (String) -> Unit,
+ onMediaClick: (String, String) -> Unit,
+) {
val uiState = viewModel.state.collectAsState()
TransactionDetailsScreen(
state = uiState.value,
onClose = onClose,
+ onPostClick = onPostClick,
+ onProfileClick = onProfileClick,
+ onHashtagClick = onHashtagClick,
+ onMediaClick = onMediaClick,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun TransactionDetailsScreen(state: UiState, onClose: () -> Unit) {
+fun TransactionDetailsScreen(
+ state: UiState,
+ onClose: () -> Unit,
+ onPostClick: (String) -> Unit,
+ onProfileClick: (String) -> Unit,
+ onHashtagClick: (String) -> Unit,
+ onMediaClick: (String, String) -> Unit,
+) {
val scrollState = rememberScrollState()
+ val showTopBarDivider by remember { derivedStateOf { scrollState.value > 0 } }
Scaffold(
topBar = {
PrimalTopAppBar(
- title = "Calculating...",
+ title = state.txData.resolveTitle(),
navigationIcon = PrimalIcons.ArrowBack,
- showDivider = false,
+ showDivider = showTopBarDivider,
onNavigationIconClick = onClose,
)
},
content = { paddingValues ->
Column(
modifier = Modifier
+ .fillMaxSize()
.padding(paddingValues)
- .verticalScroll(state = scrollState),
+ .verticalScroll(state = scrollState)
+ .navigationBarsPadding(),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
+ state.txData?.let { txData ->
+ val color = when (state.txData.txType) {
+ TxType.DEPOSIT -> walletDepositColor
+ TxType.WITHDRAW -> walletWithdrawColor
+ }
+
+ BtcAmountText(
+ modifier = Modifier
+ .wrapContentWidth()
+ .padding(vertical = 32.dp)
+ .padding(start = 32.dp),
+ amountInBtc = txData.txAmountInSats.toBtc().toBigDecimal(),
+ textSize = 48.sp,
+ amountColor = color,
+ currencyColor = color,
+ )
+
+ val text = when (txData.txType) {
+ TxType.DEPOSIT -> stringResource(id = R.string.wallet_transaction_details_received_from)
+ TxType.WITHDRAW -> stringResource(id = R.string.wallet_transaction_details_sent_to)
+ }
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 8.dp),
+ text = text.uppercase(),
+ textAlign = TextAlign.Start,
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+
+ TransactionCard(
+ txData = txData,
+ onProfileClick = onProfileClick,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ state.feedPost?.let {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 8.dp),
+ text = stringResource(id = R.string.wallet_transaction_details_zapped_note).uppercase(),
+ textAlign = TextAlign.Start,
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+
+ FeedNoteCard(
+ data = it,
+ modifier = Modifier.padding(horizontal = 12.dp),
+ colors = transactionCardColors(),
+ onPostClick = onPostClick,
+ onProfileClick = onProfileClick,
+ onHashtagClick = onHashtagClick,
+ onMediaClick = onMediaClick,
+ onMuteUserClick = {},
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+ }
}
},
)
}
-class TransactionParameterProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf()
+@Composable
+private fun TransactionDetailDataUi?.resolveTitle(): String {
+ return when (this?.txType) {
+ TxType.DEPOSIT -> if (isZap) {
+ stringResource(id = R.string.wallet_transaction_details_title_zap_received)
+ } else {
+ stringResource(id = R.string.wallet_transaction_details_title_payment_received)
+ }
+
+ TxType.WITHDRAW -> if (isZap) {
+ stringResource(id = R.string.wallet_transaction_details_title_zap_sent)
+ } else {
+ stringResource(id = R.string.wallet_transaction_details_title_payment_sent)
+ }
+
+ else -> ""
+ }
+}
+
+@Composable
+private fun transactionCardColors(): CardColors {
+ return if (LocalPrimalTheme.current.isDarkTheme) {
+ CardDefaults.cardColors(
+ containerColor = AppTheme.extraColorScheme.surfaceVariantAlt2,
+ contentColor = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+ } else {
+ CardDefaults.cardColors()
+ }
+}
+
+@Composable
+private fun TransactionDetailDataUi.typeToReadableString(): String {
+ return when {
+ isZap -> stringResource(id = R.string.wallet_transaction_details_type_nostr_zap)
+ isStorePurchase -> stringResource(id = R.string.wallet_transaction_details_type_in_app_purchase)
+ onChainAddress != null -> stringResource(
+ id = R.string.wallet_transaction_details_type_on_chain_payment,
+ )
+
+ else -> stringResource(id = R.string.wallet_transaction_details_type_lightning_payment)
+ }
+}
+
+@Composable
+private fun TransactionCard(txData: TransactionDetailDataUi, onProfileClick: (String) -> Unit) {
+ val numberFormat = remember { NumberFormat.getNumberInstance().apply { maximumFractionDigits = 2 } }
+
+ val isExpandable = txData.isZap && (
+ txData.txAmountInUsd != null || txData.exchangeRate != null ||
+ txData.totalFeeInSats != null || txData.invoice != null
+ )
+
+ var expanded by remember { mutableStateOf(!txData.isZap) }
+
+ Card(
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .animateContentSize(
+ animationSpec = spring(stiffness = StiffnessMediumLow),
+ ),
+ colors = transactionCardColors(),
+ ) {
+ if (!txData.otherUserDisplayName.isNullOrEmpty()) {
+ FeedNoteHeader(
+ modifier = Modifier
+ .padding(horizontal = 12.dp, vertical = 12.dp)
+ .fillMaxWidth()
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ enabled = txData.otherUserId != null,
+ onClick = { txData.otherUserId?.let(onProfileClick) },
+ ),
+ authorAvatarSize = 42.dp,
+ authorDisplayName = txData.otherUserDisplayName,
+ authorAvatarCdnImage = txData.otherUserAvatarCdnImage,
+ authorInternetIdentifier = txData.otherUserInternetIdentifier,
+ onAuthorAvatarClick = { txData.otherUserId?.let(onProfileClick) },
+ label = txData.otherUserLightningAddress,
+ labelStyle = AppTheme.typography.bodyMedium,
+ )
+ } else {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 12.dp, vertical = 12.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ TransactionIcon(
+ background = Color(0xFF222222),
+ ) {
+ Image(
+ imageVector = PrimalIcons.WalletLnPayment,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(color = AppTheme.extraColorScheme.zapped),
+ )
+ }
+
+ Column(
+ modifier = Modifier.padding(horizontal = 10.dp),
+ ) {
+ Text(
+ text = txData.typeToReadableString(),
+ style = AppTheme.typography.bodyMedium.copy(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ ),
+ )
+
+ txData.otherUserLightningAddress?.let { lud16Receiver ->
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = lud16Receiver,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+ }
+ }
+ }
+ }
+
+ txData.txNote?.ifEmpty { null }?.let { note ->
+ Text(
+ text = note,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp),
+ color = AppTheme.colorScheme.onPrimary,
+ style = AppTheme.typography.bodyMedium.copy(fontSize = 16.sp),
+ )
+ }
+
+ PrimalDivider()
+ TransactionDetailListItem(
+ section = stringResource(id = R.string.wallet_transaction_details_date_item),
+ value = txData.txInstant.formatToDefaultFormat(FormatStyle.MEDIUM),
+ )
+
+ PrimalDivider()
+ TransactionDetailListItem(
+ section = stringResource(id = R.string.wallet_transaction_details_status_item),
+ value = txData.txState.toReadableString(),
+ )
+
+ PrimalDivider()
+ TransactionDetailListItem(
+ section = stringResource(id = R.string.wallet_transaction_details_type_item),
+ value = txData.typeToReadableString(),
+ )
+
+ if (expanded) {
+ if (txData.txAmountInUsd != null || txData.exchangeRate != null) {
+ val usdAmount = txData.txAmountInUsd?.toDouble() ?: txData.exchangeRate?.let { rate ->
+ txData.txAmountInSats.toBtc() / rate.toDouble()
+ }
+
+ val formattedUsdAmount = try {
+ numberFormat.format(usdAmount)
+ } catch (error: IllegalArgumentException) {
+ Timber.e(error)
+ null
+ }
+
+ if (formattedUsdAmount != null) {
+ PrimalDivider()
+ TransactionDetailListItem(
+ section = stringResource(id = R.string.wallet_transaction_details_original_usd_item),
+ value = "$$formattedUsdAmount",
+ )
+ }
+ }
+
+ txData.totalFeeInSats?.let { feeAmount ->
+ PrimalDivider()
+ TransactionDetailListItem(
+ section = stringResource(id = R.string.wallet_transaction_details_fee_item),
+ value = "${
+ numberFormat.format(
+ feeAmount.toLong(),
+ )
+ } ${stringResource(id = R.string.wallet_sats_suffix)}",
+ )
+ }
+
+ txData.invoice?.let { invoice ->
+ val clipboardManager = LocalClipboardManager.current
+
+ PrimalDivider()
+ TransactionDetailListItem(
+ modifier = Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = {
+ clipboardManager.setText(AnnotatedString(text = invoice))
+ },
+ ),
+ section = stringResource(id = R.string.wallet_transaction_details_invoice_item),
+ value = invoice.ellipsizeMiddle(size = 10),
+ trailingIcon = PrimalIcons.Copy,
+ )
+ }
+ }
+
+ if (isExpandable) {
+ PrimalDivider()
+ IconText(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { expanded = !expanded },
+ ),
+ text = stringResource(id = R.string.wallet_transaction_details_expand_collapse_hint),
+ textAlign = TextAlign.Center,
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
+ trailingIcon = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ trailingIconTintColor = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
+ )
+ }
+ }
+}
+
+@Composable
+private fun TxState.toReadableString(): String {
+ return when (this) {
+ TxState.CREATED -> stringResource(id = R.string.wallet_transaction_details_status_created)
+ TxState.PROCESSING -> stringResource(id = R.string.wallet_transaction_details_status_processing)
+ TxState.SUCCEEDED -> stringResource(id = R.string.wallet_transaction_details_status_succeeded)
+ TxState.FAILED -> stringResource(id = R.string.wallet_transaction_details_status_failed)
+ TxState.CANCELED -> stringResource(id = R.string.wallet_transaction_details_status_canceled)
+ }
+}
+
+@Composable
+fun Instant.formatToDefaultFormat(dateTimeStyle: FormatStyle): String {
+ val zoneId: ZoneId = ZoneId.systemDefault()
+ val locale: Locale = Locale.getDefault()
+
+ val formatter: DateTimeFormatter = DateTimeFormatter
+ .ofLocalizedDateTime(dateTimeStyle)
+ .withLocale(locale)
+
+ return formatter.format(this.atZone(zoneId))
+}
+
+@Composable
+private fun TransactionDetailListItem(
+ section: String,
+ value: String,
+ modifier: Modifier = Modifier,
+ trailingIcon: ImageVector? = null,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(horizontal = 12.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = section,
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+
+ IconText(
+ text = value,
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ trailingIcon = trailingIcon,
+ trailingIconTintColor = AppTheme.extraColorScheme.onSurfaceVariantAlt2,
+ )
+ }
+}
+
+class TransactionParameterProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ TransactionDetailDataUi(
+ txId = "123",
+ txType = TxType.DEPOSIT,
+ txAmountInSats = 9999.toULong(),
+ txNote = "Bought sats from Primal",
+ txInstant = Instant.now(),
+ otherUserId = "storeId",
+ otherUserAvatarCdnImage = null,
+ isZap = false,
+ isStorePurchase = true,
+ exchangeRate = null,
+ txAmountInUsd = null,
+ txState = TxState.SUCCEEDED,
+ invoice = "",
+ onChainAddress = null,
+ totalFeeInSats = null,
+ ),
+ )
}
@Preview
@Composable
fun PreviewTransactionDetail(
@PreviewParameter(provider = TransactionParameterProvider::class)
- parameter: TransactionDataUi,
+ txDataParam: TransactionDetailDataUi,
) {
PrimalTheme(primalTheme = net.primal.android.theme.domain.PrimalTheme.Sunset) {
Surface {
TransactionDetailsScreen(
- state = UiState(),
+ state = UiState(
+ txData = txDataParam,
+ ),
onClose = {},
+ onProfileClick = {},
+ onPostClick = {},
+ onHashtagClick = {},
+ onMediaClick = { _, _ -> },
)
}
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt
index f7453890f..6074dea2a 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/details/TransactionDetailsViewModel.kt
@@ -4,21 +4,33 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import java.time.Instant
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.getAndUpdate
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.primal.android.core.compose.feed.model.asFeedPostUi
+import net.primal.android.core.coroutines.CoroutineDispatcherProvider
+import net.primal.android.core.utils.authorNameUiFriendly
+import net.primal.android.feed.repository.FeedRepository
import net.primal.android.navigation.transactionIdOrThrow
+import net.primal.android.networking.sockets.errors.WssException
+import net.primal.android.wallet.db.WalletTransaction
import net.primal.android.wallet.repository.WalletRepository
-import net.primal.android.wallet.transactions.details.TransactionDetailsContract.UiEvent
import net.primal.android.wallet.transactions.details.TransactionDetailsContract.UiState
+import net.primal.android.wallet.utils.CurrencyConversionUtils.toSats
+import timber.log.Timber
@HiltViewModel
class TransactionDetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
+ private val dispatcherProvider: CoroutineDispatcherProvider,
private val walletRepository: WalletRepository,
+ private val feedRepository: FeedRepository,
) : ViewModel() {
private val transactionId = savedStateHandle.transactionIdOrThrow
@@ -28,16 +40,65 @@ class TransactionDetailsViewModel @Inject constructor(
private fun setState(reducer: UiState.() -> UiState) =
viewModelScope.launch { _state.getAndUpdate { it.reducer() } }
- private val events = MutableSharedFlow()
- fun setEvent(event: UiEvent) = viewModelScope.launch { events.emit(event) }
-
init {
- observeEvents()
+ loadTransaction()
}
- private fun observeEvents() =
+ private fun loadTransaction() =
+ viewModelScope.launch {
+ val tx = withContext(dispatcherProvider.io()) {
+ walletRepository.findTransactionById(txId = transactionId)
+ }
+ setState { copy(loading = false, txData = tx?.mapAsTransactionDataUi()) }
+ tx?.data?.zapNoteId?.let {
+ observeZappedNote(it)
+ fetchZappedNote(it)
+ }
+ }
+
+ private fun observeZappedNote(noteId: String) =
viewModelScope.launch {
- events.collect {
+ feedRepository.observeConversation(postId = noteId)
+ .filter { it.isNotEmpty() }
+ .mapNotNull { conversation -> conversation.first { it.data.postId == noteId } }
+ .collect {
+ setState { copy(feedPost = it.asFeedPostUi()) }
+ }
+ }
+
+ private fun fetchZappedNote(noteId: String) =
+ viewModelScope.launch {
+ setState { copy(loading = true) }
+ try {
+ withContext(dispatcherProvider.io()) {
+ feedRepository.fetchReplies(postId = noteId)
+ }
+ } catch (error: WssException) {
+ Timber.e(error)
+ } finally {
+ setState { copy(loading = false) }
}
}
+
+ private fun WalletTransaction.mapAsTransactionDataUi() =
+ TransactionDetailDataUi(
+ txId = this.data.id,
+ txType = this.data.type,
+ txState = this.data.state,
+ txAmountInSats = this.data.amountInBtc.toBigDecimal().abs().toSats(),
+ txAmountInUsd = this.data.amountInUsd,
+ txInstant = Instant.ofEpochSecond(this.data.completedAt ?: this.data.createdAt),
+ txNote = this.data.note,
+ invoice = this.data.invoice,
+ totalFeeInSats = this.data.totalFeeInBtc?.toBigDecimal()?.abs()?.toSats(),
+ exchangeRate = this.data.exchangeRate,
+ onChainAddress = this.data.onChainAddress,
+ otherUserId = this.data.otherUserId,
+ otherUserAvatarCdnImage = this.otherProfileData?.avatarCdnImage,
+ otherUserDisplayName = this.otherProfileData?.authorNameUiFriendly(),
+ otherUserInternetIdentifier = this.otherProfileData?.internetIdentifier,
+ otherUserLightningAddress = this.data.otherLightningAddress,
+ isZap = this.data.isZap,
+ isStorePurchase = this.data.isStorePurchase,
+ )
}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionIcon.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionIcon.kt
new file mode 100644
index 000000000..523d12bab
--- /dev/null
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionIcon.kt
@@ -0,0 +1,30 @@
+package net.primal.android.wallet.transactions.list
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import net.primal.android.theme.AppTheme
+
+@Composable
+fun TransactionIcon(
+ background: Color = AppTheme.extraColorScheme.surfaceVariantAlt1,
+ content: @Composable () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .background(
+ color = background,
+ shape = CircleShape,
+ )
+ .size(48.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ content()
+ }
+}
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt
index 51fe7befa..4c3c14848 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/list/TransactionListItem.kt
@@ -2,12 +2,8 @@ package net.primal.android.wallet.transactions.list
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Surface
@@ -23,7 +19,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.NumberFormat
import java.time.Instant
@@ -203,21 +198,6 @@ private fun TransactionTrailingContent(txAmountInSats: String, txType: TxType) {
}
}
-@Composable
-private fun TransactionIcon(content: @Composable () -> Unit) {
- Box(
- modifier = Modifier
- .background(
- color = AppTheme.extraColorScheme.surfaceVariantAlt1,
- shape = CircleShape,
- )
- .size(48.dp),
- contentAlignment = Alignment.Center,
- ) {
- content()
- }
-}
-
private fun Instant.formatAsTime(): String {
val formatter = DateTimeFormatter.ofPattern("HH:mm")
return formatter.format(this.atZone(ZoneId.systemDefault()))
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/receive/ReceivePaymentScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/receive/ReceivePaymentScreen.kt
index b0d19ac28..ccff08858 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/receive/ReceivePaymentScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/receive/ReceivePaymentScreen.kt
@@ -80,7 +80,7 @@ import net.primal.android.core.compose.numericpad.PrimalNumericPad
import net.primal.android.crypto.urlToLnUrlHrp
import net.primal.android.theme.AppTheme
import net.primal.android.wallet.api.parseAsLNUrlOrNull
-import net.primal.android.wallet.dashboard.ui.AmountText
+import net.primal.android.wallet.dashboard.ui.BtcAmountText
import net.primal.android.wallet.transactions.receive.ReceivePaymentContract.UiState
import net.primal.android.wallet.utils.CurrencyConversionUtils.formatAsString
import net.primal.android.wallet.utils.CurrencyConversionUtils.toBtc
@@ -221,7 +221,7 @@ private fun ReceivePaymentViewer(
if (paymentDetails.amountInBtc != null) {
Spacer(modifier = Modifier.height(16.dp))
- AmountText(
+ BtcAmountText(
modifier = Modifier
.padding(start = 32.dp)
.height(72.dp),
@@ -372,7 +372,7 @@ private fun ReceivePaymentEditor(
) {
Spacer(modifier = Modifier.height(32.dp))
- AmountText(
+ BtcAmountText(
modifier = Modifier
.padding(start = 32.dp)
.height(72.dp),
diff --git a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionScreen.kt b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionScreen.kt
index 6bfb54de7..f77535118 100644
--- a/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionScreen.kt
+++ b/app/src/main/kotlin/net/primal/android/wallet/transactions/send/create/CreateTransactionScreen.kt
@@ -67,7 +67,7 @@ import net.primal.android.core.compose.icons.primaliconpack.WalletError
import net.primal.android.core.compose.icons.primaliconpack.WalletSuccess
import net.primal.android.core.compose.numericpad.PrimalNumericPad
import net.primal.android.theme.AppTheme
-import net.primal.android.wallet.dashboard.ui.AmountText
+import net.primal.android.wallet.dashboard.ui.BtcAmountText
import net.primal.android.wallet.numericPadContentTransformAnimation
import net.primal.android.wallet.transactions.send.create.CreateTransactionContract.UiEvent.SendTransaction
import net.primal.android.wallet.utils.CurrencyConversionUtils.toBtc
@@ -216,7 +216,7 @@ private fun TransactionEditor(
Spacer(modifier = Modifier.height(32.dp))
- AmountText(
+ BtcAmountText(
modifier = Modifier
.padding(start = 32.dp)
.height(72.dp)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 94e58296c..1893b8f46 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -391,7 +391,30 @@
Apply
Unable to add details. Please try again in few moments.
- Something
+ Zap Received
+ Zap Sent
+ Payment Received
+ Payment Sent
+ Sent To
+ Received From
+ Zapped Note
+ Date
+ Status
+ Transaction Type
+ Current USD value
+ Original USD value
+ Transaction Fee
+ Invoice
+ Transaction details
+ Nostr Zap
+ In-app Purchase
+ On-chain Payment
+ Lightning Payment
+ Draft
+ Pending
+ Paid
+ Failed
+ Canceled
Permission required
Primal needs access to your camera to scan QR codes.