Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1fc2257
Notification highlights and UI improvements
tunjid Jan 2, 2026
4bb5dd6
Remove unused statement
tunjid Jan 2, 2026
2a44321
Merge pull request #785 from tunjid/tj/unread-notifications-highlights
tunjid Jan 2, 2026
6b02124
UI for creating a new conversation
tunjid Jan 2, 2026
9dfd7af
Remove unused imports
tunjid Jan 2, 2026
351c04e
PR feedback
tunjid Jan 2, 2026
74a1274
Merge pull request #786 from tunjid/tj/compose-dms
tunjid Jan 2, 2026
8943e65
Resolve conversation id for new messages
tunjid Jan 3, 2026
0369f40
Clean up PR
tunjid Jan 3, 2026
030d471
Merge pull request #787 from tunjid/tj/resolve-conversation-id
tunjid Jan 3, 2026
e30575e
Do not show empty load empty conversations
tunjid Jan 3, 2026
8c1d6b5
Gating for chats
tunjid Jan 3, 2026
193f4f3
Lint
tunjid Jan 3, 2026
3b73e9e
PR feedback
tunjid Jan 3, 2026
8766ce9
Merge pull request #789 from tunjid/tj/chat-gating
tunjid Jan 3, 2026
eb2b927
Add missing annotations to chat allowed
tunjid Jan 3, 2026
3540279
Merge pull request #790 from tunjid/tj/chat-allowed-serialzable-annot…
tunjid Jan 3, 2026
f687b3d
Allow for setting alt text on media
tunjid Jan 3, 2026
db81204
Update UI
tunjid Jan 3, 2026
4a742e4
PR feedback
tunjid Jan 3, 2026
8a02b4b
Clean up code
tunjid Jan 3, 2026
0967a3c
Merge pull request #791 from tunjid/tj/media-alt-text
tunjid Jan 3, 2026
ad48c0d
Clean up media files code
tunjid Jan 3, 2026
0158006
Merge pull request #792 from tunjid/tj/media-files-refactor
tunjid Jan 3, 2026
b0203be
re-enable dynamic color/themes
ThatOneCalculator Jan 4, 2026
7604331
extrapolate dynamic theming to setting
ThatOneCalculator Jan 4, 2026
4475fc0
feat: compact navigation bar setting
ThatOneCalculator Jan 4, 2026
66ddf0a
fix: standard compact size (m3) is 72dp
ThatOneCalculator Jan 4, 2026
a4e462f
Merge pull request #795 from ThatOneCalculator/reenable-material-you
tunjid Jan 4, 2026
3b2a70a
Merge pull request #796 from ThatOneCalculator/feat/compact-toolbar
tunjid Jan 4, 2026
80f10cf
Make SavedStateDataSource internal
tunjid Jan 4, 2026
1992b5a
Start making changes to drill down compact nav bar prob down the UI tree
tunjid Jan 4, 2026
90aa449
Drill bottom nav props down to screens
tunjid Jan 4, 2026
144eba9
Consolidate compact bottom nav height logic
tunjid Jan 4, 2026
8e68470
Animate bottom nav size changes
tunjid Jan 4, 2026
85a4729
run ktlint
tunjid Jan 4, 2026
a654e46
Update bottom nav and settings copy
tunjid Jan 4, 2026
46e3272
PR feedback
tunjid Jan 4, 2026
89da6d6
Update AppearanceItem expect actual file names
tunjid Jan 4, 2026
b19382d
Merge pull request #798 from tunjid/tj/ui-changes
tunjid Jan 4, 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 @@ -21,6 +21,7 @@ import com.tunjid.heron.conversation.di.ConversationBindings
import com.tunjid.heron.data.di.AppCoroutineScope
import com.tunjid.heron.data.di.DataBindings
import com.tunjid.heron.data.repository.AuthRepository
import com.tunjid.heron.data.repository.UserDataRepository
import com.tunjid.heron.data.utilities.writequeue.WriteQueue
import com.tunjid.heron.editprofile.di.EditProfileBindings
import com.tunjid.heron.feed.di.FeedBindings
Expand Down Expand Up @@ -95,6 +96,7 @@ interface AppGraph {
@AppCoroutineScope
appScope: CoroutineScope,
authRepository: AuthRepository,
userDataRepository: UserDataRepository,
navigationStateHolder: NavigationStateHolder,
notificationStateHolder: NotificationStateHolder,
imageLoader: ImageLoader,
Expand All @@ -103,6 +105,7 @@ interface AppGraph {
): AppState = AppState(
entryMap = entryMap,
authRepository = authRepository,
userDataRepository = userDataRepository,
navigationStateHolder = navigationStateHolder,
notificationStateHolder = notificationStateHolder,
imageLoader = imageLoader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ private val HeronOauthScopes = listOf(

private val ChatProxyPaths = listOf(
"chat.bsky.convo.listConvos",
"chat.bsky.convo.getConvoForMembers",
"chat.bsky.convo.getMessages",
"chat.bsky.convo.getLog",
"chat.bsky.convo.sendMessage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.tunjid.heron.data.network.models

import app.bsky.actor.KnownFollowers
import app.bsky.actor.ProfileAssociated
import app.bsky.actor.ProfileAssociatedChatAllowIncoming
import app.bsky.actor.ProfileView
import app.bsky.actor.ProfileViewBasic
import app.bsky.actor.ProfileViewDetailed
Expand Down Expand Up @@ -183,6 +185,9 @@ internal fun ProfileViewBasic.profile() = Profile(
createdListCount = associated?.lists ?: 0,
createdFeedGeneratorCount = associated?.feedgens ?: 0,
createdStarterPackCount = associated?.starterPacks ?: 0,
chat = Profile.ChatInfo(
allowed = associated.allowedChat(),
),
),
labels = labels?.map(com.atproto.label.Label::asExternalModel) ?: emptyList(),
isLabeler = associated?.labeler ?: false,
Expand All @@ -205,11 +210,24 @@ internal fun ProfileView.profile() = Profile(
createdListCount = associated?.lists ?: 0,
createdFeedGeneratorCount = associated?.feedgens ?: 0,
createdStarterPackCount = associated?.starterPacks ?: 0,
chat = Profile.ChatInfo(
allowed = associated.allowedChat(),
),
),
labels = labels?.map(com.atproto.label.Label::asExternalModel) ?: emptyList(),
isLabeler = associated?.labeler ?: false,
)

private fun ProfileAssociated?.allowedChat(): Profile.ChatInfo.Allowed =
when (this?.chat?.allowIncoming) {
ProfileAssociatedChatAllowIncoming.All -> Profile.ChatInfo.Allowed.Everyone
ProfileAssociatedChatAllowIncoming.Following -> Profile.ChatInfo.Allowed.Following
ProfileAssociatedChatAllowIncoming.None,
is ProfileAssociatedChatAllowIncoming.Unknown,
null,
-> Profile.ChatInfo.Allowed.NoOne
}

private fun profileViewerStateEntity(
viewingProfileId: ProfileId,
viewedProfileId: ProfileId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import app.bsky.embed.Record as BskyRecord
import chat.bsky.convo.AddReactionRequest
import chat.bsky.convo.AddReactionResponse
import chat.bsky.convo.DeletedMessageView
import chat.bsky.convo.GetConvoForMembersQueryParams
import chat.bsky.convo.GetLogQueryParams
import chat.bsky.convo.GetLogResponseLogUnion as Log
import chat.bsky.convo.GetMessagesQueryParams
Expand Down Expand Up @@ -47,14 +48,10 @@ import com.tunjid.heron.data.core.models.Message
import com.tunjid.heron.data.core.models.offset
import com.tunjid.heron.data.core.models.value
import com.tunjid.heron.data.core.types.ConversationId
import com.tunjid.heron.data.core.types.ProfileId
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.MessageDao
import com.tunjid.heron.data.database.daos.PostDao
import com.tunjid.heron.data.database.daos.ProfileDao
import com.tunjid.heron.data.database.daos.StarterPackDao
import com.tunjid.heron.data.database.entities.PopulatedConversationEntity
import com.tunjid.heron.data.database.entities.PopulatedMessageEntity
import com.tunjid.heron.data.database.entities.asExternalModel
Expand Down Expand Up @@ -82,6 +79,7 @@ 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
data class ConversationQuery(
Expand All @@ -108,6 +106,10 @@ interface MessageRepository {

suspend fun monitorConversationLogs()

suspend fun resolveConversation(
with: ProfileId,
): Result<ConversationId>

suspend fun sendMessage(
message: Message.Create,
): Outcome
Expand Down Expand Up @@ -319,6 +321,32 @@ internal class OfflineMessageRepository @Inject constructor(
}
}

override suspend fun resolveConversation(
with: ProfileId,
): Result<ConversationId> = savedStateDataSource.inCurrentProfileSession { signedInProfileId ->
if (signedInProfileId == null) return@inCurrentProfileSession expiredSessionResult()
networkService.runCatchingWithMonitoredNetworkRetry {
getConvoForMembers(
params = GetConvoForMembersQueryParams(
members = listOf(
with.id.let(::Did),
),
),
)
}
.onSuccess {
multipleEntitySaverProvider.saveInTransaction {
add(
viewingProfileId = signedInProfileId,
convoView = it.convo,
)
}
}
.map {
ConversationId(it.convo.id)
}
} ?: expiredSessionResult()

override suspend fun sendMessage(
message: Message.Create,
): Outcome = savedStateDataSource.inCurrentProfileSession { signedInProfileId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.tunjid.heron.data.core.models.Preferences
import com.tunjid.heron.data.core.models.Server
import com.tunjid.heron.data.core.types.ProfileHandle
import com.tunjid.heron.data.core.types.ProfileId
import com.tunjid.heron.data.core.types.Uri
import com.tunjid.heron.data.core.utilities.Outcome
import com.tunjid.heron.data.datastore.migrations.VersionedSavedState
import com.tunjid.heron.data.datastore.migrations.VersionedSavedStateOkioSerializer
Expand Down Expand Up @@ -101,6 +100,7 @@ abstract class SavedState {
didDoc.service
.firstOrNull()
?.serviceEndpoint

is DPoP -> pdsUrl
}

Expand Down Expand Up @@ -216,9 +216,13 @@ abstract class SavedState {
}
}

val InitialSavedState: SavedState = VersionedSavedState.Initial
private val InitialSavedState: SavedState = VersionedSavedState.Initial

private val EmptySavedState: SavedState = VersionedSavedState.Empty

val EmptySavedState: SavedState = VersionedSavedState.Empty
val InitialNavigation: SavedState.Navigation = InitialSavedState.navigation

val EmptyNavigation: SavedState.Navigation = EmptySavedState.navigation

fun SavedState.isSignedIn(): Boolean =
auth.ifSignedIn() != null
Expand Down Expand Up @@ -266,7 +270,7 @@ private fun preferencesForUrl(url: String) =
else -> Preferences.BlueSkyGuestPreferences
}

sealed class SavedStateDataSource {
internal sealed class SavedStateDataSource {
abstract val savedState: StateFlow<SavedState>

abstract suspend fun setNavigationState(
Expand All @@ -280,14 +284,6 @@ sealed class SavedStateDataSource {
internal abstract suspend fun updateSignedInProfileData(
block: SavedState.ProfileData.(signedInProfileId: ProfileId?) -> SavedState.ProfileData,
)

abstract suspend fun setLastViewedHomeTimelineUri(
uri: Uri,
)

abstract suspend fun setRefreshedHomeTimelineOnLaunch(
refreshOnLaunch: Boolean,
)
}

@Inject
Expand Down Expand Up @@ -333,6 +329,7 @@ internal class DataStoreSavedStateDataSource(
update = { copy(auth = null) },
)
}

is SavedState.AuthTokens.Guest -> profileData.updateOrPutValue(
key = auth.authProfileId,
update = {
Expand All @@ -343,6 +340,7 @@ internal class DataStoreSavedStateDataSource(
},
put = { SavedState.ProfileData.fromTokens(auth) },
)

else -> profileData.updateOrPutValue(
key = auth.authProfileId,
update = { copy(auth = auth) },
Expand All @@ -367,18 +365,6 @@ internal class DataStoreSavedStateDataSource(
)
}

override suspend fun setLastViewedHomeTimelineUri(
uri: Uri,
) = updateSignedInProfileData {
copy(preferences = preferences.copy(lastViewedHomeTimelineUri = uri))
}

override suspend fun setRefreshedHomeTimelineOnLaunch(
refreshOnLaunch: Boolean,
) = updateSignedInProfileData {
copy(preferences = preferences.copy(refreshHomeTimelineOnLaunch = refreshOnLaunch))
}

private suspend fun updateState(update: VersionedSavedState.() -> VersionedSavedState) {
dataStore.updateData(update)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import app.bsky.actor.PutPreferencesRequest
import com.tunjid.heron.data.core.models.MutedWordPreference
import com.tunjid.heron.data.core.models.Preferences
import com.tunjid.heron.data.core.models.Timeline
import com.tunjid.heron.data.core.types.Uri
import com.tunjid.heron.data.core.utilities.Outcome
import com.tunjid.heron.data.network.NetworkService
import com.tunjid.heron.data.utilities.preferenceupdater.PreferenceUpdater
import com.tunjid.heron.data.utilities.runCatchingUnlessCancelled
import com.tunjid.heron.data.utilities.toOutcome
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
Expand All @@ -17,6 +20,28 @@ interface UserDataRepository {

val preferences: Flow<Preferences>

val navigation: Flow<SavedState.Navigation>

suspend fun persistNavigationState(
navigation: SavedState.Navigation,
): Outcome

suspend fun setLastViewedHomeTimelineUri(
uri: Uri,
): Outcome

suspend fun setRefreshedHomeTimelineOnLaunch(
refreshOnLaunch: Boolean,
): Outcome

suspend fun setDynamicTheming(
dynamicTheming: Boolean,
): Outcome

suspend fun setCompactNavigation(
compactNavigation: Boolean,
): Outcome

suspend fun updateMutedWords(
mutedWordPreferences: List<MutedWordPreference>,
): Outcome
Expand All @@ -39,6 +64,42 @@ internal class OfflineUserDataRepository @Inject constructor(
}
.distinctUntilChanged()

override val navigation: Flow<SavedState.Navigation>
get() = savedStateDataSource.savedState
.map { it.navigation }

override suspend fun persistNavigationState(
navigation: SavedState.Navigation,
): Outcome = runCatchingUnlessCancelled {
if (navigation != InitialNavigation) savedStateDataSource.setNavigationState(
navigation = navigation,
)
}.toOutcome()

override suspend fun setLastViewedHomeTimelineUri(
uri: Uri,
): Outcome = updatePreferences {
copy(lastViewedHomeTimelineUri = uri)
}

override suspend fun setRefreshedHomeTimelineOnLaunch(
refreshOnLaunch: Boolean,
): Outcome = updatePreferences {
copy(refreshHomeTimelineOnLaunch = refreshOnLaunch)
}

override suspend fun setDynamicTheming(
dynamicTheming: Boolean,
): Outcome = updatePreferences {
copy(useDynamicTheming = dynamicTheming)
}

override suspend fun setCompactNavigation(
compactNavigation: Boolean,
): Outcome = updatePreferences {
copy(useCompactNavigation = compactNavigation)
}

override suspend fun updateMutedWords(
mutedWordPreferences: List<MutedWordPreference>,
): Outcome {
Expand Down Expand Up @@ -91,4 +152,13 @@ internal class OfflineUserDataRepository @Inject constructor(
copy(preferences = updatedPreferences)
}
}

private suspend fun updatePreferences(
updater: Preferences.() -> Preferences,
): Outcome =
runCatchingUnlessCancelled {
savedStateDataSource.updateSignedInProfileData {
copy(preferences = preferences.updater())
}
}.toOutcome()
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interface MessageDao {
lastReaction.createdAt AS lastReaction_createdAt,
MAX(COALESCE(lastReaction.createdAt, lastMessage.sentAt), lastMessage.sentAt) AS sort
FROM conversations
LEFT JOIN messages AS lastMessage
INNER JOIN messages AS lastMessage
ON lastMessageId = lastMessage.id
LEFT JOIN messages AS lastMessageReactedTo
ON lastReactedToMessageId = lastMessageReactedTo.id
Expand Down
Loading
Loading