Skip to content

Commit c8c1df5

Browse files
authored
Merge pull request #743 from tunjid/bugfix/1.1.3
Bugfix/1.1.3
2 parents 8d9e480 + cdd0da1 commit c8c1df5

File tree

41 files changed

+621
-571
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+621
-571
lines changed

.github/workflows/publish.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ name: Publish
22

33
on:
44
workflow_dispatch:
5-
push:
6-
tags:
7-
- '*'
85

96
jobs:
107
publish-android-app:

data/core/src/commonMain/kotlin/com/tunjid/heron/data/network/VideoUploadService.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,23 @@ internal class SuspendingVideoUploadService @Inject constructor(
109109
),
110110
)
111111
}.mapCatchingUnlessCancelled { tokenResponse ->
112-
videoUploadClient.post(UploadVideoEndpoint) {
113-
url {
114-
parameters.append(
115-
name = DidQueryParam,
116-
value = authToken.authProfileId.id,
117-
)
118-
parameters.append(
119-
name = NameQueryParam,
120-
value = Clock.System.now().toEpochMilliseconds().toString(),
121-
)
112+
fileManager.source(file).use { source ->
113+
videoUploadClient.post(UploadVideoEndpoint) {
114+
url {
115+
parameters.append(
116+
name = DidQueryParam,
117+
value = authToken.authProfileId.id,
118+
)
119+
parameters.append(
120+
name = NameQueryParam,
121+
value = Clock.System.now().toEpochMilliseconds().toString(),
122+
)
123+
}
124+
bearerAuth(tokenResponse.token)
125+
contentType(ContentType.Video.MP4)
126+
setBody(ByteReadChannel(source))
127+
headers[ContentLengthHeaderKey] = fileManager.size(file).toString()
122128
}
123-
bearerAuth(tokenResponse.token)
124-
contentType(ContentType.Video.MP4)
125-
setBody(ByteReadChannel(fileManager.source(file)))
126-
headers[ContentLengthHeaderKey] = fileManager.size(file).toString()
127129
}
128130
.let {
129131
if (it.status.isSuccess()) it.body<VideoUploadResponse>()

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/NotificationsRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ import sh.christian.ozone.api.response.AtpResponse
5454
@Serializable
5555
data class NotificationsQuery(
5656
override val data: CursorQuery.Data,
57-
) : CursorQuery
57+
) : CursorQuery {
58+
init {
59+
require(data.limit < 20) {
60+
"Notification query limit must be less than 20 items"
61+
}
62+
}
63+
}
5864

5965
interface NotificationsRepository {
6066
val unreadCount: Flow<Long>

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/PostRepository.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import com.tunjid.heron.data.utilities.toOutcome
9292
import com.tunjid.heron.data.utilities.with
9393
import com.tunjid.heron.data.utilities.withRefresh
9494
import dev.zacsweers.metro.Inject
95+
import io.ktor.utils.io.ByteReadChannel
9596
import kotlin.time.Clock
9697
import kotlin.time.Duration.Companion.seconds
9798
import kotlinx.coroutines.async
@@ -107,6 +108,7 @@ import kotlinx.coroutines.flow.flatMapLatest
107108
import kotlinx.coroutines.flow.flow
108109
import kotlinx.coroutines.flow.map
109110
import kotlinx.coroutines.flow.mapNotNull
111+
import kotlinx.io.Source
110112
import kotlinx.serialization.Serializable
111113
import sh.christian.ozone.api.AtUri
112114
import sh.christian.ozone.api.Cid
@@ -441,9 +443,9 @@ internal class OfflinePostRepository @Inject constructor(
441443
request.metadata.embeddedMedia.map { file ->
442444
async {
443445
when (file) {
444-
is File.Media.Photo -> networkService.uploadImageBlob(
445-
data = fileManager.readBytes(file),
446-
)
446+
is File.Media.Photo -> fileManager.source(file).use {
447+
networkService.uploadImageBlob(data = it)
448+
}
447449
is File.Media.Video -> videoUploadService.uploadVideo(
448450
file = file,
449451
)
@@ -759,9 +761,9 @@ private fun CreateRecordResponse.successWithUri(): Pair<Boolean, String> =
759761
Pair(validationStatus is CreateRecordValidationStatus.Valid, uri.atUri)
760762

761763
private suspend fun NetworkService.uploadImageBlob(
762-
data: ByteArray,
764+
data: Source,
763765
): Result<Blob> = runCatchingWithMonitoredNetworkRetry {
764-
api.uploadBlob(data)
766+
api.uploadBlob(ByteReadChannel(data))
765767
.map(UploadBlobResponse::blob)
766768
}
767769

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/ProfileRepository.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import com.tunjid.heron.data.utilities.toOutcome
8181
import com.tunjid.heron.data.utilities.toProfileWithViewerStates
8282
import com.tunjid.heron.data.utilities.withRefresh
8383
import dev.zacsweers.metro.Inject
84+
import io.ktor.utils.io.ByteReadChannel
8485
import kotlin.time.Clock
8586
import kotlinx.coroutines.async
8687
import kotlinx.coroutines.awaitAll
@@ -614,8 +615,9 @@ internal class OfflineProfileRepository @Inject constructor(
614615
async {
615616
if (file == null) return@async null
616617
networkService.runCatchingWithMonitoredNetworkRetry {
617-
val bytes = fileManager.readBytes(file)
618-
uploadBlob(bytes)
618+
fileManager.source(file).use { source ->
619+
uploadBlob(ByteReadChannel(source))
620+
}
619621
}
620622
.onSuccess { fileManager.delete(file) }
621623
.getOrNull()
@@ -629,7 +631,7 @@ internal class OfflineProfileRepository @Inject constructor(
629631
async {
630632
if (file == null) null
631633
else networkService.runCatchingWithMonitoredNetworkRetry {
632-
uploadBlob(file.data)
634+
uploadBlob(ByteReadChannel(file.data))
633635
}
634636
.getOrNull()
635637
?.blob

data/core/src/commonMain/kotlin/com/tunjid/heron/data/repository/TimelineRepository.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,8 @@ internal class OfflineTimelineRepository(
11621162
else Unit
11631163

11641164
// New reply to the OP, start its own thread
1165-
lastItem is TimelineItem.Thread && lastItem.posts.first().uri != thread.rootPostUri -> list += TimelineItem.Thread(
1165+
lastItem is TimelineItem.Thread &&
1166+
lastItem.posts.first().uri != thread.rootPostUri -> list += TimelineItem.Thread(
11661167
id = thread.entity.uri.uri,
11671168
generation = thread.generation,
11681169
anchorPostIndex = 0,
@@ -1172,17 +1173,23 @@ internal class OfflineTimelineRepository(
11721173
signedInProfileId = signedInProfileId,
11731174
postUrisToThreadGates = mapOf(post.uri to threadGate(post.uri)),
11741175
)
1175-
11761176
// Just tack the post to the current thread
1177-
lastItem is TimelineItem.Thread -> list[list.lastIndex] = lastItem.copy(
1178-
posts = lastItem.posts + post,
1179-
postUrisToThreadGates = lastItem.postUrisToThreadGates + (post.uri to threadGate(post.uri)),
1180-
)
1177+
lastItem is TimelineItem.Thread ->
1178+
// Make sure only consecutive generations are added to the thread.
1179+
// Nonconsecutive generations are dropped. Users can see these replies by
1180+
// diving into the thread.
1181+
if (lastItem.nextGeneration == thread.generation) list[list.lastIndex] = lastItem.copy(
1182+
posts = lastItem.posts + post,
1183+
postUrisToThreadGates = lastItem.postUrisToThreadGates + (post.uri to threadGate(post.uri)),
1184+
)
11811185
else -> Unit
11821186
}
11831187
}
11841188
}
11851189

1190+
private val TimelineItem.Thread.nextGeneration
1191+
get() = generation?.let { it + posts.size }
1192+
11861193
private fun TimelinePreferencesEntity?.preferredPresentation(): Timeline.Presentation =
11871194
when (this?.preferredPresentation) {
11881195
Timeline.Presentation.Media.Expanded.key -> Timeline.Presentation.Media.Expanded

data/database/src/commonMain/kotlin/com/tunjid/heron/data/database/daos/PostDao.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,13 @@ interface PostDao {
318318
WITH RECURSIVE
319319
-- 1. ParentHierarchy CTE: Recursively finds all parent URIs for the given post
320320
-- and calculates their generation (negative).
321-
ParentHierarchy(uri, rootPostUri, generation, sort) AS (
321+
ParentHierarchy(uri, rootPostUri, generation, ancestorCreated, postCreated) AS (
322322
SELECT
323323
pt.parentPostUri, -- each parent is its own root
324324
pt.postUri AS rootPostUri,
325325
-1 AS generation,
326-
-1 AS sort -- sort parents of the OP strictly by their generation
326+
-1 AS ancestorCreated, -- Always a negative constant for ancestors
327+
-1 AS postCreated -- Always a negative constant for ancestors
327328
FROM
328329
postThreads pt
329330
WHERE postUri = :postUri
@@ -332,7 +333,8 @@ interface PostDao {
332333
pt.parentPostUri,
333334
ph.rootPostUri,
334335
ph.generation - 1,
335-
ph.sort -1
336+
ph.ancestorCreated - 1,
337+
ph.postCreated - 1
336338
FROM
337339
postThreads pt
338340
INNER JOIN
@@ -341,12 +343,13 @@ interface PostDao {
341343
342344
-- 2. ChildHierarchy CTE: Recursively finds all child URIs for the given post
343345
-- and calculates their generation (positive).
344-
ChildHierarchy(uri, rootPostUri, generation, sort) AS (
346+
ChildHierarchy(uri, rootPostUri, generation, ancestorCreated, postCreated) AS (
345347
SELECT
346348
pt.postUri,
347349
pt.postUri AS rootPostUri, -- add the very first reply to the OP as the root
348350
1 AS generation,
349-
p.createdAt as sort -- sort all replies by the very first reply to the OP
351+
p.createdAt as ancestorCreated, -- sort all replies by the very first reply to the OP
352+
p.createdAt as postCreated -- second sort is the actual post's createdAt
350353
FROM
351354
postThreads pt
352355
JOIN posts p ON pt.postUri = p.uri
@@ -357,21 +360,23 @@ interface PostDao {
357360
pt.postUri,
358361
ch.rootPostUri,
359362
ch.generation + 1,
360-
ch.sort
363+
ch.ancestorCreated, -- preserve the ancestorCreatedDate in the thread
364+
p.createdAt -- pull in the actual post's createdAt date
361365
FROM
362366
postThreads pt
367+
INNER JOIN posts p ON pt.postUri = p.uri
363368
INNER JOIN
364369
ChildHierarchy ch ON pt.parentPostUri = ch.uri
365370
),
366371
367372
-- 3. FullThread CTE: Combines the URIs and generations from parents, children,
368373
-- and the post itself (generation 0).
369-
FullThread(uri, rootPostUri, generation, sort) AS (
370-
SELECT uri, rootPostUri, generation, sort FROM ParentHierarchy
374+
FullThread(uri, rootPostUri, generation, ancestorCreated, postCreated) AS (
375+
SELECT uri, rootPostUri, generation, ancestorCreated, postCreated FROM ParentHierarchy
371376
UNION
372-
SELECT uri, rootPostUri, generation, sort FROM ChildHierarchy
377+
SELECT uri, rootPostUri, generation, ancestorCreated, postCreated FROM ChildHierarchy
373378
UNION
374-
SELECT :postUri, NULL, 0, 0
379+
SELECT :postUri, NULL, 0, 0, 0
375380
)
376381
377382
-- 4. Final SELECT: Fetches all columns from the `posts` table for every URI
@@ -380,13 +385,14 @@ interface PostDao {
380385
p.*,
381386
ft.rootPostUri AS rootPostUri,
382387
ft.generation AS generation,
383-
ft.sort AS sort
388+
ft.ancestorCreated AS ancestorCreated,
389+
ft.postCreated AS postCreated
384390
FROM
385391
posts p
386392
JOIN
387393
FullThread ft ON p.uri = ft.uri
388394
ORDER BY
389-
ft.sort, ft.generation; -- sort by the first reply to the op, then the generation
395+
ft.ancestorCreated, ft.generation, ft.postCreated; -- sort by the first reply to the op, then the generation, then the post itself
390396
""",
391397
)
392398
fun postThread(

data/files/src/commonMain/kotlin/com/tunjid/heron/data/files/FileKitFileManager.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,10 @@ import io.github.vinceglb.filekit.extension
2929
import io.github.vinceglb.filekit.name
3030
import io.github.vinceglb.filekit.nameWithoutExtension
3131
import io.github.vinceglb.filekit.path
32-
import io.github.vinceglb.filekit.readBytes
3332
import io.github.vinceglb.filekit.size
3433
import io.github.vinceglb.filekit.source
3534
import io.github.vinceglb.filekit.write
3635
import kotlin.time.Clock
37-
import kotlinx.io.IOException
3836
import kotlinx.io.Source
3937
import kotlinx.io.buffered
4038

@@ -87,14 +85,6 @@ internal class FileKitFileManager : FileManager {
8785
}
8886
}
8987

90-
override suspend fun readBytes(
91-
file: File,
92-
): ByteArray {
93-
val cachedFile = file.toPlatformFile()
94-
return if (cachedFile.exists()) cachedFile.readBytes()
95-
else throw IOException("File does not exist")
96-
}
97-
9888
override suspend fun source(
9989
file: File,
10090
): Source =

data/files/src/commonMain/kotlin/com/tunjid/heron/data/files/FileManager.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ interface FileManager {
2424
restrictedFile: RestrictedFile,
2525
): File?
2626

27-
suspend fun readBytes(
28-
file: File,
29-
): ByteArray
30-
3127
suspend fun source(
3228
file: File,
3329
): Source

data/lexicons/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sh.christian.ozone.api.generator.ApiReturnType
2+
import sh.christian.ozone.api.generator.BinaryDataType
23

34
plugins {
45
id("android-library-convention")
@@ -49,6 +50,12 @@ lexicons {
4950
defaults {
5051
generateUnknownsForSealedTypes.set(true)
5152
generateUnknownsForEnums.set(true)
53+
binaryDataType.set(
54+
BinaryDataType.Custom(
55+
packageName = "io.ktor.utils.io",
56+
simpleNames = listOf("ByteReadChannel"),
57+
),
58+
)
5259
}
5360

5461
generateApi("BlueskyApi") {

0 commit comments

Comments
 (0)