Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
ef359e0
Reverese logo leg rotation angle
tunjid Jan 31, 2026
3b59d2d
Merge pull request #918 from tunjid/tj/logo-finesse
tunjid Jan 31, 2026
8131edd
Add Timeline.Source
tunjid Jan 31, 2026
19c7474
Pull out Timeline.Source.id
tunjid Jan 31, 2026
dc1ce66
Add Timeline.Source as the identifier for timelines
tunjid Jan 31, 2026
1abc380
Fix build
tunjid Jan 31, 2026
c72973d
Fix bild
tunjid Jan 31, 2026
55941b8
Merge pull request #919 from tunjid/tj/timeline-source
tunjid Jan 31, 2026
41d51de
Update TimelineQuery to have a reference to the source, not the Timel…
tunjid Jan 31, 2026
655ae9a
Update usages of TimelineQuery
tunjid Jan 31, 2026
2fe2662
PR feedback
tunjid Jan 31, 2026
e31f66a
Merge pull request #920 from tunjid/tj/timeline-query-update
tunjid Jan 31, 2026
4d98fc2
Make Timeline.Source serializable
tunjid Jan 31, 2026
88a426f
Make Timeline.Source a UrlEncodableModel
tunjid Jan 31, 2026
a1d4d9b
lint
tunjid Jan 31, 2026
21f8a9f
Remove serialization annotation on enum
tunjid Jan 31, 2026
cc0ce31
Merge pull request #921 from tunjid/tj/serializable-timeline-source
tunjid Jan 31, 2026
f80f461
Pass timeline source to gallery
tunjid Jan 31, 2026
eca9ef4
Pass timeline source along to record destinations
tunjid Jan 31, 2026
17492ab
Pass along timeline source in post detail
tunjid Jan 31, 2026
ed8253f
Merge pull request #922 from tunjid/tj/route-timeline-source
tunjid Jan 31, 2026
ac0db43
Make sure nessages with records that have no embeds are saved
tunjid Feb 1, 2026
03f6346
Clean up code
tunjid Feb 1, 2026
db616f7
Merge pull request #924 from tunjid/tj/message-embed
tunjid Feb 1, 2026
b15cd2e
Update padding for post composer
tunjid Feb 1, 2026
3c3b735
Merge pull request #925 from tunjid/tj/compose-post-padding
tunjid Feb 1, 2026
e13df8d
Vertical gallery WIP
tunjid Feb 1, 2026
1f72628
Refactor GalleryViewModel to account for vertical scrolling
tunjid Feb 1, 2026
5874c9f
Restructure gallery state declarations
tunjid Feb 1, 2026
1fcb635
Fix accidental modifier reuse
tunjid Feb 1, 2026
d95b0df
Merge pull request #926 from tunjid/tj/vertical-gallery
tunjid Feb 1, 2026
926bcaa
Pagination for vertically scrolling items in the gallery
tunjid Feb 1, 2026
904c224
Cleanup code
tunjid Feb 1, 2026
6d75d3a
Merge pull request #927 from tunjid/tj/vertical-gallery-2
tunjid Feb 1, 2026
246fa27
Make CursorQuery.Data UrlEncodableModel
tunjid Feb 1, 2026
77e809f
Fix dangling commit
tunjid Feb 1, 2026
6f9f691
lint
tunjid Feb 1, 2026
c42f4ab
Merge pull request #928 from tunjid/tj/vertical-gallery-3
tunjid Feb 1, 2026
4cb50f7
Expand galleryDestination to take a list of UrlEncodableModel
tunjid Feb 1, 2026
9bab35c
Update PostsScreen
tunjid Feb 1, 2026
9798a84
Merge pull request #929 from tunjid/tj/vertical-gallery-4
tunjid Feb 1, 2026
c7d08f2
Add metadata about if the post or media clicked is for the main post
tunjid Feb 1, 2026
8705cc5
Fix build errors
tunjid Feb 1, 2026
bd6b08d
Merge pull request #930 from tunjid/tj/vertical-gallery-5
tunjid Feb 1, 2026
2f2cb76
Pass metadata for gallery destination only when needed
tunjid Feb 1, 2026
2f87c40
Merge pull request #931 from tunjid/tj/vertical-gallery-6
tunjid Feb 1, 2026
8f28a9c
Read metadata for vertically paging gallery
tunjid Feb 1, 2026
6ce84ee
Merge pull request #932 from tunjid/tj/vertical-gallery-7
tunjid Feb 1, 2026
997400d
Do not let vertical pager grab drag to dismiss gesture
tunjid Feb 1, 2026
94f3df6
Merge pull request #933 from tunjid/tj/vertical-gallery-7
tunjid Feb 1, 2026
689ce09
Add consisten titles to screens
tunjid Feb 2, 2026
525d040
Merge pull request #934 from tunjid/tj/screen-titles
tunjid Feb 2, 2026
f6ff8c7
Autoplay gallery videos on scroll
tunjid Feb 2, 2026
3d18f07
Merge pull request #935 from tunjid/tj/gallery-auto-play
tunjid Feb 2, 2026
0f8bf26
Drag to pop with nested scroll WIP
tunjid Feb 2, 2026
cec7e38
Clean up
tunjid Feb 2, 2026
21fdc1e
More evaluation of DragToPop2
tunjid Feb 2, 2026
53cf5d7
Continue iterating on dragToPop2
tunjid Feb 2, 2026
3f35d82
Add ScrollableState.isConstrainedBy
tunjid Feb 2, 2026
516e0c9
Clean up code
tunjid Feb 2, 2026
11d640a
Use rememberDragToPop2State on AvatarScreen
tunjid Feb 2, 2026
294923b
Remove unused imports
tunjid Feb 2, 2026
d15786a
Complete rewrite of DragToPop
tunjid Feb 2, 2026
16d3524
Merge pull request #937 from tunjid/tj/drag-to-pop-rewrite
tunjid Feb 3, 2026
886bdb2
Split GalleryScreen methods into separate files
tunjid Feb 3, 2026
902ba88
Merge pull request #938 from tunjid/tj/gallery-organization
tunjid Feb 3, 2026
0234400
Lock gallery shared elements to items in the viewport
tunjid Feb 3, 2026
244395e
PR feedback
tunjid Feb 3, 2026
900ee5e
Merge pull request #939 from tunjid/tj/gallery-shared-elements
tunjid Feb 3, 2026
c0d49e7
Refine gallery drag to pop
tunjid Feb 3, 2026
452a26d
Refine gallery drag to pop
tunjid Feb 3, 2026
0b4de38
Keep initializer in composition
tunjid Feb 3, 2026
a65e7ac
Rename getOrCreate to manage
tunjid Feb 3, 2026
0722147
Merge pull request #940 from tunjid/tj/gallery-drag-to-pop
tunjid Feb 3, 2026
2353645
Add DragToPopState as a receiver in shouldDragToPop
tunjid Feb 3, 2026
cd9fa81
Update kdoc
tunjid Feb 3, 2026
a4e5c4c
Update kdoc
tunjid Feb 3, 2026
cd4c977
PR feedback
tunjid Feb 3, 2026
b617107
Merge pull request #941 from tunjid/tj/drag-to-pop-state-receiver
tunjid Feb 3, 2026
d4b4540
Gallery drag to pop refinement
tunjid Feb 3, 2026
82c2cf0
Merge pull request #942 from tunjid/tj/gallery-drag-to-pop-2
tunjid Feb 3, 2026
3468387
Speed up galley drag to pop
tunjid Feb 3, 2026
1a34fe0
Merge pull request #943 from tunjid/tj/galley-drag-to-pop-refinement
tunjid Feb 3, 2026
60ef3e1
Add indicator for gallery
tunjid Feb 3, 2026
601f0b1
Merge pull request #944 from tunjid/tj/gallery-indicators
tunjid Feb 3, 2026
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 @@ -42,7 +42,9 @@ import com.tunjid.heron.data.core.models.Post
import com.tunjid.heron.data.core.models.Preferences
import com.tunjid.heron.data.core.models.Timeline
import com.tunjid.heron.data.core.models.TimelineItem
import com.tunjid.heron.data.core.models.id
import com.tunjid.heron.data.core.models.offset
import com.tunjid.heron.data.core.models.uri
import com.tunjid.heron.data.core.models.value
import com.tunjid.heron.data.core.types.FeedGeneratorUri
import com.tunjid.heron.data.core.types.Id
Expand Down Expand Up @@ -149,29 +151,10 @@ sealed interface TimelineRequest {
}
}

