Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add notification badge support #492

Merged
merged 1 commit into from
Sep 26, 2024
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

Large diffs are not rendered by default.

716 changes: 302 additions & 414 deletions app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import dev.dimension.flare.ui.model.flatMap
import dev.dimension.flare.ui.model.map
import dev.dimension.flare.ui.model.onError
import dev.dimension.flare.ui.model.onSuccess
import dev.dimension.flare.ui.presenter.home.NotificationBadgePresenter
import dev.dimension.flare.ui.presenter.home.UserPresenter
import dev.dimension.flare.ui.presenter.home.UserState
import dev.dimension.flare.ui.presenter.invoke
Expand Down Expand Up @@ -354,6 +355,10 @@ internal fun timelineItemPresenter(timelineTabItem: TimelineTabItem): TimelineIt
remember(timelineTabItem) {
timelineTabItem.createPresenter()
}
val badge =
remember(timelineTabItem) {
NotificationBadgePresenter(timelineTabItem.account)
}.invoke()
val scope = rememberCoroutineScope()
val state = timelinePresenter()
var showNewToots by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -399,6 +404,7 @@ internal fun timelineItemPresenter(timelineTabItem: TimelineTabItem): TimelineIt
scope.launch {
state.refresh()
}
badge.refresh()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ private fun presenter(
it
.filterIsInstance<TimelineTabItem>()
.filter { tab ->
tab !in cacheTabs
cacheTabs.none { it.key == tab.key }
}.toImmutableList()
}
var showAddTab by remember { mutableStateOf(false) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ class MastodonDataSource(
database,
account.accountKey,
pagingKey,
onClearMarker = {
MemCacheable.update(notificationMarkerKey, 0)
},
)

NotificationFilter.Mention ->
Expand Down Expand Up @@ -1147,4 +1150,18 @@ class MastodonDataSource(
) {
updateList(listId, metaData.title)
}

private val notificationMarkerKey: String
get() = "notificationBadgeCount_${account.accountKey}"

override fun notificationBadgeCount(): CacheData<Int> {
return MemCacheable(
key = notificationMarkerKey,
fetchSource = {
val marker = service.notificationMarkers().notifications?.lastReadID ?: return@MemCacheable 0
val timeline = service.notification(min_id = marker)
timeline.size
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import dev.dimension.flare.data.database.cache.CacheDatabase
import dev.dimension.flare.data.database.cache.mapper.Mastodon
import dev.dimension.flare.data.database.cache.model.DbPagingTimelineView
import dev.dimension.flare.data.network.mastodon.MastodonService
import dev.dimension.flare.data.network.mastodon.api.model.MarkerUpdate
import dev.dimension.flare.data.network.mastodon.api.model.UpdateContent
import dev.dimension.flare.model.MicroBlogKey

@OptIn(ExperimentalPagingApi::class)
Expand All @@ -16,6 +18,7 @@ internal class NotificationRemoteMediator(
private val database: CacheDatabase,
private val accountKey: MicroBlogKey,
private val pagingKey: String,
private val onClearMarker: () -> Unit,
) : RemoteMediator<Int, DbPagingTimelineView>() {
override suspend fun load(
loadType: LoadType,
Expand All @@ -30,6 +33,10 @@ internal class NotificationRemoteMediator(
limit = state.config.pageSize,
).also {
database.pagingTimelineDao().delete(pagingKey = pagingKey, accountKey = accountKey)
it.firstOrNull()?.id?.let { it1 ->
service.updateMarker(MarkerUpdate(notifications = UpdateContent(it1)))
onClearMarker.invoke()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.dimension.flare.data.datasource.microblog

import androidx.paging.PagingData
import dev.dimension.flare.common.CacheData
import dev.dimension.flare.common.Cacheable
import dev.dimension.flare.model.MicroBlogKey
import dev.dimension.flare.ui.model.UiAccount
import dev.dimension.flare.ui.model.UiHashtag
Expand All @@ -12,6 +13,7 @@ import dev.dimension.flare.ui.model.UiTimeline
import dev.dimension.flare.ui.model.UiUserV2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

interface MicroblogDataSource {
val account: UiAccount
Expand Down Expand Up @@ -92,6 +94,8 @@ interface MicroblogDataSource {
userKey: MicroBlogKey,
relation: UiRelation,
)

fun notificationBadgeCount(): CacheData<Int> = Cacheable({ }, { flowOf(0) })
}

data class ComposeProgress(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
package dev.dimension.flare.data.network.mastodon.api

import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.POST
import dev.dimension.flare.data.network.mastodon.api.model.Emoji
import dev.dimension.flare.data.network.mastodon.api.model.Marker
import dev.dimension.flare.data.network.mastodon.api.model.MarkerUpdate

internal interface MastodonResources {
@GET("api/v1/custom_emojis")
suspend fun emojis(): List<Emoji>

@GET("api/v1/markers?timeline[]=notifications")
suspend fun notificationMarkers(): Marker

@POST("api/v1/markers")
suspend fun updateMarker(
@Body data: MarkerUpdate,
@Header("Content-Type") contentType: String = "application/json",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,29 @@ internal data class Emoji(
val visibleInPicker: Boolean? = null,
val category: String? = null,
)

@Serializable
data class Marker(
val notifications: Notifications? = null,
)

@Serializable
data class Notifications(
@SerialName("last_read_id")
val lastReadID: String? = null,
val version: Long? = null,
@SerialName("updated_at")
val updatedAt: String? = null,
)

@Serializable
data class MarkerUpdate(
val home: UpdateContent? = null,
val notifications: UpdateContent? = null,
)

@Serializable
data class UpdateContent(
@SerialName("last_read_id")
val lastReadID: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dev.dimension.flare.ui.presenter.home

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import dev.dimension.flare.common.collectAsState
import dev.dimension.flare.data.repository.accountServiceProvider
import dev.dimension.flare.model.AccountType
import dev.dimension.flare.ui.model.UiState
import dev.dimension.flare.ui.model.flatMap
import dev.dimension.flare.ui.model.map
import dev.dimension.flare.ui.model.onSuccess
import dev.dimension.flare.ui.model.toUi
import dev.dimension.flare.ui.presenter.PresenterBase
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.minutes

class NotificationBadgePresenter(
private val accountType: AccountType,
private val autoRefresh: Boolean = true,
) : PresenterBase<NotificationBadgeState>() {
@Composable
override fun body(): NotificationBadgeState {
val serviceState = accountServiceProvider(accountType = accountType)
val countState =
serviceState.map { service ->
remember(service) {
service.notificationBadgeCount()
}.collectAsState()
}
if (autoRefresh) {
countState.onSuccess {
LaunchedEffect(Unit) {
delay(1.minutes)
it.refresh()
}
}
}
return object : NotificationBadgeState {
override val count: UiState<Int> = countState.flatMap { it.toUi() }

override fun refresh() {
countState.onSuccess {
it.refresh()
}
}
}
}
}

interface NotificationBadgeState {
val count: UiState<Int>

fun refresh()
}
Loading