- 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:      
201 lines
7.7 KiB
Kotlin
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"
|
|
}
|