class TimelineQuery(
data class TimelineQuery(
override val data: CursorQuery.Data,
val timeline: Timeline,
) : CursorQuery {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as TimelineQuery

if (data != other.data) return false
if (timeline.sourceId != other.timeline.sourceId) return false

return true
}

override fun hashCode(): Int {
var result = data.hashCode()
result = 31 * result + timeline.sourceId.hashCode()
return result
}
}
val source: Timeline.Source,
) : CursorQuery

interface TimelineRepository {

Expand Down Expand Up @@ -232,8 +215,8 @@ internal class OfflineTimelineRepository(
override fun timelineItems(
query: TimelineQuery,
cursor: Cursor,
): Flow<CursorList<TimelineItem>> = when (val timeline = query.timeline) {
is Timeline.Home.Following -> observeAndRefreshTimeline(
): Flow<CursorList<TimelineItem>> = when (val source = query.source) {
is Timeline.Source.Following -> observeAndRefreshTimeline(
query = query,
nextCursorFlow = networkService.nextTimelineCursorFlow(
query = query,
Expand All @@ -251,15 +234,15 @@ internal class OfflineTimelineRepository(
),
)

is Timeline.Home.Feed -> observeAndRefreshTimeline(
is Timeline.Source.Record.Feed -> observeAndRefreshTimeline(
query = query,
nextCursorFlow = networkService.nextTimelineCursorFlow(
query = query,
currentCursor = cursor,
currentRequestWithNextCursor = {
getFeed(
GetFeedQueryParams(
feed = AtUri(timeline.source.uri),
feed = AtUri(source.uri.uri),
limit = query.data.limit,
cursor = cursor.value,
),
Expand All @@ -270,15 +253,15 @@ internal class OfflineTimelineRepository(
),
)

is Timeline.Home.List -> observeAndRefreshTimeline(
is Timeline.Source.Record.List -> observeAndRefreshTimeline(
query = query,
nextCursorFlow = networkService.nextTimelineCursorFlow(
query = query,
currentCursor = cursor,
currentRequestWithNextCursor = {
getListFeed(
GetListFeedQueryParams(
list = AtUri(timeline.source.uri),
list = AtUri(source.uri.uri),
limit = query.data.limit,
cursor = cursor.value,
),
Expand All @@ -289,7 +272,7 @@ internal class OfflineTimelineRepository(
),
)

is Timeline.Profile -> when (timeline.type) {
is Timeline.Source.Profile -> when (source.type) {
Timeline.Profile.Type.Likes -> observeAndRefreshTimeline(
query = query,
nextCursorFlow = networkService.nextTimelineCursorFlow(
Expand All @@ -298,7 +281,7 @@ internal class OfflineTimelineRepository(
currentRequestWithNextCursor = {
getActorLikes(
GetActorLikesQueryParams(
actor = Did(timeline.profileId.id),
actor = Did(source.profileId.id),
limit = query.data.limit,
cursor = cursor.value,
),
Expand All @@ -317,7 +300,7 @@ internal class OfflineTimelineRepository(
currentRequestWithNextCursor = {
getAuthorFeed(
GetAuthorFeedQueryParams(
actor = Did(timeline.profileId.id),
actor = Did(source.profileId.id),
limit = query.data.limit,
cursor = cursor.value,
filter = GetAuthorFeedFilter.PostsWithMedia,
Expand All @@ -337,7 +320,7 @@ internal class OfflineTimelineRepository(
currentRequestWithNextCursor = {
getAuthorFeed(
GetAuthorFeedQueryParams(
actor = Did(timeline.profileId.id),
actor = Did(source.profileId.id),
limit = query.data.limit,
cursor = cursor.value,
filter = GetAuthorFeedFilter.PostsNoReplies,
Expand All @@ -357,7 +340,7 @@ internal class OfflineTimelineRepository(
currentRequestWithNextCursor = {
getAuthorFeed(
GetAuthorFeedQueryParams(
actor = Did(timeline.profileId.id),
actor = Did(source.profileId.id),
limit = query.data.limit,
cursor = cursor.value,
filter = GetAuthorFeedFilter.PostsWithReplies,
Expand All @@ -377,7 +360,7 @@ internal class OfflineTimelineRepository(
currentRequestWithNextCursor = {
getAuthorFeed(
GetAuthorFeedQueryParams(
actor = Did(timeline.profileId.id),
actor = Did(source.profileId.id),
limit = query.data.limit,
cursor = cursor.value,
filter = GetAuthorFeedFilter.PostsWithVideo,
Expand All @@ -389,14 +372,6 @@ internal class OfflineTimelineRepository(
),
)
}

is Timeline.StarterPack -> timelineItems(
query = TimelineQuery(
data = query.data,
timeline = timeline.listTimeline,
),
cursor = cursor,
)
}
.distinctUntilChanged()

Expand All @@ -409,7 +384,7 @@ internal class OfflineTimelineRepository(
networkRequestBlock = { query ->
getFeed(
GetFeedQueryParams(
feed = AtUri(timeline.source.uri),
feed = AtUri(timeline.uri.uri),
limit = query.data.limit,
cursor = null,
),
Expand Down Expand Up @@ -438,7 +413,7 @@ internal class OfflineTimelineRepository(
networkRequestBlock = { query ->
getListFeed(
GetListFeedQueryParams(
list = AtUri(timeline.source.uri),
list = AtUri(timeline.uri.uri),
limit = query.data.limit,
cursor = null,
),
Expand Down Expand Up @@ -621,7 +596,7 @@ internal class OfflineTimelineRepository(
.merge()
.scan(emptyList<Timeline.Home>()) { timelines, timeline ->
// Add newest item first
(listOf(timeline) + timelines).distinctBy(Timeline.Home::sourceId)
(listOf(timeline) + timelines).distinctBy { it.source.id }
}
.map { homeTimelines ->
homeTimelines.sortedBy(Timeline.Home::position)
Expand Down Expand Up @@ -764,7 +739,7 @@ internal class OfflineTimelineRepository(
timelineDao.updatePreferredTimelinePresentation(
partial = preferredPresentationPartial(
signedInProfileId = signedInProfileId,
sourceId = timeline.sourceId,
sourceId = timeline.source.id,
presentation = presentation,
),
)
Expand Down Expand Up @@ -810,12 +785,12 @@ internal class OfflineTimelineRepository(
onResponse = {
multipleEntitySaverProvider.saveInTransaction {
if (timelineDao.isFirstPageForDifferentAnchor(signedInProfileId, query)) {
timelineDao.deleteAllFeedsFor(query.timeline.sourceId)
timelineDao.deleteAllFeedsFor(query.source.id)
timelineDao.insertOrPartiallyUpdateTimelineFetchedAt(
listOf(
TimelinePreferencesEntity(
viewingProfileId = signedInProfileId,
sourceId = query.timeline.sourceId,
sourceId = query.source.id,
lastFetchedAt = query.data.cursorAnchor,
preferredPresentation = null,
),
Expand All @@ -825,7 +800,7 @@ internal class OfflineTimelineRepository(
add(
viewingProfileId = signedInProfileId,
query = query,
timeline = query.timeline,
source = query.source,
feedViewPosts = networkFeed(),
)
}
Expand Down Expand Up @@ -860,7 +835,7 @@ internal class OfflineTimelineRepository(
add(
viewingProfileId = signedInProfileId,
query = query,
timeline = timeline,
source = timeline.source,
feedViewPosts = fetchedFeedViewPosts,
)
}
Expand All @@ -875,22 +850,22 @@ internal class OfflineTimelineRepository(
combine(
timelineDao.lastFetchKey(
viewingProfileId = signedInProfileId?.id,
sourceId = timeline.sourceId,
sourceId = timeline.source.id,
)
.map { it?.lastFetchedAt ?: pollInstant }
.distinctUntilChangedBy(Instant::toEpochMilliseconds)
.flatMapLatest {
timelineDao.feedItems(
viewingProfileId = signedInProfileId?.id,
sourceId = timeline.sourceId,
sourceId = timeline.source.id,
before = it,
limit = 1,
offset = 0,
)
},
timelineDao.feedItems(
viewingProfileId = signedInProfileId?.id,
sourceId = timeline.sourceId,
sourceId = timeline.source.id,
before = pollInstant,
limit = 1,
offset = 0,
Expand Down Expand Up @@ -919,7 +894,7 @@ internal class OfflineTimelineRepository(
savedStateDataSource.singleSessionFlow { signedInProfileId ->
timelineDao.feedItems(
viewingProfileId = signedInProfileId?.id,
sourceId = query.timeline.sourceId,
sourceId = query.source.id,
before = query.data.cursorAnchor,
offset = query.data.offset,
limit = query.data.limit,
Expand Down Expand Up @@ -948,7 +923,7 @@ internal class OfflineTimelineRepository(
},
block = block@{ entity ->
// Muted posts should only show up on profile timelines
val hideMuted = query.timeline !is Timeline.Profile
val hideMuted = query.source !is Timeline.Source.Profile
var isMuted = isMuted(post)
if (hideMuted && isMuted) return@block

Expand Down Expand Up @@ -1247,7 +1222,7 @@ private suspend fun TimelineDao.isFirstPageForDifferentAnchor(
if (query.data.page != 0) return false
val lastFetchedAt = lastFetchKey(
viewingProfileId = signedInProfileId?.id,
sourceId = query.timeline.sourceId,
sourceId = query.source.id,
).first()?.lastFetchedAt
return lastFetchedAt?.toEpochMilliseconds() != query.data.cursorAnchor.toEpochMilliseconds()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.tunjid.heron.data.utilities.multipleEntitysaver
import app.bsky.feed.FeedViewPost
import com.tunjid.heron.data.core.models.CursorQuery
import com.tunjid.heron.data.core.models.Timeline
import com.tunjid.heron.data.core.models.id
import com.tunjid.heron.data.core.models.offset
import com.tunjid.heron.data.core.types.PostUri
import com.tunjid.heron.data.core.types.ProfileId
Expand All @@ -32,15 +33,15 @@ import com.tunjid.heron.data.network.models.profileEntity
internal fun MultipleEntitySaver.add(
viewingProfileId: ProfileId?,
query: CursorQuery,
timeline: Timeline,
source: Timeline.Source,
feedViewPosts: List<FeedViewPost>,
) {
for (index in feedViewPosts.indices) {
val feedView = feedViewPosts[index]
// Extract data from feed
add(
feedView.feedItemEntity(
sourceId = timeline.sourceId,
sourceId = source.id,
itemSort = query.itemSortKey(index),
viewingProfileId = viewingProfileId,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,40 +141,44 @@ private fun MultipleEntitySaver.add(
messageId: MessageId,
record: RecordViewRecordUnion.ViewRecord,
) {
record.value.embeds?.forEach { embed ->
val postView = PostView(
uri = record.value.uri,
cid = record.value.cid,
author = record.value.author,
record = record.value.value,
embed = when (embed) {
is MessagePost.ExternalView -> TimelinePost.ExternalView(embed.value)
is MessagePost.ImagesView -> TimelinePost.ImagesView(embed.value)
is MessagePost.RecordView -> TimelinePost.RecordView(embed.value)
is MessagePost.RecordWithMediaView -> TimelinePost.RecordWithMediaView(embed.value)
is MessagePost.Unknown -> TimelinePost.Unknown(embed.value)
is MessagePost.VideoView -> TimelinePost.VideoView(embed.value)
},
replyCount = record.value.replyCount,
repostCount = record.value.repostCount,
likeCount = record.value.likeCount,
quoteCount = record.value.quoteCount,
indexedAt = record.value.indexedAt,
viewer = null,
labels = record.value.labels,
threadgate = null,
)
add(
viewingProfileId = viewingProfileId,
postView = postView,
)
add(
entity = MessagePostEntity(
messageId = messageId,
postUri = postView.uri.atUri.let(::PostUri),
),
)
}
// This is an error in the upstream lexicon. Only one embed should in a record
// Here, the first (and only) embed present is actually written.
val embed = record.value.embeds?.firstOrNull()

val postView = PostView(
uri = record.value.uri,
cid = record.value.cid,
author = record.value.author,
record = record.value.value,
embed = when (embed) {
is MessagePost.ExternalView -> TimelinePost.ExternalView(embed.value)
is MessagePost.ImagesView -> TimelinePost.ImagesView(embed.value)
is MessagePost.RecordView -> TimelinePost.RecordView(embed.value)
is MessagePost.RecordWithMediaView -> TimelinePost.RecordWithMediaView(embed.value)
is MessagePost.Unknown -> TimelinePost.Unknown(embed.value)
is MessagePost.VideoView -> TimelinePost.VideoView(embed.value)
null -> null
},
replyCount = record.value.replyCount,
repostCount = record.value.repostCount,
likeCount = record.value.likeCount,
quoteCount = record.value.quoteCount,
indexedAt = record.value.indexedAt,
viewer = null,
labels = record.value.labels,
threadgate = null,
)

add(
viewingProfileId = viewingProfileId,
postView = postView,
)
add(
entity = MessagePostEntity(
messageId = messageId,
postUri = postView.uri.atUri.let(::PostUri),
),
)
}

internal fun MultipleEntitySaver.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.tunjid.heron.data.core.models.NotificationPreferences
import com.tunjid.heron.data.core.models.Post
import com.tunjid.heron.data.core.models.Profile
import com.tunjid.heron.data.core.models.Timeline
import com.tunjid.heron.data.core.models.sourceId
import com.tunjid.heron.data.core.utilities.Outcome
import kotlin.time.Instant
import kotlinx.serialization.Serializable
Expand Down
Loading
Loading