Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.tunjid.heron.data.repository

import app.bsky.embed.Record as BskyRecord
import chat.bsky.convo.AddReactionRequest
import chat.bsky.convo.AddReactionResponse
import chat.bsky.convo.DeletedMessageView
Expand All @@ -38,7 +37,6 @@ import chat.bsky.convo.MessageView
import chat.bsky.convo.RemoveReactionRequest
import chat.bsky.convo.RemoveReactionResponse
import chat.bsky.convo.SendMessageRequest
import com.atproto.repo.StrongRef
import com.tunjid.heron.data.core.models.Conversation
import com.tunjid.heron.data.core.models.Cursor
import com.tunjid.heron.data.core.models.CursorList
Expand All @@ -63,6 +61,7 @@ import com.tunjid.heron.data.utilities.nextCursorFlow
import com.tunjid.heron.data.utilities.profileLookup.ProfileLookup
import com.tunjid.heron.data.utilities.recordResolver.RecordResolver
import com.tunjid.heron.data.utilities.toOutcome
import com.tunjid.heron.data.utilities.toStrongReferencedRecord
import dev.zacsweers.metro.Inject
import kotlin.time.Clock
import kotlin.time.Duration.Companion.seconds
Expand All @@ -76,8 +75,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
import kotlinx.serialization.Serializable
import sh.christian.ozone.api.AtUri
import sh.christian.ozone.api.Cid
import sh.christian.ozone.api.Did

