Skip to content

Commit

Permalink
3532: Show badge on conversations tab on new conversations
Browse files Browse the repository at this point in the history
  • Loading branch information
Lakoja committed Jul 27, 2023
1 parent ee71159 commit 11b8d48
Show file tree
Hide file tree
Showing 9 changed files with 1,125 additions and 6 deletions.
1,016 changes: 1,016 additions & 0 deletions app/schemas/com.keylesspalace.tusky.db.AppDatabase/53.json

Large diffs are not rendered by default.

61 changes: 60 additions & 1 deletion app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.CacheUpdater
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
Expand All @@ -83,6 +86,7 @@ import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.FabFragment
Expand Down Expand Up @@ -176,6 +180,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
/** Adapter for the different timeline tabs */
private lateinit var tabAdapter: MainPagerAdapter

private var directMessageTab: TabLayout.Tab? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -294,11 +300,32 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje

setupTabs(false)
}

is AnnouncementReadEvent -> {
unreadAnnouncementsCount--
updateAnnouncementsBadge()
}
is NewNotificationsEvent -> {
directMessageTab?.let { tab ->
if (event.accountId == accountManager.activeAccount?.accountId) {
val hasDirectMessageNotification =
event.notifications.any { it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT }

if (hasDirectMessageNotification) {
showDirectMessageBadge(true)
}
}
}
}
is NotificationsLoadingEvent -> {
if (event.accountId == accountManager.activeAccount?.accountId) {
showDirectMessageBadge(false)
}
}
is ConversationsLoadingEvent -> {
if (event.accountId == accountManager.activeAccount?.accountId) {
showDirectMessageBadge(false)
}
}
}
}
}
Expand Down Expand Up @@ -341,6 +368,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
draftsAlert.observeInContext(this, true)
}

private fun showDirectMessageBadge(showBadge: Boolean) {
directMessageTab?.let { tab ->
tab.badge?.isVisible = showBadge

// TODO a bit cumbersome (also for resetting)
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge != showBadge) {
it.hasDirectMessageBadge = showBadge
accountManager.saveAccount(it)
}
}
}
}

override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_main, menu)
menu.findItem(R.id.action_search)?.apply {
Expand Down Expand Up @@ -700,6 +741,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Detach any existing mediator before changing tab contents and attaching a new mediator
tabLayoutMediator?.detach()

directMessageTab = null

tabAdapter.tabs = tabs
tabAdapter.notifyItemRangeChanged(0, tabs.size)

Expand All @@ -710,6 +753,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LIST -> tabs[position].arguments[1]
else -> getString(tabs[position].text)
}
if (tabs[position].id == DIRECT) {
tab.orCreateBadge
tab.badge?.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
directMessageTab = tab
}
}.also { it.attach() }

// Selected tab is either
Expand Down Expand Up @@ -738,6 +786,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
binding.mainToolbar.title = tab.contentDescription

refreshComposeButtonState(tabAdapter, tab.position)

if (tab == directMessageTab) {
tab.badge?.isVisible = false

accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge) {
it.hasDirectMessageBadge = false
accountManager.saveAccount(it)
}
}
}
}

override fun onTabUnselected(tab: TabLayout.Tab) {}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.keylesspalace.tusky.appstore

import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status

Expand All @@ -23,3 +24,6 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
data class DomainMuteEvent(val instance: String) : Event
data class AnnouncementReadEvent(val announcementId: String) : Event
data class PinEvent(val statusId: String, val pinned: Boolean) : Event
data class NewNotificationsEvent(val accountId: String, val notifications: List<Notification>) : Event
data class ConversationsLoadingEvent(val accountId: String) : Event
data class NotificationsLoadingEvent(val accountId: String) : Event
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.keylesspalace.tusky.components.conversation

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
Expand All @@ -40,6 +41,7 @@ import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.account.AccountActivity
Expand All @@ -54,6 +56,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.isAnyLoading
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData
Expand All @@ -64,6 +67,7 @@ import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
Expand Down Expand Up @@ -128,6 +132,12 @@ class ConversationsFragment :
binding.statusView.hide()
binding.progressBar.hide()

