Skip to content

Commit 089ec44

Browse files
authored
Merge pull request #1041 from tunjid/bugfix/2.0.3
Bugfix/2.0.3
2 parents cce37e2 + 55bb938 commit 089ec44

File tree

5 files changed

+84
-41
lines changed

5 files changed

+84
-41
lines changed

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

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import app.bsky.notification.ListNotificationsResponse
2828
import app.bsky.notification.Preference
2929
import app.bsky.notification.PutPreferencesV2Request
3030
import app.bsky.notification.UpdateSeenRequest
31+
import com.atproto.server.GetServiceAuthQueryParams
3132
import com.tunjid.heron.data.InternalEndpoints
3233
import com.tunjid.heron.data.core.models.Block
3334
import com.tunjid.heron.data.core.models.Cursor
@@ -51,6 +52,7 @@ import com.tunjid.heron.data.core.models.shouldShowNotification
5152
import com.tunjid.heron.data.core.models.value
5253
import com.tunjid.heron.data.core.types.MutedThreadException
5354
import com.tunjid.heron.data.core.types.NotificationFilteredOutException
55+
import com.tunjid.heron.data.core.types.PostUri
5456
import com.tunjid.heron.data.core.types.ProfileId
5557
import com.tunjid.heron.data.core.types.RecordUri
5658
import com.tunjid.heron.data.core.types.RepostUri
@@ -86,13 +88,15 @@ import dev.zacsweers.metro.Inject
8688
import io.ktor.client.HttpClient
8789
import io.ktor.client.plugins.DefaultRequest
8890
import io.ktor.client.plugins.HttpTimeout
91+
import io.ktor.client.request.bearerAuth
8992
import io.ktor.client.request.post
9093
import io.ktor.client.request.setBody
9194
import io.ktor.http.ContentType
9295
import io.ktor.http.contentType
9396
import io.ktor.http.takeFrom
9497
import kotlin.time.Clock
9598
import kotlin.time.Duration.Companion.milliseconds
99+
import kotlin.time.Duration.Companion.minutes
96100
import kotlin.time.Duration.Companion.seconds
97101
import kotlin.time.Instant
98102
import kotlinx.coroutines.CoroutineDispatcher
@@ -112,6 +116,8 @@ import kotlinx.coroutines.flow.map
112116
import kotlinx.coroutines.flow.stateIn
113117
import kotlinx.coroutines.plus
114118
import kotlinx.serialization.Serializable
119+
import sh.christian.ozone.api.Did
120+
import sh.christian.ozone.api.Nsid
115121
import sh.christian.ozone.api.response.AtpResponse
116122

