chinwag-android/app/src/main/java/com/keylesspalace/tusky/MainViewModel.kt
Konrad Pozniak d0b20cf06e
various not push related notification improvements (#4929)
- support new notification type `severed_relationships`, closes
https://github.com/tuskyapp/Tusky/issues/4835, closes
https://github.com/tuskyapp/Tusky/issues/4334
- support new notification type `moderation_warning`
- the account note is now shown again for follow request and follow
notifcations (was broken since
https://github.com/tuskyapp/Tusky/pull/4026)
- closes https://github.com/tuskyapp/Tusky/issues/4571
- The "unknown notification type" notification now shows the unknown
type and a info dialog when you click it
https://chaos.social/@ConnyDuck/113601791254050485
- The notification policy banner in the notification tab is now cached
for better offline behavior (and less jumping of the list on every load)
and updates when interacting with the requests
- Fixes a bug where some notifications wouldn't be filtered correctly.
Behavior should now match Mastodon.
https://mastodon.social/@alm10965/113639206858728177
- Fixes a bug where some system notifications wouldn't have a body
- For filters and channels, report and signup notifications are now
grouped as "Admin", severed relationship events and moderation warnings
as "other". These lists are super long already.
- The icon for the "`<user>` just posted" notification is now a bell
instead of a home
- Follow requests won't be filtered by default in the notification tab.
No idea why this one got special treatment. This change will only affect
new logins and not existing ones.
- closes #4440 
- Adds info about attached media or poll to
StatusNotificationViewHolder. This is important context that has been
missing before.
- Adds (private) reply/(private) mention text above mention
notification. (Partially?) closes
https://github.com/tuskyapp/Tusky/issues/3883

Some screenshots:

![follow](https://github.com/user-attachments/assets/5f962116-c16f-4574-aae1-b1f931ce1508)

![moderation_warning](https://github.com/user-attachments/assets/55a2ee7e-ebcd-4ae8-9170-f07f9f5df5d2)

![severed_relationship](https://github.com/user-attachments/assets/a8d6b898-eb44-43b4-9b6d-3fb5f7aeb852)

![unknown](https://github.com/user-attachments/assets/c74ee33e-6926-42b1-b952-dc888b72fd27)

![unknown_info](https://github.com/user-attachments/assets/19ff11bf-aaff-4219-87e2-ea980ebbd118)

![notifications](https://github.com/user-attachments/assets/b5021cbb-f6c0-4a17-9e15-73e669504647)
2025-02-24 14:53:05 +01:00

201 lines
7.7 KiB
Kotlin

/* Copyright 2024 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ShareShortcutHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
class MainViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val api: MastodonApi,
private val eventHub: EventHub,
private val accountManager: AccountManager,
private val shareShortcutHelper: ShareShortcutHelper,
private val notificationService: NotificationService,
) : ViewModel() {
private val activeAccount = accountManager.activeAccount!!
val accounts: StateFlow<List<AccountViewData>> = accountManager.accountsFlow
.map { accounts ->
accounts.map { account ->
AccountViewData(
id = account.id,
domain = account.domain,
username = account.username,
displayName = account.displayName,
profilePictureUrl = account.profilePictureUrl,
profileHeaderUrl = account.profileHeaderUrl,
emojis = account.emojis
)
}
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
val tabs: StateFlow<List<TabData>> = accountManager.activeAccount(viewModelScope)
.mapNotNull { account -> account?.tabPreferences }
.stateIn(viewModelScope, SharingStarted.Eagerly, activeAccount.tabPreferences)
private val _unreadAnnouncementsCount = MutableStateFlow(0)
val unreadAnnouncementsCount: StateFlow<Int> = _unreadAnnouncementsCount.asStateFlow()
val showDirectMessagesBadge: StateFlow<Boolean> = accountManager.activeAccount(viewModelScope)
.map { account -> account?.hasDirectMessageBadge == true }
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
init {
loadAccountData()
fetchAnnouncements()
collectEvents()
}
private fun loadAccountData() {
viewModelScope.launch {
api.accountVerifyCredentials().fold(
{ userInfo ->
accountManager.updateAccount(activeAccount, userInfo)
shareShortcutHelper.updateShortcuts()
setupNotifications(activeAccount)
},
{ throwable ->
Log.e(TAG, "Failed to fetch user info.", throwable)
}
)
}
}
private fun fetchAnnouncements() {
viewModelScope.launch {
api.announcements()
.fold(
{ announcements ->
_unreadAnnouncementsCount.value = announcements.count { !it.read }
},
{ throwable ->
Log.w(TAG, "Failed to fetch announcements.", throwable)
}
)
}
}
private fun collectEvents() {
viewModelScope.launch {
eventHub.events.collect { event ->
when (event) {
is AnnouncementReadEvent -> {
_unreadAnnouncementsCount.value--
}
is NewNotificationsEvent -> {
if (event.accountId == activeAccount.accountId) {
val hasDirectMessageNotification =
event.notifications.any {
it.type == Notification.Type.Mention && it.status?.visibility == Status.Visibility.DIRECT
}
if (hasDirectMessageNotification) {
accountManager.updateAccount(activeAccount) { copy(hasDirectMessageBadge = true) }
}
}
}
is NotificationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
accountManager.updateAccount(activeAccount) { copy(hasDirectMessageBadge = false) }
}
}
is ConversationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
accountManager.updateAccount(activeAccount) { copy(hasDirectMessageBadge = false) }
}
}
}
}
}
}
fun dismissDirectMessagesBadge() {
viewModelScope.launch {
accountManager.updateAccount(activeAccount) { copy(hasDirectMessageBadge = false) }
}
}
fun setupNotifications(account: AccountEntity? = null) {
// TODO this is only called on full app (re) start; so changes in-between (push distributor uninstalled/subscription changed, or
// notifications fully disabled) will get unnoticed; and also an app restart cannot be easily triggered by the user.
if (account != null) {
// TODO it's quite odd to separate channel creation (for an account) from the "is enabled by channels" question below
notificationService.createNotificationChannelsForAccount(account)
}
if (notificationService.areNotificationsEnabledBySystem()) {
viewModelScope.launch {
notificationService.setupNotifications(account)
}
} else {
viewModelScope.launch {
notificationService.disableAllNotifications()
}
}
}
companion object {
private const val TAG = "MainViewModel"
}
}
data class AccountViewData(
val id: Long,
val domain: String,
val username: String,
val displayName: String,
val profilePictureUrl: String,
val profileHeaderUrl: String,
val emojis: List<Emoji>
) {
val fullName: String
get() = "@$username@$domain"
}