if (loadState.isAnyLoading()) {
runBlocking {
eventHub.dispatch(ConversationsLoadingEvent(accountManager.activeAccount?.accountId ?: ""))
}
}

if (adapter.itemCount == 0) {
when (loadState.refresh) {
is LoadState.NotLoading -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.annotation.WorkerThread
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.components.notifications.NotificationHelper.filterNotification
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
Expand All @@ -28,7 +30,8 @@ import kotlin.time.Duration.Companion.milliseconds
class NotificationFetcher @Inject constructor(
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val context: Context
private val context: Context,
private val eventHub: EventHub
) {
suspend fun fetchAndShow() {
for (account in accountManager.getAllAccountsOrderedByActive()) {
Expand All @@ -42,6 +45,10 @@ class NotificationFetcher @Inject constructor(
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
.toMutableList()

// TODO do this before filter above? But one could argue that (for example) a tab badge is also a notification
// (and should therefore adhere to the notification config).
eventHub.dispatch(NewNotificationsEvent(account.accountId, notifications))

// There's a maximum limit on the number of notifications an Android app
// can display. If the total number of notifications (current notifications,
// plus new ones) exceeds this then some newer notifications will be dropped.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
Expand All @@ -62,6 +64,7 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.isAnyLoading
import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list
Expand All @@ -78,6 +81,7 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.IOException
import javax.inject.Inject

Expand All @@ -98,6 +102,9 @@ class NotificationsFragment :

private val binding by viewBinding(FragmentTimelineNotificationsBinding::bind)

@Inject
lateinit var eventHub: EventHub

private lateinit var adapter: NotificationsPagingAdapter

private lateinit var layoutManager: LinearLayoutManager
Expand Down Expand Up @@ -392,7 +399,6 @@ class NotificationsFragment :

// Update the UI from the loadState
adapter.loadStateFlow
.distinctUntilChangedBy { it.refresh }
.collect { loadState ->
binding.recyclerView.isVisible = true
binding.progressBar.isVisible = loadState.refresh is LoadState.Loading &&
Expand Down Expand Up @@ -432,6 +438,12 @@ class NotificationsFragment :
binding.recyclerView.isVisible = false
binding.statusView.isVisible = true
}

if (loadState.isAnyLoading()) {
runBlocking {
eventHub.dispatch(NotificationsLoadingEvent(accountManager.activeAccount?.accountId ?: ""))
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.keylesspalace.tusky.db

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
Expand Down Expand Up @@ -104,7 +105,10 @@ data class AccountEntity(

/** true if the connected Mastodon account is locked (has to manually approve all follow requests **/
@ColumnInfo(defaultValue = "0")
var locked: Boolean = false
var locked: Boolean = false,

@ColumnInfo(defaultValue = "0")
var hasDirectMessageBadge: Boolean = false
) {

val identifier: String
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@
TimelineAccountEntity.class,
ConversationEntity.class
},
version = 52,
version = 53,
autoMigrations = {
@AutoMigration(from = 48, to = 49),
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),
@AutoMigration(from = 50, to = 51),
@AutoMigration(from = 51, to = 52)
@AutoMigration(from = 51, to = 52),
@AutoMigration(from = 52, to = 53) // hasDirectMessageBadge in AccountEntity
}
)
public abstract class AppDatabase extends RoomDatabase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util

import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TrendingTag
Expand Down Expand Up @@ -86,3 +88,7 @@ fun List<TrendingTag>.toViewData(): List<TrendingViewData.Tag> {
)
}
}

fun CombinedLoadStates.isAnyLoading(): Boolean {
return this.refresh == LoadState.Loading || this.append == LoadState.Loading || this.prepend == LoadState.Loading
}

0 comments on commit 11b8d48

Please sign in to comment.