117123
@Serializable
@@ -120,6 +126,7 @@ data class NotificationsQuery(
120126
) : CursorQuery {
121127
data class Push(
122128
val senderId: ProfileId,
129+
val targetDid: ProfileId,
123130
val recordUri: RecordUri,
124131
val reason: Notification.Reason,
125132
)
@@ -302,18 +309,37 @@ internal class OfflineNotificationsRepository @Inject constructor(
302309
token: String,
303310
) = savedStateDataSource.inCurrentProfileSession { signedProfileId ->
304311
if (signedProfileId == null) return@inCurrentProfileSession expiredSessionOutcome()
305-
val saveNotificationTokenRequest = SaveNotificationTokenRequest(
306-
did = signedProfileId.id,
307-
token = token,
308-
)
309-
networkMonitor.runCatchingWithNetworkRetry(
310-
block = {
311-
notificationsClient.post(SaveNotificationTokenPath) {
312-
contentType(ContentType.Application.Json)
313-
setBody(saveNotificationTokenRequest)
314-
}
315-
},
316-
).toOutcome()
312+
313+
networkService.runCatchingWithMonitoredNetworkRetry {
314+
getServiceAuth(
315+
GetServiceAuthQueryParams(
316+
aud = Did(signedProfileId.id),
317+
exp = Clock.System.now().epochSeconds + 5.minutes.inWholeSeconds,
318+
lxm = Nsid(PostUri.NAMESPACE),
319+
),
320+
)
321+
}.mapToResult { tokenResponse ->
322+
val saveNotificationTokenRequest = SaveNotificationTokenRequest(
323+
did = signedProfileId.id,
324+
token = token,
325+
otherDids = savedStateDataSource.savedState
326+
.value.pastSessions
327+
?.mapNotNull {
328+
if (it.profileId == signedProfileId) null
329+
else it.profileId.id
330+
}
331+
.orEmpty(),
332+
)
333+
networkMonitor.runCatchingWithNetworkRetry(
334+
block = {
335+
notificationsClient.post(SaveNotificationTokenPath) {
336+
contentType(ContentType.Application.Json)
337+
setBody(saveNotificationTokenRequest)
338+
bearerAuth(tokenResponse.token)
339+
}
340+
},
341+
)
342+
}.toOutcome()
317343
} ?: expiredSessionOutcome()
318344

319345
override suspend fun updateNotificationPreferences(
@@ -348,7 +374,9 @@ internal class OfflineNotificationsRepository @Inject constructor(
348374
push = update.push,
349375
)
350376
when (update.reason) {
351-
Notification.Reason.JoinedStarterPack -> currentPrefs.copy(starterpackJoined = simplePref)
377+
Notification.Reason.JoinedStarterPack -> currentPrefs.copy(
378+
starterpackJoined = simplePref,
379+
)
352380
Notification.Reason.SubscribedPost -> currentPrefs.copy(subscribedPost = simplePref)
353381
Notification.Reason.Unverified -> currentPrefs.copy(unverified = simplePref)
354382
Notification.Reason.Verified -> currentPrefs.copy(verified = simplePref)
@@ -405,12 +433,9 @@ internal class OfflineNotificationsRepository @Inject constructor(
405433
query: NotificationsQuery.Push,
406434
): Result<Notification> =
407435
// Push notifications can be received for any profile that has been signed in
408-
savedStateDataSource.inPastSession(query.recordUri.profileId()) { token ->
409-
val signedInProfileId = token.authProfileId
410-
436+
savedStateDataSource.inProfileSession(query.targetDid) { signedInProfileId ->
411437
recordResolver.resolve(query.recordUri)
412438
.mapCatchingUnlessCancelled { resolvedRecord ->
413-
414439
val authorEntity = profileDao.profiles(
415440
signedInProfiledId = signedInProfileId.id,
416441
ids = listOf(query.senderId),
@@ -552,7 +577,9 @@ internal class OfflineNotificationsRepository @Inject constructor(
552577
}
553578
}
554579

555-
val notificationPreferences = savedStateDataSource.savedState.value.signedNotificationPreferencesOrDefault()
580+
val notificationPreferences = profileData.notifications
581+
.preferences
582+
?: NotificationPreferences.Default
556583

557584
val isAuthorFollowed = viewerState?.isFollowing == true
558585

@@ -690,6 +717,7 @@ private fun SavedState.signedInProfileNotifications() =
690717
private data class SaveNotificationTokenRequest(
691718
val did: String,
692719
val token: String,
720+
val otherDids: List<String>,
693721
)
694722

695723
private const val SaveNotificationTokenPath = "/saveNotificationToken"

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

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -458,26 +458,24 @@ internal suspend fun SavedStateDataSource.updateSignedInUserNotifications(
458458
* Runs the [block] in the context of a single profile's session
459459
*/
460460
internal suspend inline fun <T> SavedStateDataSource.inCurrentProfileSession(
461-
crossinline block: suspend (ProfileId?) -> T,
461+
crossinline block: suspend SessionContext.Current.(ProfileId?) -> T,
462462
): T? {
463463
val state = savedState.first { it != InitialSavedState }
464464
val currentProfileId = state.signedInProfileId
465465
val profileData = currentProfileId?.let { state.profileData(it) }
466-
467-
return withContext(
468-
SessionContext.Current(
469-
tokens = profileData?.auth,
470-
profileData = profileData ?: SavedState.ProfileData.defaultGuestData,
471-
),
472-
) {
466+
val context = SessionContext.Current(
467+
tokens = profileData?.auth,
468+
profileData = profileData ?: SavedState.ProfileData.defaultGuestData,
469+
)
470+
return withContext(context) {
473471
coroutineScope {
474472
select {
475473
async {
476474
savedState.first { it.signedInProfileId != currentProfileId }
477475
null
478476
}.onAwait { it }
479477
async {
480-
block(currentProfileId)
478+
block(context, currentProfileId)
481479
}.onAwait { it }
482480
}.also { coroutineContext.cancelChildren() }
483481
}
@@ -545,20 +543,33 @@ internal inline fun <T> SavedStateDataSource.singleSessionFlow(
545543
*/
546544
internal suspend inline fun <T> SavedStateDataSource.inPastSession(
547545
profileId: ProfileId,
548-
crossinline block: suspend (SavedState.AuthTokens.Authenticated) -> T,
546+
crossinline block: suspend SessionContext.Previous.() -> T,
549547
): T? {
550548
val state = savedState.first { it != InitialSavedState }
551549

552550
val profileData = state.profileData(profileId) ?: return null
553551
val auth = profileData.auth as? SavedState.AuthTokens.Authenticated ?: return null
552+
val context = SessionContext.Previous(
553+
tokens = auth,
554+
profileData = profileData,
555+
)
556+
return withContext(context) {
557+
block(context)
558+
}
559+
}
554560

555-
return withContext(
556-
SessionContext.Previous(
557-
tokens = auth,
558-
profileData = profileData,
559-
),
560-
) {
561-
block(auth)
561+
internal suspend inline fun <T> SavedStateDataSource.inProfileSession(
562+
profileId: ProfileId,
563+
crossinline block: suspend SessionContext.(ProfileId) -> T,
564+
): T? {
565+
val state = savedState.first { it != InitialSavedState }
566+
val currentProfileId = state.signedInProfileId
567+
568+
return if (currentProfileId == profileId) inCurrentProfileSession { signedInProfileId ->
569+
if (signedInProfileId == null) null else block(profileId)
570+
}
571+
else inPastSession(profileId) {
572+
block(profileId)
562573
}
563574
}
564575

scaffold/src/androidMain/kotlin/com/tunjid/heron/scaffold/notifications/AndroidNotifier.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ class AndroidNotifier(
5555
private val notificationManager = NotificationManagerCompat.from(context)
5656

5757
override suspend fun displayNotifications(notifications: List<Notification>) {
58-
val currentLifecycleState = ProcessLifecycleOwner.get().lifecycle.currentStateFlow.value
59-
60-
// Show notifications in the background only
61-
if (currentLifecycleState.isAtLeast(Lifecycle.State.RESUMED)) return
58+
// TODO: When in app notifications are supported, check process lifecycle
59+
// before displaying in the notifications tray. Till then, display in tray.
6260

6361
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
6462
ContextCompat.checkSelfPermission(

scaffold/src/commonMain/kotlin/com/tunjid/heron/scaffold/notifications/NotificationStateHolder.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ sealed class NotificationAction(
7474
val senderDid = payload[NotificationAtProtoSenderDid]
7575
?.let(::ProfileId)
7676

77+
val targetDid = payload[NotificationAtProtoTargetDid]
78+
?.let(::ProfileId)
79+
7780
val recordUri: RecordUri? = payload[NotificationAtProtoRecordUri]
7881
?.let { "${Uri.Host.AtProto.prefix}$it" }
7982
?.asRecordUriOrNull()
@@ -164,7 +167,7 @@ private fun Flow<NotificationAction.RegisterToken>.registerTokenMutations(
164167
// support updating a queued write to something else. For now, just write
165168
// using the app scope and fix in a follow up PR.
166169
val state = currentState()
167-
if (state.hasNotificationPermissions && action.token != state.notificationToken) {
170+
if (state.hasNotificationPermissions) {
168171
val tokenRegistrationOutcome = notificationsRepository.registerPushNotificationToken(
169172
action.token,
170173
)
@@ -186,6 +189,7 @@ private fun Flow<NotificationAction.HandleNotification>.handleNotificationMutati
186189
.flatMapMerge(NotificationProcessingMaxConcurrencyLimit) { action ->
187190

188191
val senderId = action.senderDid ?: return@flatMapMerge emptyFlow()
192+
val targetDid = action.targetDid ?: return@flatMapMerge emptyFlow()
189193
val recordUri = action.recordUri ?: return@flatMapMerge emptyFlow()
190194
val reason = action.reason ?: return@flatMapMerge emptyFlow()
191195

@@ -196,6 +200,7 @@ private fun Flow<NotificationAction.HandleNotification>.handleNotificationMutati
196200
notificationsRepository.resolvePushNotification(
197201
NotificationsQuery.Push(
198202
senderId = senderId,
203+
targetDid = targetDid,
199204
recordUri = recordUri,
200205
reason = reason,
201206
),
@@ -250,6 +255,7 @@ private fun Flow<NotificationAction.RequestedNotificationPermission>.markNotific
250255
}
251256

252257
private const val NotificationAtProtoSenderDid = "senderDid"
258+
private const val NotificationAtProtoTargetDid = "targetDid"
253259
private const val NotificationAtProtoRecordUri = "recordUri"
254260
private const val NotificationAtProtoReason = "reason"
255261
private const val NotificationProcessingMaxConcurrencyLimit = 4

ui/timeline/src/commonMain/kotlin/com/tunjid/heron/timeline/state/Timeline.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,4 @@ private fun TiledList<TimelineQuery, TimelineItem>.filterThreadDuplicates(): Til
252252
.distinctBy(TimelineItem::id)
253253
}
254254

255-
private val EMPTY_STATE_DELAY = 1.4.seconds
255+
private val EMPTY_STATE_DELAY = 2.2.seconds

0 commit comments

Comments
 (0)