@Serializable
Expand Down Expand Up @@ -361,16 +358,9 @@ internal class OfflineMessageRepository @Inject constructor(
message = MessageInput(
text = message.text,
facets = resolvedLinks.facet(),
embed = message.recordReference?.let { ref ->
MessageInputEmbedUnion.AppBskyEmbedRecord(
value = BskyRecord(
record = StrongRef(
uri = ref.uri.uri.let(::AtUri),
cid = ref.id.id.let(::Cid),
),
),
)
},
embed = message.recordReference
?.toStrongReferencedRecord()
?.let(MessageInputEmbedUnion::AppBskyEmbedRecord),
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.tunjid.heron.data.core.models.Follow
import com.tunjid.heron.data.core.models.Labeler
import com.tunjid.heron.data.core.models.Like
import com.tunjid.heron.data.core.models.LinkTarget
import com.tunjid.heron.data.core.models.ListMember
import com.tunjid.heron.data.core.models.Notification
import com.tunjid.heron.data.core.models.NotificationPreferences
import com.tunjid.heron.data.core.models.Post
Expand Down Expand Up @@ -459,6 +460,7 @@ internal class OfflineNotificationsRepository @Inject constructor(
is FeedGenerator,
is FeedList,
is Labeler,
is ListMember,
is StarterPack,
is Block,
-> throw UnknownNotificationException(query.recordUri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import app.bsky.graph.GetListQueryParams
import app.bsky.graph.GetListResponse
import app.bsky.graph.GetListsQueryParams
import app.bsky.graph.GetListsResponse
import app.bsky.graph.Listitem as BskyListMember
import com.atproto.repo.CreateRecordRequest
import com.atproto.repo.DeleteRecordRequest
import com.atproto.repo.GetRecordQueryParams
Expand All @@ -46,17 +47,22 @@ import com.tunjid.heron.data.core.models.value
import com.tunjid.heron.data.core.types.EmbeddableRecordUri
import com.tunjid.heron.data.core.types.FeedGeneratorUri
import com.tunjid.heron.data.core.types.LabelerUri
import com.tunjid.heron.data.core.types.ListMemberUri
import com.tunjid.heron.data.core.types.ListUri
import com.tunjid.heron.data.core.types.PostUri
import com.tunjid.heron.data.core.types.ProfileId
import com.tunjid.heron.data.core.types.RecordKey
import com.tunjid.heron.data.core.types.RecordUri
import com.tunjid.heron.data.core.types.StarterPackUri
import com.tunjid.heron.data.core.types.profileId
import com.tunjid.heron.data.core.types.recordUriOrNull
import com.tunjid.heron.data.core.utilities.Outcome
import com.tunjid.heron.data.database.daos.FeedGeneratorDao
import com.tunjid.heron.data.database.daos.LabelDao
import com.tunjid.heron.data.database.daos.ListDao
import com.tunjid.heron.data.database.daos.PostDao
import com.tunjid.heron.data.database.daos.StarterPackDao
import com.tunjid.heron.data.database.entities.ListMemberEntity
import com.tunjid.heron.data.database.entities.PopulatedFeedGeneratorEntity
import com.tunjid.heron.data.database.entities.PopulatedListEntity
import com.tunjid.heron.data.database.entities.PopulatedListMemberEntity
Expand All @@ -76,6 +82,7 @@ import com.tunjid.heron.data.utilities.multipleEntitysaver.add
import com.tunjid.heron.data.utilities.nextCursorFlow
import com.tunjid.heron.data.utilities.profileLookup.ProfileLookup
import com.tunjid.heron.data.utilities.recordResolver.RecordResolver
import com.tunjid.heron.data.utilities.toOutcome
import com.tunjid.heron.data.utilities.withRefresh
import dev.zacsweers.metro.Inject
import kotlin.time.Clock
Expand Down Expand Up @@ -132,6 +139,10 @@ interface RecordRepository {
update: GrazeFeed.Update,
): Result<GrazeFeed>

suspend fun addListMember(
create: ListMember.Create,
): Outcome

suspend fun deleteRecord(
uri: RecordUri,
): Outcome
Expand Down Expand Up @@ -437,6 +448,50 @@ internal class OfflineRecordRepository @Inject constructor(
}
} ?: expiredSessionResult()

override suspend fun addListMember(
create: ListMember.Create,
): Outcome = savedStateDataSource.inCurrentProfileSession { signedInProfileId ->
if (signedInProfileId == null) return@inCurrentProfileSession expiredSessionOutcome()

val createdAt = Clock.System.now()
val recordKey = RecordKey.generate()
val listOwnerId = create.listUri.profileId()

networkService.runCatchingWithMonitoredNetworkRetry {
createRecord(
CreateRecordRequest(
repo = Did(listOwnerId.id),
collection = Nsid(ListMemberUri.NAMESPACE),
rkey = RKey(recordKey.value),
record = BskyListMember(
subject = Did(create.subjectId.id),
list = AtUri(create.listUri.uri),
createdAt = createdAt,
).asJsonContent(BskyListMember.serializer()),
),
)
}.mapCatchingUnlessCancelled {
val listMemberUri = requireNotNull(
recordUriOrNull(
profileId = listOwnerId,
namespace = ListMemberUri.NAMESPACE,
recordKey = recordKey,
),
) as ListMemberUri
multipleEntitySaverProvider.saveInTransaction {
add(
ListMemberEntity(
uri = listMemberUri,
listUri = create.listUri,
subjectId = create.subjectId,
createdAt = createdAt,
),
)
}
}
.toOutcome()
} ?: expiredSessionOutcome()

override suspend fun deleteRecord(
uri: RecordUri,
): Outcome = savedStateDataSource.inCurrentProfileSession { signedInProfileId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ internal fun postEmbedUnion(
embeddedRecordReference: Record.Reference?,
mediaBlobs: List<MediaBlob>,
): PostEmbedUnion? {
val record = embeddedRecordReference?.toRecord()
val record = embeddedRecordReference?.toStrongReferencedRecord()
val video = mediaBlobs.video()
val images = mediaBlobs.images()

Expand Down Expand Up @@ -124,13 +124,16 @@ internal fun List<Link>.facet(): List<Facet> = map { link ->
)
}

private fun Record.Reference.toRecord(): BskyRecord =
BskyRecord(
record = StrongRef(
uri = AtUri(uri.uri),
cid = Cid(id.id),
),
)
internal fun Record.Reference.toStrongReferencedRecord(): BskyRecord? =
when (val id = id) {
null -> null
else -> BskyRecord(
record = StrongRef(
uri = AtUri(uri.uri),
cid = Cid(id.id),
),
)
}

private fun List<MediaBlob>.video(): BskyVideo? =
filterIsInstance<MediaBlob.Video>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import app.bsky.feed.Repost as BskyRepost
import app.bsky.graph.Block as BskyBlock
import app.bsky.graph.GetListQueryParams
import app.bsky.graph.GetStarterPackQueryParams
import app.bsky.graph.Listitem as BskyListMember
import app.bsky.labeler.GetServicesQueryParams
import app.bsky.labeler.GetServicesResponseViewUnion
import com.atproto.repo.DeleteRecordRequest
Expand All @@ -37,6 +38,7 @@ import com.tunjid.heron.data.core.models.Label
import com.tunjid.heron.data.core.models.Labeler
import com.tunjid.heron.data.core.models.LabelerPreference
import com.tunjid.heron.data.core.models.Like
import com.tunjid.heron.data.core.models.ListMember
import com.tunjid.heron.data.core.models.Post
import com.tunjid.heron.data.core.models.Profile
import com.tunjid.heron.data.core.models.Record
Expand All @@ -53,6 +55,7 @@ import com.tunjid.heron.data.core.types.FollowUri
import com.tunjid.heron.data.core.types.GenericId
import com.tunjid.heron.data.core.types.LabelerUri
import com.tunjid.heron.data.core.types.LikeUri
import com.tunjid.heron.data.core.types.ListMemberUri
import com.tunjid.heron.data.core.types.ListUri
import com.tunjid.heron.data.core.types.PostUri
import com.tunjid.heron.data.core.types.ProfileId
Expand Down Expand Up @@ -88,6 +91,7 @@ import com.tunjid.heron.data.network.NetworkService
import com.tunjid.heron.data.network.currentSessionContext
import com.tunjid.heron.data.network.models.asExternalModel
import com.tunjid.heron.data.network.models.post
import com.tunjid.heron.data.network.models.profileEntity
import com.tunjid.heron.data.repository.SavedStateDataSource
import com.tunjid.heron.data.repository.distinctUntilChangedSignedProfilePreferencesOrDefault
import com.tunjid.heron.data.repository.singleSessionFlow
Expand Down Expand Up @@ -330,6 +334,28 @@ internal class OfflineRecordResolver @Inject constructor(
it.list.asExternalModel()
}

is ListMemberUri -> fetchRecordAndSaveCreator(
recordUri = uri,
viewingProfileId = viewingProfileId,
).mapToResult { recordResponse ->
val bskyListMember = recordResponse.value.decodeAs<BskyListMember>()
networkService.runCatchingWithMonitoredNetworkRetry {
getProfile(
GetProfileQueryParams(
actor = bskyListMember.subject,
),
)
}.mapCatchingUnlessCancelled { profileResponse ->
ListMember(
uri = uri,
subject = profileResponse.profileEntity().asExternalModel(),
listUri = bskyListMember.list.atUri.let(::ListUri),
createdAt = bskyListMember.createdAt,
viewerState = null,
)
}
}

is PostUri -> networkService.runCatchingWithMonitoredNetworkRetry(times = 2) {
getPosts(
GetPostsQueryParams(
Expand Down Expand Up @@ -473,6 +499,7 @@ internal class OfflineRecordResolver @Inject constructor(
is FeedGeneratorUri -> feedGeneratorDao.deleteFeedGenerator(uri)
is LabelerUri -> labelDao.deleteLabeler(uri)
is ListUri -> listDao.deleteList(uri)
is ListMemberUri -> listDao.deleteListMember(uri)
is PostUri -> postDao.deletePost(uri)
is StarterPackUri -> starterPackDao.deleteStarterPack(uri)
is FollowUri -> profileDao.deleteFollow(uri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.tunjid.heron.data.utilities.writequeue

import com.tunjid.heron.data.core.models.ListMember
import com.tunjid.heron.data.core.models.Message
import com.tunjid.heron.data.core.models.NotificationPreferences
import com.tunjid.heron.data.core.models.Post
Expand Down Expand Up @@ -112,6 +113,21 @@ sealed interface Writable {
profileRepository.sendConnection(connection)
}

@Serializable
sealed interface FeedList {
@Serializable
data class AddMember(
val create: ListMember.Create,
) : FeedList,
Writable {
override val queueId: String
get() = "add-list-member-$this"

override suspend fun WriteQueue.write(): Outcome =
recordRepository.addListMember(create)
}
}

@Serializable
data class Restriction(
val restriction: Profile.Restriction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ private fun Writable.writeTimeout() =
is Writable.Send,
is Writable.TimelineUpdate,
is Writable.RecordDeletion,
is Writable.FeedList,
-> BasicWriteTimeout
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import com.tunjid.heron.data.core.types.ListMemberUri
import com.tunjid.heron.data.core.types.ListUri
import com.tunjid.heron.data.database.entities.ListEntity
import com.tunjid.heron.data.database.entities.ListMemberEntity
Expand Down Expand Up @@ -134,4 +135,14 @@ interface ListDao {
suspend fun deleteList(
uri: ListUri,
)

@Query(
"""
DELETE FROM listMembers
WHERE uri = :uri
""",
)
suspend fun deleteListMember(
uri: ListMemberUri,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.tunjid.heron.data.core.models

import com.tunjid.heron.data.core.types.ListMemberUri
import com.tunjid.heron.data.core.types.ListUri
import com.tunjid.heron.data.core.types.ProfileId
import kotlin.time.Instant
import kotlinx.serialization.Serializable

Expand All @@ -28,4 +29,17 @@ data class ListMember(
val listUri: ListUri,
val createdAt: Instant,
val viewerState: ProfileViewerState?,
)
) : Record {

override val reference: Record.Reference
get() = Record.Reference(
id = null,
uri = uri,
)

@Serializable
data class Create(
val subjectId: ProfileId,
val listUri: ListUri,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ sealed interface Record {

@Serializable
data class Reference(
val id: Id,
val id: Id?,
val uri: RecordUri,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ fun RecordUri.requireCollection(): String =
is FeedGeneratorUri -> FeedGeneratorUri.NAMESPACE
is LabelerUri -> LabelerUri.NAMESPACE
is ListUri -> ListUri.NAMESPACE
is ListMemberUri -> ListMemberUri.NAMESPACE
is PostUri -> PostUri.NAMESPACE
is StarterPackUri -> StarterPackUri.NAMESPACE
is FollowUri -> FollowUri.NAMESPACE
Expand Down Expand Up @@ -224,8 +225,13 @@ value class UnknownRecordUri(
@JvmInline
value class ListMemberUri(
override val uri: String,
) : Uri {
) : Uri,
RecordUri {
override fun toString(): String = uri

companion object {
const val NAMESPACE = "app.bsky.graph.listitem"
}
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
<string name="writable_unlike">unlike</string>
<string name="writable_repost">repost</string>
<string name="writable_repost_removal">repost removal</string>
<string name="writable_add_list_member">list member addition</string>
<string name="writable_thread_gate_update">post reply update</string>
<string name="empty_timeline_generic">This feed is empty</string>
<string name="empty_timeline_generic_description">When you follow people, their posts will show up here.</string>
Expand Down
Loading