A cheap attempt at a native Bluesky client for Android

NotificationsView: render mentions, more post media

+115 -36
+10 -4
app/src/main/java/industries/geesawra/monarch/NotificationsView.kt
··· 116 116 ) 117 117 118 118 119 - // is Notification.Mention -> TODO() 119 + is Notification.Mention -> SkeetView( 120 + viewModel = viewModel, 121 + skeet = SkeetData.fromPost( 122 + notification.parent, 123 + notification.mention, 124 + notification.author 125 + ), 126 + onReplyTap = onReplyTap, 127 + ) 128 + 120 129 is Notification.Quote -> SkeetView( 121 130 viewModel = viewModel, 122 131 skeet = SkeetData.fromPost( ··· 145 154 ), 146 155 nested = true 147 156 ) 148 - 149 - else -> {} 150 - 151 157 } 152 158 }
+104 -31
app/src/main/java/industries/geesawra/monarch/datalayer/Models.kt
··· 4 4 5 5 import app.bsky.actor.ProfileView 6 6 import app.bsky.actor.ProfileViewBasic 7 + import app.bsky.embed.ExternalView 8 + import app.bsky.embed.ExternalViewExternal 9 + import app.bsky.embed.ImagesView 10 + import app.bsky.embed.ImagesViewImage 7 11 import app.bsky.embed.RecordViewRecord 8 12 import app.bsky.embed.RecordViewRecordEmbedUnion 13 + import app.bsky.embed.VideoView 9 14 import app.bsky.feed.FeedViewPost 10 15 import app.bsky.feed.FeedViewPostReasonUnion 11 16 import app.bsky.feed.Post 17 + import app.bsky.feed.PostEmbedUnion 12 18 import app.bsky.feed.PostReplyRef 13 19 import app.bsky.feed.PostView 14 20 import app.bsky.feed.PostViewEmbedUnion ··· 20 26 import kotlinx.datetime.toStdlibInstant 21 27 import sh.christian.ozone.api.AtUri 22 28 import sh.christian.ozone.api.Cid 29 + import sh.christian.ozone.api.Did 23 30 import sh.christian.ozone.api.Handle 31 + import sh.christian.ozone.api.Uri 32 + import sh.christian.ozone.api.model.Blob 24 33 import kotlin.time.ExperimentalTime 25 34 import kotlin.time.Instant 35 + 36 + enum class CDNImageSize( 37 + val size: String 38 + ) { 39 + Full("feed_fullsize"), 40 + Thumb("feed_thumbnail") 41 + } 42 + 43 + private fun cdnBlobURL(authorDid: Did, blob: Blob?, size: CDNImageSize): Uri? { 44 + val id = when (blob) { 45 + is Blob.LegacyBlob -> blob.cid 46 + is Blob.StandardBlob -> blob.ref.link 47 + null -> return null 48 + } 49 + 50 + return Uri("https://cdn.bsky.app/img/${size.size}/plain/${authorDid.did}/${id}@jpeg") 51 + } 52 + 53 + private fun cdnVideoThumb(authorDid: Did, blob: Blob?): Uri? { 54 + val id = when (blob) { 55 + is Blob.LegacyBlob -> blob.cid 56 + is Blob.StandardBlob -> blob.ref.link 57 + null -> return null 58 + } 59 + return Uri("https://video.cdn.bsky.app/hls/${authorDid.did}/${id}/thumbnail.jpg") 60 + } 61 + 62 + private fun cdnVideoPlaylist(authorDid: Did, blob: Blob?): Uri? { 63 + val id = when (blob) { 64 + is Blob.LegacyBlob -> blob.cid 65 + is Blob.StandardBlob -> blob.ref.link 66 + null -> return null 67 + } 68 + return Uri("https://video.cdn.bsky.app/hls/${authorDid.did}/${id}/playlist.m3u8") 69 + } 26 70 27 71 data class SkeetData( 28 72 val likes: Long? = null, ··· 130 174 authorHandle = author.handle, 131 175 authorLabels = author.labels, 132 176 content = post.text, 133 - // embed = when (post.embed) { 134 - // is PostEmbedUnion.External -> PostViewEmbedUnion.ExternalView( 135 - // ExternalView( 136 - // ExternalViewExternal( 137 - // uri = (post.embed as PostEmbedUnion.External).value.external.uri, 138 - // title = (post.embed as PostEmbedUnion.External).value.external.title, 139 - // description = (post.embed as PostEmbedUnion.External).value.external.description, 140 - //// thumb = (post.embed as PostEmbedUnion.External).value.external.thumb.toUri(), // TODO fix this 141 - // ) 142 - // ) 143 - // ) 144 - // 145 - // is PostEmbedUnion.Images -> PostViewEmbedUnion.ImagesView( 146 - // ImagesView((post.embed as PostEmbedUnion.Images).value.images.map { 147 - // ImagesViewImage( 148 - // fullsize = it.image, 149 - // alt = TODO(), 150 - // aspectRatio = TODO(), 151 - // ) 152 - // }) 153 - // ) 154 - // 177 + embed = when (post.embed) { 178 + is PostEmbedUnion.External -> { 179 + val c = (post.embed as PostEmbedUnion.External) 180 + PostViewEmbedUnion.ExternalView( 181 + ExternalView( 182 + ExternalViewExternal( 183 + uri = c.value.external.uri, 184 + title = c.value.external.title, 185 + description = c.value.external.description, 186 + thumb = cdnBlobURL( 187 + author.did, 188 + c.value.external.thumb, 189 + CDNImageSize.Thumb 190 + ) 191 + ) 192 + ) 193 + ) 194 + } 195 + 196 + is PostEmbedUnion.Images -> { 197 + val c = (post.embed as PostEmbedUnion.Images) 198 + PostViewEmbedUnion.ImagesView( 199 + ImagesView(c.value.images.map { 200 + ImagesViewImage( 201 + fullsize = cdnBlobURL( 202 + author.did, 203 + it.image, 204 + CDNImageSize.Full 205 + )!!, 206 + thumb = cdnBlobURL( 207 + author.did, 208 + it.image, 209 + CDNImageSize.Thumb 210 + )!!, 211 + alt = it.alt, 212 + aspectRatio = it.aspectRatio, 213 + ) 214 + }) 215 + ) 216 + } 217 + 155 218 156 219 // is PostEmbedUnion.Record -> PostViewEmbedUnion.RecordView( 157 220 // RecordView(post.embed.value.record) ··· 165 228 // ) 166 229 // 167 230 // is PostEmbedUnion.Unknown -> PostViewEmbedUnion.Unknown(post.embed.value) 168 - // is PostEmbedUnion.Video -> PostViewEmbedUnion.VideoView( 169 - // VideoView( 170 - // cid = post.embed.value.cid, thumb = post.embed.value.thumb 171 - // ) 172 - // ) 173 - // 174 - // null -> null 175 - // }, 231 + is PostEmbedUnion.Video -> { 232 + val c = (post.embed as PostEmbedUnion.Video).value 233 + PostViewEmbedUnion.VideoView( 234 + VideoView( 235 + playlist = cdnVideoPlaylist(author.did, c.video)!!, 236 + thumbnail = cdnVideoThumb(author.did, c.video), 237 + alt = c.alt, 238 + aspectRatio = c.aspectRatio, 239 + cid = parent.first 240 + ) 241 + ) 242 + } 243 + 244 + null -> null 245 + else -> null 246 + }, 176 247 // TODO: fix embeds 177 248 createdAt = post.createdAt.toStdlibInstant() 178 249 ) ··· 313 384 Notification() 314 385 315 386 data class Follow(val follow: ProfileView) : Notification() 316 - data class Mention(val mention: Post, val author: ProfileView) : Notification() 387 + data class Mention(val parent: Pair<Cid, AtUri>, val mention: Post, val author: ProfileView) : 388 + Notification() 389 + 317 390 data class Quote(val parent: Pair<Cid, AtUri>, val quote: Post, val author: ProfileView) : 318 391 Notification() 319 392 }
+1 -1
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 203 203 204 204 ListNotificationsReason.Mention -> { 205 205 val p: app.bsky.feed.Post = it.record.decodeAs() 206 - Notification.Mention(p, it.author) 206 + Notification.Mention(Pair(it.cid, it.uri), p, it.author) 207 207 } 208 208 209 209 ListNotificationsReason.Quote -> {