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:      
This commit is contained in:
parent
1157be18cf
commit
d0b20cf06e
60 changed files with 2569 additions and 402 deletions
|
|
@ -446,7 +446,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
} else if (accountRequested && intent.hasExtra(NOTIFICATION_TYPE)) {
|
||||
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
|
||||
// otherwise show notification tab
|
||||
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
|
||||
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FollowRequest.name) {
|
||||
val accountListIntent = AccountListActivity.newIntent(
|
||||
this,
|
||||
AccountListActivity.Type.FOLLOW_REQUESTS
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class MainViewModel @Inject constructor(
|
|||
if (event.accountId == activeAccount.accountId) {
|
||||
val hasDirectMessageNotification =
|
||||
event.notifications.any {
|
||||
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
|
||||
it.type == Notification.Type.Mention && it.status?.visibility == Status.Visibility.DIRECT
|
||||
}
|
||||
|
||||
if (hasDirectMessageNotification) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
|
|
@ -53,6 +55,9 @@ class TuskyApplication : Application(), Configuration.Provider {
|
|||
@Inject
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var notificationManager: NotificationManager
|
||||
|
||||
override fun onCreate() {
|
||||
// Uncomment me to get StrictMode violation logs
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
|
|
@ -80,6 +85,14 @@ class TuskyApplication : Application(), Configuration.Provider {
|
|||
// A new periodic work request is enqueued by unique name (and not tag anymore): stop the old one
|
||||
workManager.cancelAllWorkByTag("pullNotifications")
|
||||
}
|
||||
if (oldVersion < 2025022001 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// delete old now unused notification channels
|
||||
for (channel in notificationManager.notificationChannels) {
|
||||
if (channel.id.startsWith("CHANNEL_SIGN_UP") || channel.id.startsWith("CHANNEL_REPORT")) {
|
||||
notificationManager.deleteNotificationChannel(channel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,5 +119,6 @@ class FollowRequestViewHolder(
|
|||
}
|
||||
}
|
||||
itemView.setOnClickListener { listener.onViewAccount(accountId) }
|
||||
binding.accountNote.setOnClickListener { listener.onViewAccount(accountId) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import android.widget.Button;
|
|||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
|
@ -40,9 +39,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
|
||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||
|
|
@ -134,16 +130,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
statusInfo.setText(emojifiedText);
|
||||
statusInfo.setCompoundDrawablesWithIntrinsicBounds(isReply ? R.drawable.ic_reply_all_18dp : R.drawable.ic_reblog_18dp, 0, 0, 0);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// don't use this on the same ViewHolder as setStatusInfoContent, will cause recycling issues as paddings are changed
|
||||
protected void setPollInfo(final boolean ownPoll) {
|
||||
statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted);
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0);
|
||||
statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10));
|
||||
statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.getContext(), 28), 0, 0, 0);
|
||||
statusInfo.setCompoundDrawablesWithIntrinsicBounds(isReply ? R.drawable.ic_reply_18dp : R.drawable.ic_reblog_18dp, 0, 0, 0);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +146,10 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
statusInfo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
protected TextView getStatusInfo() {
|
||||
return statusInfo;
|
||||
}
|
||||
|
||||
private void setupCollapsedState(boolean sensitive,
|
||||
boolean expanded,
|
||||
final StatusViewData.Concrete status,
|
||||
|
|
|
|||
|
|
@ -20,15 +20,22 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.databinding.ItemFollowBinding
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
||||
class FollowViewHolder(
|
||||
private val binding: ItemFollowBinding,
|
||||
private val listener: AccountActionListener,
|
||||
private val linkListener: LinkListener
|
||||
) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder {
|
||||
|
||||
override fun bind(
|
||||
|
|
@ -42,7 +49,7 @@ class FollowViewHolder(
|
|||
val context = itemView.context
|
||||
val account = viewData.account
|
||||
val messageTemplate =
|
||||
context.getString(if (viewData.type == Notification.Type.SIGN_UP) R.string.notification_sign_up_format else R.string.notification_follow_format)
|
||||
context.getString(if (viewData.type == Notification.Type.SignUp) R.string.notification_sign_up_format else R.string.notification_follow_format)
|
||||
val wrappedDisplayName = account.name.unicodeWrap()
|
||||
|
||||
binding.notificationText.text = messageTemplate.format(wrappedDisplayName)
|
||||
|
|
@ -57,16 +64,28 @@ class FollowViewHolder(
|
|||
)
|
||||
binding.notificationDisplayName.text = emojifiedDisplayName
|
||||
|
||||
if (account.note.isEmpty()) {
|
||||
binding.accountNote.hide()
|
||||
} else {
|
||||
binding.accountNote.show()
|
||||
|
||||
val emojifiedNote = account.note.parseAsMastodonHtml()
|
||||
.emojify(account.emojis, binding.accountNote, statusDisplayOptions.animateEmojis)
|
||||
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener)
|
||||
}
|
||||
|
||||
val avatarRadius = context.resources
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
binding.notificationAvatar,
|
||||
avatarRadius,
|
||||
statusDisplayOptions.animateAvatars,
|
||||
null
|
||||
statusDisplayOptions.animateAvatars
|
||||
)
|
||||
|
||||
binding.avatarBadge.visible(statusDisplayOptions.showBotOverlay && account.bot)
|
||||
|
||||
itemView.setOnClickListener { listener.onViewAccount(account.id) }
|
||||
binding.accountNote.setOnClickListener { listener.onViewAccount(account.id) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/* Copyright 2025 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.components.notifications
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.databinding.ItemModerationWarningNotificationBinding
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
||||
class ModerationWarningViewHolder(
|
||||
private val binding: ItemModerationWarningNotificationBinding,
|
||||
private val instanceDomain: String
|
||||
) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder {
|
||||
|
||||
override fun bind(
|
||||
viewData: NotificationViewData.Concrete,
|
||||
payloads: List<*>,
|
||||
statusDisplayOptions: StatusDisplayOptions
|
||||
) {
|
||||
if (payloads.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
val warning = viewData.moderationWarning!!
|
||||
|
||||
binding.moderationWarningDescription.setText(warning.action.text)
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://$instanceDomain/disputes/strikes/${warning.id}".toUri())
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemFilteredNotificationsInfoBinding
|
||||
import com.keylesspalace.tusky.usecase.NotificationPolicyState
|
||||
import com.keylesspalace.tusky.db.entity.NotificationPolicyEntity
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import java.text.NumberFormat
|
||||
|
||||
|
|
@ -28,9 +28,9 @@ class NotificationPolicySummaryAdapter(
|
|||
private val onOpenDetails: () -> Unit
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemFilteredNotificationsInfoBinding>>() {
|
||||
|
||||
private var state: NotificationPolicyState = NotificationPolicyState.Loading
|
||||
private var state: NotificationPolicyEntity? = null
|
||||
|
||||
fun updateState(newState: NotificationPolicyState) {
|
||||
fun updateState(newState: NotificationPolicyEntity?) {
|
||||
val oldShowInfo = state.shouldShowInfo()
|
||||
val newShowInfo = newState.shouldShowInfo()
|
||||
state = newState
|
||||
|
|
@ -58,16 +58,15 @@ class NotificationPolicySummaryAdapter(
|
|||
override fun getItemCount() = if (state.shouldShowInfo()) 1 else 0
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemFilteredNotificationsInfoBinding>, position: Int) {
|
||||
val policySummary = (state as? NotificationPolicyState.Loaded)?.policy?.summary
|
||||
if (policySummary != null) {
|
||||
state?.let { policyState ->
|
||||
val binding = holder.binding
|
||||
val context = holder.binding.root.context
|
||||
binding.notificationPolicySummaryDescription.text = context.getString(R.string.notifications_from_people_you_may_know, policySummary.pendingRequestsCount)
|
||||
binding.notificationPolicySummaryBadge.text = NumberFormat.getInstance().format(policySummary.pendingNotificationsCount)
|
||||
binding.notificationPolicySummaryDescription.text = context.getString(R.string.notifications_from_people_you_may_know, policyState.pendingRequestsCount)
|
||||
binding.notificationPolicySummaryBadge.text = NumberFormat.getInstance().format(policyState.pendingNotificationsCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationPolicyState.shouldShowInfo(): Boolean {
|
||||
return this is NotificationPolicyState.Loaded && this.policy.summary.pendingNotificationsCount > 0
|
||||
private fun NotificationPolicyEntity?.shouldShowInfo(): Boolean {
|
||||
return this != null && this.pendingNotificationsCount > 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ fun Placeholder.toNotificationEntity(
|
|||
accountId = null,
|
||||
statusId = null,
|
||||
reportId = null,
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
loading = loading
|
||||
)
|
||||
|
||||
|
|
@ -50,6 +52,8 @@ fun Notification.toEntity(
|
|||
accountId = account.id,
|
||||
statusId = status?.reblog?.id ?: status?.id,
|
||||
reportId = report?.id,
|
||||
event = event,
|
||||
moderationWarning = moderationWarning,
|
||||
loading = false
|
||||
)
|
||||
|
||||
|
|
@ -66,7 +70,9 @@ fun Notification.toViewData(
|
|||
isExpanded = isExpanded,
|
||||
isCollapsed = isCollapsed
|
||||
),
|
||||
report = report
|
||||
report = report,
|
||||
moderationWarning = moderationWarning,
|
||||
event = event
|
||||
)
|
||||
|
||||
fun Report.toEntity(
|
||||
|
|
@ -106,7 +112,9 @@ fun NotificationDataEntity.toViewData(
|
|||
report.toReport(reportTargetAccount)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
event = event,
|
||||
moderationWarning = moderationWarning
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.components.notifications.requests.NotificationRequestsActivity
|
||||
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
|
||||
import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding
|
||||
import com.keylesspalace.tusky.databinding.NotificationsFilterBinding
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
|
|
@ -115,9 +115,11 @@ class NotificationsFragment :
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
val activeAccount = accountManager.activeAccount ?: return
|
||||
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||
mediaPreviewEnabled = activeAccount.mediaPreviewEnabled,
|
||||
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
|
||||
showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
|
||||
useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
|
||||
|
|
@ -131,8 +133,8 @@ class NotificationsFragment :
|
|||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
||||
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
|
||||
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
|
||||
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
||||
showSensitiveMedia = activeAccount.alwaysShowSensitiveMedia,
|
||||
openSpoiler = activeAccount.alwaysOpenSpoiler
|
||||
)
|
||||
|
||||
binding.recyclerView.ensureBottomPadding(fab = true)
|
||||
|
|
@ -149,11 +151,12 @@ class NotificationsFragment :
|
|||
// Setup the RecyclerView.
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
val adapter = NotificationsPagingAdapter(
|
||||
accountId = accountManager.activeAccount!!.accountId,
|
||||
accountId = activeAccount.accountId,
|
||||
statusListener = this,
|
||||
notificationActionListener = this,
|
||||
accountActionListener = this,
|
||||
statusDisplayOptions = statusDisplayOptions
|
||||
statusDisplayOptions = statusDisplayOptions,
|
||||
instanceName = activeAccount.domain
|
||||
)
|
||||
this.notificationsAdapter = adapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
|
|
@ -447,8 +450,8 @@ class NotificationsFragment :
|
|||
}
|
||||
|
||||
private fun showFilterMenu() {
|
||||
val notificationTypeList = Notification.Type.visibleTypes.map { type ->
|
||||
getString(type.uiString)
|
||||
val notificationTypeList = NotificationChannelData.entries.map { type ->
|
||||
getString(type.title)
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_multiple_choice, notificationTypeList)
|
||||
|
|
@ -457,7 +460,7 @@ class NotificationsFragment :
|
|||
|
||||
menuBinding.buttonApply.setOnClickListener {
|
||||
val checkedItems = menuBinding.listView.getCheckedItemPositions()
|
||||
val excludes = Notification.Type.visibleTypes.filterIndexed { index, _ ->
|
||||
val excludes = NotificationChannelData.entries.filterIndexed { index, _ ->
|
||||
!checkedItems[index, false]
|
||||
}
|
||||
window.dismiss()
|
||||
|
|
@ -467,7 +470,7 @@ class NotificationsFragment :
|
|||
menuBinding.listView.setAdapter(adapter)
|
||||
menuBinding.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE)
|
||||
|
||||
Notification.Type.visibleTypes.forEachIndexed { index, type ->
|
||||
NotificationChannelData.entries.forEachIndexed { index, type ->
|
||||
menuBinding.listView.setItemChecked(index, !viewModel.excludes.value.contains(type))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import com.keylesspalace.tusky.adapter.PlaceholderViewHolder
|
|||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemModerationWarningNotificationBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemSeveredRelationshipNotificationBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
|
||||
|
|
@ -57,7 +59,8 @@ class NotificationsPagingAdapter(
|
|||
private var statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusListener: StatusActionListener,
|
||||
private val notificationActionListener: NotificationActionListener,
|
||||
private val accountActionListener: AccountActionListener
|
||||
private val accountActionListener: AccountActionListener,
|
||||
private val instanceName: String
|
||||
) : PagingDataAdapter<NotificationViewData, RecyclerView.ViewHolder>(NotificationsDifferCallback) {
|
||||
|
||||
var mediaPreviewEnabled: Boolean
|
||||
|
|
@ -79,20 +82,26 @@ class NotificationsPagingAdapter(
|
|||
return when (val notification = getItem(position)) {
|
||||
is NotificationViewData.Concrete -> {
|
||||
when (notification.type) {
|
||||
Notification.Type.MENTION,
|
||||
Notification.Type.POLL -> if (notification.statusViewData?.filterAction == Filter.Action.WARN) {
|
||||
Notification.Type.Mention,
|
||||
Notification.Type.Poll -> if (notification.statusViewData?.filterAction == Filter.Action.WARN) {
|
||||
VIEW_TYPE_STATUS_FILTERED
|
||||
} else {
|
||||
VIEW_TYPE_STATUS
|
||||
}
|
||||
Notification.Type.STATUS,
|
||||
Notification.Type.FAVOURITE,
|
||||
Notification.Type.REBLOG,
|
||||
Notification.Type.UPDATE -> VIEW_TYPE_STATUS_NOTIFICATION
|
||||
Notification.Type.FOLLOW,
|
||||
Notification.Type.SIGN_UP -> VIEW_TYPE_FOLLOW
|
||||
Notification.Type.FOLLOW_REQUEST -> VIEW_TYPE_FOLLOW_REQUEST
|
||||
Notification.Type.REPORT -> VIEW_TYPE_REPORT
|
||||
Notification.Type.Status,
|
||||
Notification.Type.Update -> if (notification.statusViewData?.filterAction == Filter.Action.WARN) {
|
||||
VIEW_TYPE_STATUS_FILTERED
|
||||
} else {
|
||||
VIEW_TYPE_STATUS_NOTIFICATION
|
||||
}
|
||||
Notification.Type.Favourite,
|
||||
Notification.Type.Reblog -> VIEW_TYPE_STATUS_NOTIFICATION
|
||||
Notification.Type.Follow,
|
||||
Notification.Type.SignUp -> VIEW_TYPE_FOLLOW
|
||||
Notification.Type.FollowRequest -> VIEW_TYPE_FOLLOW_REQUEST
|
||||
Notification.Type.Report -> VIEW_TYPE_REPORT
|
||||
Notification.Type.SeveredRelationship -> VIEW_TYPE_SEVERED_RELATIONSHIP
|
||||
Notification.Type.ModerationWarning -> VIEW_TYPE_MODERATION_WARNING
|
||||
else -> VIEW_TYPE_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +128,8 @@ class NotificationsPagingAdapter(
|
|||
)
|
||||
VIEW_TYPE_FOLLOW -> FollowViewHolder(
|
||||
ItemFollowBinding.inflate(inflater, parent, false),
|
||||
accountActionListener
|
||||
accountActionListener,
|
||||
statusListener
|
||||
)
|
||||
VIEW_TYPE_FOLLOW_REQUEST -> FollowRequestViewHolder(
|
||||
ItemFollowRequestBinding.inflate(inflater, parent, false),
|
||||
|
|
@ -136,6 +146,14 @@ class NotificationsPagingAdapter(
|
|||
notificationActionListener,
|
||||
accountActionListener
|
||||
)
|
||||
VIEW_TYPE_SEVERED_RELATIONSHIP -> SeveredRelationshipNotificationViewHolder(
|
||||
ItemSeveredRelationshipNotificationBinding.inflate(inflater, parent, false),
|
||||
instanceName
|
||||
)
|
||||
VIEW_TYPE_MODERATION_WARNING -> ModerationWarningViewHolder(
|
||||
ItemModerationWarningNotificationBinding.inflate(inflater, parent, false),
|
||||
instanceName
|
||||
)
|
||||
else -> UnknownNotificationViewHolder(
|
||||
ItemUnknownNotificationBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
|
@ -166,7 +184,9 @@ class NotificationsPagingAdapter(
|
|||
private const val VIEW_TYPE_FOLLOW_REQUEST = 4
|
||||
private const val VIEW_TYPE_PLACEHOLDER = 5
|
||||
private const val VIEW_TYPE_REPORT = 6
|
||||
private const val VIEW_TYPE_UNKNOWN = 7
|
||||
private const val VIEW_TYPE_SEVERED_RELATIONSHIP = 7
|
||||
private const val VIEW_TYPE_MODERATION_WARNING = 8
|
||||
private const val VIEW_TYPE_UNKNOWN = 9
|
||||
|
||||
val NotificationsDifferCallback = object : DiffUtil.ItemCallback<NotificationViewData>() {
|
||||
override fun areItemsTheSame(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.paging.LoadType
|
|||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import com.keylesspalace.tusky.components.systemnotifications.toTypes
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||
|
|
@ -57,7 +58,7 @@ class NotificationsRemoteMediator(
|
|||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
val excludes = viewModel.excludes.value
|
||||
val excludes = viewModel.excludes.value.toTypes()
|
||||
|
||||
try {
|
||||
var dbEmpty = false
|
||||
|
|
|
|||
|
|
@ -34,18 +34,20 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.FilterUpdatedEvent
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.components.systemnotifications.toTypes
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.entity.NotificationPolicyEntity
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.usecase.NotificationPolicyState
|
||||
import com.keylesspalace.tusky.usecase.NotificationPolicyUsecase
|
||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
|
@ -56,6 +58,7 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -84,7 +87,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
|
||||
private val refreshTrigger = MutableStateFlow(0L)
|
||||
|
||||
val excludes: StateFlow<Set<Notification.Type>> = activeAccountFlow
|
||||
val excludes: StateFlow<Set<NotificationChannelData>> = activeAccountFlow
|
||||
.map { account -> account?.notificationsFilter.orEmpty() }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, activeAccountFlow.value?.notificationsFilter.orEmpty())
|
||||
|
||||
|
|
@ -119,7 +122,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
val notificationPolicy: StateFlow<NotificationPolicyState> = notificationPolicyUsecase.state
|
||||
val notificationPolicy: Flow<NotificationPolicyEntity?> = notificationPolicyUsecase.info
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -148,7 +151,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun updateNotificationFilters(newFilters: Set<Notification.Type>) {
|
||||
fun updateNotificationFilters(newFilters: Set<NotificationChannelData>) {
|
||||
val account = activeAccountFlow.value
|
||||
if (newFilters != excludes.value && account != null) {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -163,7 +166,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
|
||||
private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action {
|
||||
return when ((notificationViewData as? NotificationViewData.Concrete)?.type) {
|
||||
Notification.Type.MENTION, Notification.Type.POLL -> {
|
||||
Notification.Type.Mention, Notification.Type.Poll, Notification.Type.Status, Notification.Type.Update -> {
|
||||
val account = activeAccountFlow.value
|
||||
notificationViewData.statusViewData?.let { statusViewData ->
|
||||
if (statusViewData.status.account.id == account?.accountId) {
|
||||
|
|
@ -320,7 +323,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
maxId = idAbovePlaceholder,
|
||||
minId = idBelowPlaceholder,
|
||||
limit = TimelineViewModel.LOAD_AT_ONCE,
|
||||
excludes = excludes.value
|
||||
excludes = excludes.value.toTypes()
|
||||
)
|
||||
// Using sinceId, loads up to LOAD_AT_ONCE statuses immediately before
|
||||
// maxId, and no smaller than minId.
|
||||
|
|
@ -328,7 +331,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
maxId = idAbovePlaceholder,
|
||||
sinceId = idBelowPlaceholder,
|
||||
limit = TimelineViewModel.LOAD_AT_ONCE,
|
||||
excludes = excludes.value
|
||||
excludes = excludes.value.toTypes()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/* Copyright 2025 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.components.notifications
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
|
||||
import com.keylesspalace.tusky.databinding.ItemSeveredRelationshipNotificationBinding
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
||||
class SeveredRelationshipNotificationViewHolder(
|
||||
private val binding: ItemSeveredRelationshipNotificationBinding,
|
||||
private val instanceName: String
|
||||
) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder {
|
||||
|
||||
override fun bind(
|
||||
viewData: NotificationViewData.Concrete,
|
||||
payloads: List<*>,
|
||||
statusDisplayOptions: StatusDisplayOptions
|
||||
) {
|
||||
if (payloads.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
val event = viewData.event!!
|
||||
val context = binding.root.context
|
||||
|
||||
binding.severedRelationshipText.text = NotificationService.severedRelationShipText(
|
||||
context,
|
||||
event,
|
||||
instanceName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
|||
import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
||||
|
|
@ -44,8 +45,10 @@ import com.keylesspalace.tusky.util.SmartLengthInputFilter
|
|||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
|
@ -84,8 +87,8 @@ internal class StatusNotificationViewHolder(
|
|||
setDisplayName(account.name, account.emojis, statusDisplayOptions.animateEmojis)
|
||||
setUsername(account.username)
|
||||
setCreatedAt(createdAt, statusDisplayOptions.useAbsoluteTime)
|
||||
if (viewData.type == Notification.Type.STATUS ||
|
||||
viewData.type == Notification.Type.UPDATE
|
||||
if (viewData.type == Notification.Type.Status ||
|
||||
viewData.type == Notification.Type.Update
|
||||
) {
|
||||
setAvatar(
|
||||
account.avatar,
|
||||
|
|
@ -136,6 +139,7 @@ internal class StatusNotificationViewHolder(
|
|||
binding.notificationContent.visible(show)
|
||||
binding.notificationStatusAvatar.visible(show)
|
||||
binding.notificationNotificationAvatar.visible(show)
|
||||
binding.notificationAttachmentInfo.visible(show)
|
||||
}
|
||||
|
||||
private fun setDisplayName(name: String, emojis: List<Emoji>, animateEmojis: Boolean) {
|
||||
|
|
@ -230,19 +234,19 @@ internal class StatusNotificationViewHolder(
|
|||
val format: String
|
||||
val icon: Drawable?
|
||||
when (type) {
|
||||
Notification.Type.FAVOURITE -> {
|
||||
Notification.Type.Favourite -> {
|
||||
icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange)
|
||||
format = context.getString(R.string.notification_favourite_format)
|
||||
}
|
||||
Notification.Type.REBLOG -> {
|
||||
Notification.Type.Reblog -> {
|
||||
icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.tusky_blue)
|
||||
format = context.getString(R.string.notification_reblog_format)
|
||||
}
|
||||
Notification.Type.STATUS -> {
|
||||
icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.tusky_blue)
|
||||
Notification.Type.Status -> {
|
||||
icon = getIconWithColor(context, R.drawable.ic_notifications_active_24dp, R.color.tusky_blue)
|
||||
format = context.getString(R.string.notification_subscription_format)
|
||||
}
|
||||
Notification.Type.UPDATE -> {
|
||||
Notification.Type.Update -> {
|
||||
icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.tusky_blue)
|
||||
format = context.getString(R.string.notification_update_format)
|
||||
}
|
||||
|
|
@ -330,15 +334,18 @@ internal class StatusNotificationViewHolder(
|
|||
R.string.post_content_warning_show_more
|
||||
)
|
||||
binding.notificationContent.filters = COLLAPSE_INPUT_FILTER
|
||||
binding.notificationAttachmentInfo.hide()
|
||||
} else {
|
||||
binding.buttonToggleNotificationContent.setText(
|
||||
R.string.post_content_warning_show_less
|
||||
)
|
||||
binding.notificationContent.filters = NO_INPUT_FILTER
|
||||
setupAttachmentInfo(statusViewData.status)
|
||||
}
|
||||
} else {
|
||||
binding.buttonToggleNotificationContent.visibility = View.GONE
|
||||
binding.notificationContent.filters = NO_INPUT_FILTER
|
||||
setupAttachmentInfo(statusViewData.status)
|
||||
}
|
||||
val emojifiedText = content.emojify(
|
||||
emojis = emojis,
|
||||
|
|
@ -360,6 +367,22 @@ internal class StatusNotificationViewHolder(
|
|||
binding.notificationContentWarningDescription.text = emojifiedContentWarning
|
||||
}
|
||||
|
||||
private fun setupAttachmentInfo(status: Status) {
|
||||
if (status.attachments.isNotEmpty()) {
|
||||
binding.notificationAttachmentInfo.show()
|
||||
binding.notificationAttachmentInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_attach_file_24dp, 0, 0, 0)
|
||||
val attachmentCount = status.attachments.size
|
||||
val attachmentText = binding.root.context.resources.getQuantityString(R.plurals.media_attachments, attachmentCount, attachmentCount)
|
||||
binding.notificationAttachmentInfo.text = attachmentText
|
||||
} else if (status.poll != null) {
|
||||
binding.notificationAttachmentInfo.show()
|
||||
binding.notificationAttachmentInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0)
|
||||
binding.notificationAttachmentInfo.setText(R.string.poll)
|
||||
} else {
|
||||
binding.notificationAttachmentInfo.hide()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COLLAPSE_INPUT_FILTER: Array<InputFilter> = arrayOf(SmartLengthInputFilter)
|
||||
private val NO_INPUT_FILTER: Array<InputFilter> = arrayOf()
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.view.View
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
||||
internal class StatusViewHolder(
|
||||
|
|
@ -50,11 +54,40 @@ internal class StatusViewHolder(
|
|||
payloads,
|
||||
false
|
||||
)
|
||||
}
|
||||
if (viewData.type == Notification.Type.POLL) {
|
||||
setPollInfo(accountId == viewData.account.id)
|
||||
} else {
|
||||
hideStatusInfo()
|
||||
if (payloads.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (viewData.type == Notification.Type.Poll) {
|
||||
statusInfo.setText(if (accountId == viewData.account.id) R.string.poll_ended_created else R.string.poll_ended_voted)
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0)
|
||||
statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.context, 10))
|
||||
statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.context, 28), 0, 0, 0)
|
||||
statusInfo.show()
|
||||
} else if (viewData.type == Notification.Type.Mention) {
|
||||
statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.context, 6))
|
||||
statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.context, 38), 0, 0, 0)
|
||||
statusInfo.show()
|
||||
if (viewData.statusViewData.status.inReplyToAccountId == accountId) {
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_reply_18dp, 0, 0, 0)
|
||||
|
||||
if (viewData.statusViewData.status.visibility == Status.Visibility.DIRECT) {
|
||||
statusInfo.setText(R.string.notification_info_private_reply)
|
||||
} else {
|
||||
statusInfo.setText(R.string.notification_info_reply)
|
||||
}
|
||||
} else {
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_at_18dp, 0, 0, 0)
|
||||
|
||||
if (viewData.statusViewData.status.visibility == Status.Visibility.DIRECT) {
|
||||
statusInfo.setText(R.string.notification_info_private_mention)
|
||||
} else {
|
||||
statusInfo.setText(R.string.notification_info_mention)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hideStatusInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
|
||||
internal class UnknownNotificationViewHolder(
|
||||
binding: ItemUnknownNotificationBinding,
|
||||
private val binding: ItemUnknownNotificationBinding,
|
||||
) : NotificationsViewHolder, RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
override fun bind(
|
||||
|
|
@ -31,6 +33,13 @@ internal class UnknownNotificationViewHolder(
|
|||
payloads: List<*>,
|
||||
statusDisplayOptions: StatusDisplayOptions
|
||||
) {
|
||||
// nothing to do
|
||||
binding.unknownNotificationType.text = viewData.type.name
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(binding.root.context)
|
||||
.setMessage(R.string.unknown_notification_type_explanation)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.NotificationRequest
|
|||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import java.text.NumberFormat
|
||||
|
||||
class NotificationRequestsAdapter(
|
||||
private val onAcceptRequest: (notificationRequestId: String) -> Unit,
|
||||
|
|
@ -36,6 +37,8 @@ class NotificationRequestsAdapter(
|
|||
private val animateEmojis: Boolean,
|
||||
) : PagingDataAdapter<NotificationRequest, BindingHolder<ItemNotificationRequestBinding>>(NOTIFICATION_REQUEST_COMPARATOR) {
|
||||
|
||||
private val numberFormat: NumberFormat = NumberFormat.getNumberInstance()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
|
|
@ -58,7 +61,7 @@ class NotificationRequestsAdapter(
|
|||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
loadAvatar(account.avatar, binding.notificationRequestAvatar, avatarRadius, animateAvatar)
|
||||
|
||||
binding.notificationRequestBadge.text = notificationRequest.notificationsCount
|
||||
binding.notificationRequestBadge.text = numberFormat.format(notificationRequest.notificationsCount)
|
||||
|
||||
val emojifiedName = account.name.emojify(
|
||||
account.emojis,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.entity.NotificationRequest
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.usecase.NotificationPolicyUsecase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
|
|
@ -39,7 +40,8 @@ import kotlinx.coroutines.launch
|
|||
@HiltViewModel
|
||||
class NotificationRequestsViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
private val eventHub: EventHub,
|
||||
private val notificationPolicyUsecase: NotificationPolicyUsecase
|
||||
) : ViewModel() {
|
||||
|
||||
var currentSource: NotificationRequestsPagingSource? = null
|
||||
|
|
@ -108,6 +110,13 @@ class NotificationRequestsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun removeNotificationRequest(id: String) {
|
||||
requestData.forEach { request ->
|
||||
if (request.id == id) {
|
||||
viewModelScope.launch {
|
||||
notificationPolicyUsecase.updateCounts(request.notificationsCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
requestData.removeAll { request -> request.id == id }
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,9 +102,10 @@ class NotificationRequestDetailsFragment : SFragment(R.layout.fragment_notificat
|
|||
}
|
||||
|
||||
private fun setupAdapter(): NotificationsPagingAdapter {
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||
mediaPreviewEnabled = activeAccount.mediaPreviewEnabled,
|
||||
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
|
||||
showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
|
||||
useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
|
||||
|
|
@ -118,16 +119,17 @@ class NotificationRequestDetailsFragment : SFragment(R.layout.fragment_notificat
|
|||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
||||
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
|
||||
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
|
||||
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
||||
showSensitiveMedia = activeAccount.alwaysShowSensitiveMedia,
|
||||
openSpoiler = activeAccount.alwaysOpenSpoiler
|
||||
)
|
||||
|
||||
return NotificationsPagingAdapter(
|
||||
accountId = accountManager.activeAccount!!.accountId,
|
||||
accountId = activeAccount.accountId,
|
||||
statusDisplayOptions = statusDisplayOptions,
|
||||
statusListener = this,
|
||||
notificationActionListener = this,
|
||||
accountActionListener = this
|
||||
accountActionListener = this,
|
||||
instanceName = activeAccount.domain
|
||||
).apply {
|
||||
addLoadStateListener { loadState ->
|
||||
binding.progressBar.visible(
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
category.isIconSpaceReserved = false
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_follows)
|
||||
key = PrefKeys.NOTIFICATIONS_FILTER_FOLLOWS
|
||||
setTitle(R.string.notification_follow_name)
|
||||
setSummary(R.string.notification_follow_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsFollowed
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -73,8 +73,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_follow_requests)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_FOLLOW_REQUESTS
|
||||
setTitle(R.string.notification_follow_request_name)
|
||||
setSummary(R.string.notification_follow_request_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsFollowRequested
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -84,8 +84,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_reblogs)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_REBLOGS
|
||||
setTitle(R.string.notification_boost_name)
|
||||
setSummary(R.string.notification_boost_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsReblogged
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -95,8 +95,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_favourites)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_FAVS
|
||||
setTitle(R.string.notification_favourite_name)
|
||||
setSummary(R.string.notification_favourite_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsFavorited
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -106,8 +106,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_poll)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_POLLS
|
||||
setTitle(R.string.notification_poll_name)
|
||||
setSummary(R.string.notification_poll_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsPolls
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -117,8 +117,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_subscriptions)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS
|
||||
setTitle(R.string.notification_subscription_name)
|
||||
setSummary(R.string.notification_subscription_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsSubscriptions
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -128,19 +128,8 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_sign_ups)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsSignUps
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAccount { copy(notificationsSignUps = newValue as Boolean) }
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_updates)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_UPDATES
|
||||
setTitle(R.string.notification_update_name)
|
||||
setSummary(R.string.notification_update_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsUpdates
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -150,12 +139,23 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
|
|||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_reports)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_REPORTS
|
||||
setTitle(R.string.notification_channel_admin)
|
||||
setSummary(R.string.notification_channel_admin_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsReports
|
||||
isChecked = activeAccount.notificationsAdmin
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAccount { copy(notificationsReports = newValue as Boolean) }
|
||||
updateAccount { copy(notificationsAdmin = newValue as Boolean) }
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.notification_channel_other)
|
||||
setSummary(R.string.notification_channel_other_description)
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsOther
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAccount { copy(notificationsOther = newValue as Boolean) }
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import android.os.Bundle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.settings.AppTheme
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.emojiPreference
|
||||
|
|
@ -245,13 +245,13 @@ class PreferencesFragment : BasePreferencesFragment() {
|
|||
val notificationFilter = account.notificationsFilter.toMutableSet()
|
||||
|
||||
if (value == true) {
|
||||
notificationFilter.add(Notification.Type.FAVOURITE)
|
||||
notificationFilter.add(Notification.Type.FOLLOW)
|
||||
notificationFilter.add(Notification.Type.REBLOG)
|
||||
notificationFilter.add(NotificationChannelData.FAVOURITE)
|
||||
notificationFilter.add(NotificationChannelData.FOLLOW)
|
||||
notificationFilter.add(NotificationChannelData.REBLOG)
|
||||
} else {
|
||||
notificationFilter.remove(Notification.Type.FAVOURITE)
|
||||
notificationFilter.remove(Notification.Type.FOLLOW)
|
||||
notificationFilter.remove(Notification.Type.REBLOG)
|
||||
notificationFilter.remove(NotificationChannelData.FAVOURITE)
|
||||
notificationFilter.remove(NotificationChannelData.FOLLOW)
|
||||
notificationFilter.remove(NotificationChannelData.REBLOG)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package com.keylesspalace.tusky.components.systemnotifications
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.StringRes
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
|
||||
@Keep
|
||||
enum class NotificationChannelData(
|
||||
val notificationTypes: List<Notification.Type>,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val description: Int,
|
||||
) {
|
||||
MENTION(
|
||||
listOf(Notification.Type.Mention),
|
||||
R.string.notification_mention_name,
|
||||
R.string.notification_mention_descriptions,
|
||||
),
|
||||
|
||||
REBLOG(
|
||||
listOf(Notification.Type.Reblog),
|
||||
R.string.notification_boost_name,
|
||||
R.string.notification_boost_description
|
||||
),
|
||||
|
||||
FAVOURITE(
|
||||
listOf(Notification.Type.Favourite),
|
||||
R.string.notification_favourite_name,
|
||||
R.string.notification_favourite_description
|
||||
),
|
||||
|
||||
FOLLOW(
|
||||
listOf(Notification.Type.Follow),
|
||||
R.string.notification_follow_name,
|
||||
R.string.notification_follow_description
|
||||
),
|
||||
|
||||
FOLLOW_REQUEST(
|
||||
listOf(Notification.Type.FollowRequest),
|
||||
R.string.notification_follow_request_name,
|
||||
R.string.notification_follow_request_description
|
||||
),
|
||||
|
||||
POLL(
|
||||
listOf(Notification.Type.Poll),
|
||||
R.string.notification_poll_name,
|
||||
R.string.notification_poll_description
|
||||
),
|
||||
|
||||
STATUS(
|
||||
listOf(Notification.Type.Status),
|
||||
R.string.notification_subscription_name,
|
||||
R.string.notification_subscription_description
|
||||
),
|
||||
|
||||
UPDATE(
|
||||
listOf(Notification.Type.Update),
|
||||
R.string.notification_update_name,
|
||||
R.string.notification_update_description
|
||||
),
|
||||
|
||||
ADMIN(
|
||||
listOf(Notification.Type.SignUp, Notification.Type.Report),
|
||||
R.string.notification_channel_admin,
|
||||
R.string.notification_channel_admin_description
|
||||
),
|
||||
|
||||
OTHER(
|
||||
listOf(Notification.Type.SeveredRelationship, Notification.Type.ModerationWarning),
|
||||
R.string.notification_channel_other,
|
||||
R.string.notification_channel_other_description
|
||||
);
|
||||
|
||||
fun getChannelId(account: AccountEntity): String {
|
||||
return getChannelId(account.identifier)
|
||||
}
|
||||
|
||||
fun getChannelId(accountIdentifier: String): String {
|
||||
return "CHANNEL_${name}_$accountIdentifier"
|
||||
}
|
||||
}
|
||||
|
||||
fun Set<NotificationChannelData>.toTypes(): Set<Notification.Type> {
|
||||
return flatMap { channelData -> channelData.notificationTypes }.toSet()
|
||||
}
|
||||
|
|
@ -14,15 +14,16 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.NotificationManagerCompat.NotificationWithIdAndTag
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
|
|
@ -48,6 +49,8 @@ import com.keylesspalace.tusky.db.entity.AccountEntity
|
|||
import com.keylesspalace.tusky.di.ApplicationScope
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.NotificationSubscribeResult
|
||||
import com.keylesspalace.tusky.entity.RelationshipSeveranceEvent
|
||||
import com.keylesspalace.tusky.entity.visibleNotificationTypes
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
|
|
@ -58,6 +61,7 @@ import com.keylesspalace.tusky.viewdata.buildDescription
|
|||
import com.keylesspalace.tusky.viewdata.calculatePercent
|
||||
import com.keylesspalace.tusky.worker.NotificationWorker
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.text.NumberFormat
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
|
@ -138,71 +142,15 @@ class NotificationService @Inject constructor(
|
|||
|
||||
fun createNotificationChannelsForAccount(account: AccountEntity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
data class ChannelData(
|
||||
val id: String,
|
||||
@StringRes val name: Int,
|
||||
@StringRes val description: Int,
|
||||
)
|
||||
|
||||
val channelData = arrayOf(
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.MENTION)!!,
|
||||
R.string.notification_mention_name,
|
||||
R.string.notification_mention_descriptions,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.FOLLOW)!!,
|
||||
R.string.notification_follow_name,
|
||||
R.string.notification_follow_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.FOLLOW_REQUEST)!!,
|
||||
R.string.notification_follow_request_name,
|
||||
R.string.notification_follow_request_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.REBLOG)!!,
|
||||
R.string.notification_boost_name,
|
||||
R.string.notification_boost_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.FAVOURITE)!!,
|
||||
R.string.notification_favourite_name,
|
||||
R.string.notification_favourite_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.POLL)!!,
|
||||
R.string.notification_poll_name,
|
||||
R.string.notification_poll_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.STATUS)!!,
|
||||
R.string.notification_subscription_name,
|
||||
R.string.notification_subscription_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.SIGN_UP)!!,
|
||||
R.string.notification_sign_up_name,
|
||||
R.string.notification_sign_up_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.UPDATE)!!,
|
||||
R.string.notification_update_name,
|
||||
R.string.notification_update_description,
|
||||
),
|
||||
ChannelData(
|
||||
getChannelId(account, Notification.Type.REPORT)!!,
|
||||
R.string.notification_report_name,
|
||||
R.string.notification_report_description,
|
||||
),
|
||||
)
|
||||
// TODO enumerate all keys of Notification.Type and check if one is missing here?
|
||||
|
||||
val channelGroup = NotificationChannelGroup(account.identifier, account.fullName)
|
||||
notificationManager.createNotificationChannelGroup(channelGroup)
|
||||
|
||||
val channels = channelData.map {
|
||||
NotificationChannel(it.id, context.getString(it.name), NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
val channels = NotificationChannelData.entries.map {
|
||||
NotificationChannel(
|
||||
it.getChannelId(account),
|
||||
context.getString(it.title),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = context.getString(it.description)
|
||||
enableLights(true)
|
||||
lightColor = -0xd46f27
|
||||
|
|
@ -260,17 +208,17 @@ class NotificationService @Inject constructor(
|
|||
}
|
||||
|
||||
return when (type) {
|
||||
Notification.Type.MENTION -> account.notificationsMentioned
|
||||
Notification.Type.STATUS -> account.notificationsSubscriptions
|
||||
Notification.Type.FOLLOW -> account.notificationsFollowed
|
||||
Notification.Type.FOLLOW_REQUEST -> account.notificationsFollowRequested
|
||||
Notification.Type.REBLOG -> account.notificationsReblogged
|
||||
Notification.Type.FAVOURITE -> account.notificationsFavorited
|
||||
Notification.Type.POLL -> account.notificationsPolls
|
||||
Notification.Type.SIGN_UP -> account.notificationsSignUps
|
||||
Notification.Type.UPDATE -> account.notificationsUpdates
|
||||
Notification.Type.REPORT -> account.notificationsReports
|
||||
else -> false
|
||||
Notification.Type.Mention -> account.notificationsMentioned
|
||||
Notification.Type.Status -> account.notificationsSubscriptions
|
||||
Notification.Type.Follow -> account.notificationsFollowed
|
||||
Notification.Type.FollowRequest -> account.notificationsFollowRequested
|
||||
Notification.Type.Reblog -> account.notificationsReblogged
|
||||
Notification.Type.Favourite -> account.notificationsFavorited
|
||||
Notification.Type.Poll -> account.notificationsPolls
|
||||
Notification.Type.SignUp -> account.notificationsAdmin
|
||||
Notification.Type.Update -> account.notificationsUpdates
|
||||
Notification.Type.Report -> account.notificationsAdmin
|
||||
else -> account.notificationsOther
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +263,7 @@ class NotificationService @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// Only public for one test...
|
||||
@VisibleForTesting
|
||||
fun createBaseNotification(apiNotification: Notification, account: AccountEntity): android.app.Notification? {
|
||||
val channelId = getChannelId(account, apiNotification.type) ?: return null
|
||||
|
||||
|
|
@ -333,41 +281,43 @@ class NotificationService @Inject constructor(
|
|||
notificationId++
|
||||
|
||||
val builder = if (existingAndroidNotification == null) {
|
||||
getNotificationBuilder(body.type, account, channelId)
|
||||
getNotificationBuilder(body, account, channelId)
|
||||
} else {
|
||||
NotificationCompat.Builder(context, existingAndroidNotification)
|
||||
}
|
||||
|
||||
builder
|
||||
.setContentTitle(titleForType(body, account))
|
||||
.setContentText(bodyForType(body, account.alwaysOpenSpoiler))
|
||||
.setContentText(bodyForType(body, account))
|
||||
|
||||
if (body.type == Notification.Type.MENTION || body.type == Notification.Type.POLL) {
|
||||
if (body.type == Notification.Type.Mention || body.type == Notification.Type.Poll) {
|
||||
builder.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(bodyForType(body, account.alwaysOpenSpoiler))
|
||||
.bigText(bodyForType(body, account))
|
||||
)
|
||||
}
|
||||
|
||||
val accountAvatar = try {
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(body.account.avatar)
|
||||
.transform(RoundedCorners(20))
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: ExecutionException) {
|
||||
Log.d(TAG, "Error loading account avatar", e)
|
||||
BitmapFactory.decodeResource(context.resources, R.drawable.avatar_default)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Error loading account avatar", e)
|
||||
BitmapFactory.decodeResource(context.resources, R.drawable.avatar_default)
|
||||
if (body.type != Notification.Type.SeveredRelationship && body.type != Notification.Type.ModerationWarning) {
|
||||
val accountAvatar = try {
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(body.account.avatar)
|
||||
.transform(RoundedCorners(20))
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: ExecutionException) {
|
||||
Log.d(TAG, "Error loading account avatar", e)
|
||||
BitmapFactory.decodeResource(context.resources, R.drawable.avatar_default)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Error loading account avatar", e)
|
||||
BitmapFactory.decodeResource(context.resources, R.drawable.avatar_default)
|
||||
}
|
||||
|
||||
builder.setLargeIcon(accountAvatar)
|
||||
}
|
||||
|
||||
builder.setLargeIcon(accountAvatar)
|
||||
|
||||
// Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat
|
||||
if (body.type == Notification.Type.MENTION) {
|
||||
if (body.type == Notification.Type.Mention) {
|
||||
val replyRemoteInput = RemoteInput.Builder(KEY_REPLY)
|
||||
.setLabel(context.getString(R.string.label_quick_reply))
|
||||
.build()
|
||||
|
|
@ -471,19 +421,9 @@ class NotificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getChannelId(account: AccountEntity, type: Notification.Type): String? {
|
||||
return when (type) {
|
||||
Notification.Type.MENTION -> CHANNEL_MENTION + account.identifier
|
||||
Notification.Type.STATUS -> "CHANNEL_SUBSCRIPTIONS" + account.identifier
|
||||
Notification.Type.FOLLOW -> "CHANNEL_FOLLOW" + account.identifier
|
||||
Notification.Type.FOLLOW_REQUEST -> "CHANNEL_FOLLOW_REQUEST" + account.identifier
|
||||
Notification.Type.REBLOG -> "CHANNEL_BOOST" + account.identifier
|
||||
Notification.Type.FAVOURITE -> "CHANNEL_FAVOURITE" + account.identifier
|
||||
Notification.Type.POLL -> "CHANNEL_POLL" + account.identifier
|
||||
Notification.Type.SIGN_UP -> "CHANNEL_SIGN_UP" + account.identifier
|
||||
Notification.Type.UPDATE -> "CHANNEL_UPDATES" + account.identifier
|
||||
Notification.Type.REPORT -> "CHANNEL_REPORT" + account.identifier
|
||||
else -> null
|
||||
}
|
||||
return NotificationChannelData.entries.find { data ->
|
||||
data.notificationTypes.contains(type)
|
||||
}?.getChannelId(account)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -499,17 +439,24 @@ class NotificationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNotificationBuilder(notificationType: Notification.Type, account: AccountEntity, channelId: String): NotificationCompat.Builder {
|
||||
val eventResultIntent = openNotificationIntent(context, account.id, notificationType)
|
||||
private fun getNotificationBuilder(notification: Notification, account: AccountEntity, channelId: String): NotificationCompat.Builder {
|
||||
val notificationType = notification.type
|
||||
val eventResultPendingIntent = if (notificationType == Notification.Type.ModerationWarning) {
|
||||
val warning = notification.moderationWarning!!
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://${account.domain}/disputes/strikes/${warning.id}".toUri())
|
||||
PendingIntent.getActivity(context, account.id.toInt(), intent, pendingIntentFlags(false))
|
||||
} else {
|
||||
val eventResultIntent = openNotificationIntent(context, account.id, notificationType)
|
||||
|
||||
val eventStackBuilder = TaskStackBuilder.create(context)
|
||||
eventStackBuilder.addParentStack(MainActivity::class.java)
|
||||
eventStackBuilder.addNextIntent(eventResultIntent)
|
||||
val eventStackBuilder = TaskStackBuilder.create(context)
|
||||
eventStackBuilder.addParentStack(MainActivity::class.java)
|
||||
eventStackBuilder.addNextIntent(eventResultIntent)
|
||||
|
||||
val eventResultPendingIntent = eventStackBuilder.getPendingIntent(
|
||||
account.id.toInt(),
|
||||
pendingIntentFlags(false)
|
||||
)
|
||||
eventStackBuilder.getPendingIntent(
|
||||
account.id.toInt(),
|
||||
pendingIntentFlags(false)
|
||||
)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
|
|
@ -530,46 +477,42 @@ class NotificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun titleForType(notification: Notification, account: AccountEntity): String? {
|
||||
if (notification.status == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val accountName = notification.account.name.unicodeWrap()
|
||||
when (notification.type) {
|
||||
Notification.Type.MENTION -> return context.getString(R.string.notification_mention_format, accountName)
|
||||
Notification.Type.STATUS -> return context.getString(R.string.notification_subscription_format, accountName)
|
||||
Notification.Type.FOLLOW -> return context.getString(R.string.notification_follow_format, accountName)
|
||||
Notification.Type.FOLLOW_REQUEST -> return context.getString(R.string.notification_follow_request_format, accountName)
|
||||
Notification.Type.FAVOURITE -> return context.getString(R.string.notification_favourite_format, accountName)
|
||||
Notification.Type.REBLOG -> return context.getString(R.string.notification_reblog_format, accountName)
|
||||
Notification.Type.POLL -> return if (notification.status.account.id == account.accountId) {
|
||||
Notification.Type.Mention -> return context.getString(R.string.notification_mention_format, accountName)
|
||||
Notification.Type.Status -> return context.getString(R.string.notification_subscription_format, accountName)
|
||||
Notification.Type.Follow -> return context.getString(R.string.notification_follow_format, accountName)
|
||||
Notification.Type.FollowRequest -> return context.getString(R.string.notification_follow_request_format, accountName)
|
||||
Notification.Type.Favourite -> return context.getString(R.string.notification_favourite_format, accountName)
|
||||
Notification.Type.Reblog -> return context.getString(R.string.notification_reblog_format, accountName)
|
||||
Notification.Type.Poll -> return if (notification.status!!.account.id == account.accountId) {
|
||||
context.getString(R.string.poll_ended_created)
|
||||
} else {
|
||||
context.getString(R.string.poll_ended_voted)
|
||||
}
|
||||
Notification.Type.SIGN_UP -> return context.getString(R.string.notification_sign_up_format, accountName)
|
||||
Notification.Type.UPDATE -> return context.getString(R.string.notification_update_format, accountName)
|
||||
Notification.Type.REPORT -> return context.getString(R.string.notification_report_format, account.domain)
|
||||
Notification.Type.UNKNOWN -> return null
|
||||
Notification.Type.SignUp -> return context.getString(R.string.notification_sign_up_format, accountName)
|
||||
Notification.Type.Update -> return context.getString(R.string.notification_update_format, accountName)
|
||||
Notification.Type.Report -> return context.getString(R.string.notification_report_format, account.domain)
|
||||
Notification.Type.SeveredRelationship -> return context.getString(R.string.relationship_severance_event_title)
|
||||
Notification.Type.ModerationWarning -> return context.getString(R.string.moderation_warning)
|
||||
is Notification.Type.Unknown -> return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun bodyForType(notification: Notification, alwaysOpenSpoiler: Boolean): String? {
|
||||
if (notification.status == null) {
|
||||
return null
|
||||
}
|
||||
private fun bodyForType(notification: Notification, account: AccountEntity): String? {
|
||||
val alwaysOpenSpoiler = account.alwaysOpenSpoiler
|
||||
|
||||
when (notification.type) {
|
||||
Notification.Type.FOLLOW, Notification.Type.FOLLOW_REQUEST, Notification.Type.SIGN_UP -> return "@" + notification.account.username
|
||||
Notification.Type.MENTION, Notification.Type.FAVOURITE, Notification.Type.REBLOG, Notification.Type.STATUS -> return if (!TextUtils.isEmpty(notification.status.spoilerText) && !alwaysOpenSpoiler) {
|
||||
Notification.Type.Follow, Notification.Type.FollowRequest, Notification.Type.SignUp -> return "@" + notification.account.username
|
||||
Notification.Type.Mention, Notification.Type.Favourite, Notification.Type.Reblog, Notification.Type.Status -> return if (!notification.status?.spoilerText.isNullOrEmpty() && !alwaysOpenSpoiler) {
|
||||
notification.status.spoilerText
|
||||
} else {
|
||||
notification.status.content.parseAsMastodonHtml().toString()
|
||||
notification.status?.content?.parseAsMastodonHtml()?.toString()
|
||||
}
|
||||
Notification.Type.POLL -> if (!TextUtils.isEmpty(notification.status.spoilerText) && !alwaysOpenSpoiler) {
|
||||
Notification.Type.Poll -> if (!notification.status?.spoilerText.isNullOrEmpty() && !alwaysOpenSpoiler) {
|
||||
return notification.status.spoilerText
|
||||
} else {
|
||||
val poll = notification.status.poll ?: return null
|
||||
val poll = notification.status?.poll ?: return null
|
||||
|
||||
val builder = StringBuilder(notification.status.content.parseAsMastodonHtml())
|
||||
builder.append('\n')
|
||||
|
|
@ -588,11 +531,13 @@ class NotificationService @Inject constructor(
|
|||
|
||||
return builder.toString()
|
||||
}
|
||||
Notification.Type.REPORT -> return context.getString(
|
||||
Notification.Type.Report -> return context.getString(
|
||||
R.string.notification_header_report_format,
|
||||
notification.account.name.unicodeWrap(),
|
||||
notification.report!!.targetAccount.name.unicodeWrap()
|
||||
)
|
||||
Notification.Type.SeveredRelationship -> return severedRelationShipText(context, notification.event!!, account.domain)
|
||||
Notification.Type.ModerationWarning -> return context.getString(notification.moderationWarning!!.action.text)
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
|
@ -908,8 +853,8 @@ class NotificationService @Inject constructor(
|
|||
|
||||
private fun buildAlertsMap(account: AccountEntity): Map<String, Boolean> =
|
||||
buildMap {
|
||||
Notification.Type.visibleTypes.forEach {
|
||||
put(it.presentation, filterNotification(account, it))
|
||||
visibleNotificationTypes.forEach {
|
||||
put(it.name, filterNotification(account, it))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1010,7 +955,6 @@ class NotificationService @Inject constructor(
|
|||
companion object {
|
||||
const val TAG = "NotificationService"
|
||||
|
||||
const val CHANNEL_MENTION: String = "CHANNEL_MENTION"
|
||||
const val KEY_CITED_STATUS_ID: String = "KEY_CITED_STATUS_ID"
|
||||
const val KEY_MENTIONS: String = "KEY_MENTIONS"
|
||||
const val KEY_REPLY: String = "KEY_REPLY"
|
||||
|
|
@ -1029,5 +973,33 @@ class NotificationService @Inject constructor(
|
|||
private const val EXTRA_NOTIFICATION_TYPE = BuildConfig.APPLICATION_ID + ".notification.extra.notification_type"
|
||||
private const val GROUP_SUMMARY_TAG = BuildConfig.APPLICATION_ID + ".notification.group_summary"
|
||||
private const val NOTIFICATION_PULL_NAME = "pullNotifications"
|
||||
|
||||
private val numberFormat = NumberFormat.getNumberInstance()
|
||||
|
||||
fun severedRelationShipText(
|
||||
context: Context,
|
||||
event: RelationshipSeveranceEvent,
|
||||
instanceName: String
|
||||
): String {
|
||||
return when (event.type) {
|
||||
RelationshipSeveranceEvent.Type.DOMAIN_BLOCK -> {
|
||||
val followers = numberFormat.format(event.followersCount)
|
||||
val following = numberFormat.format(event.followingCount)
|
||||
val followingText = context.resources.getQuantityString(R.plurals.accounts, event.followingCount, following)
|
||||
context.getString(R.string.relationship_severance_event_domain_block, instanceName, event.targetName, followers, followingText)
|
||||
}
|
||||
|
||||
RelationshipSeveranceEvent.Type.USER_DOMAIN_BLOCK -> {
|
||||
val followers = numberFormat.format(event.followersCount)
|
||||
val following = numberFormat.format(event.followingCount)
|
||||
val followingText = context.resources.getQuantityString(R.plurals.accounts, event.followingCount, following)
|
||||
context.getString(R.string.relationship_severance_event_user_domain_block, event.targetName, followers, followingText)
|
||||
}
|
||||
|
||||
RelationshipSeveranceEvent.Type.ACCOUNT_SUSPENSION -> {
|
||||
context.getString(R.string.relationship_severance_event_account_suspension, instanceName, event.targetName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fun TimelineAccount.toEntity(tuskyAccountId: Long): TimelineAccountEntity {
|
|||
url = url,
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
note = note,
|
||||
bot = bot
|
||||
)
|
||||
}
|
||||
|
|
@ -50,7 +51,7 @@ fun TimelineAccountEntity.toAccount(): TimelineAccount {
|
|||
localUsername = localUsername,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
note = "",
|
||||
note = note,
|
||||
url = url,
|
||||
avatar = avatar,
|
||||
bot = bot,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.components.conversation.ConversationEntity;
|
|||
import com.keylesspalace.tusky.db.dao.AccountDao;
|
||||
import com.keylesspalace.tusky.db.dao.DraftDao;
|
||||
import com.keylesspalace.tusky.db.dao.InstanceDao;
|
||||
import com.keylesspalace.tusky.db.dao.NotificationPolicyDao;
|
||||
import com.keylesspalace.tusky.db.dao.NotificationsDao;
|
||||
import com.keylesspalace.tusky.db.dao.TimelineAccountDao;
|
||||
import com.keylesspalace.tusky.db.dao.TimelineDao;
|
||||
|
|
@ -39,6 +40,7 @@ import com.keylesspalace.tusky.db.entity.DraftEntity;
|
|||
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity;
|
||||
import com.keylesspalace.tusky.db.entity.InstanceEntity;
|
||||
import com.keylesspalace.tusky.db.entity.NotificationEntity;
|
||||
import com.keylesspalace.tusky.db.entity.NotificationPolicyEntity;
|
||||
import com.keylesspalace.tusky.db.entity.NotificationReportEntity;
|
||||
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity;
|
||||
import com.keylesspalace.tusky.db.entity.TimelineStatusEntity;
|
||||
|
|
@ -58,11 +60,12 @@ import java.io.File;
|
|||
ConversationEntity.class,
|
||||
NotificationEntity.class,
|
||||
NotificationReportEntity.class,
|
||||
HomeTimelineEntity.class
|
||||
HomeTimelineEntity.class,
|
||||
NotificationPolicyEntity.class
|
||||
},
|
||||
// Note: Starting with version 54, database versions in Tusky are always even.
|
||||
// This is to reserve odd version numbers for use by forks.
|
||||
version = 66,
|
||||
version = 68,
|
||||
autoMigrations = {
|
||||
@AutoMigration(from = 48, to = 49),
|
||||
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),
|
||||
|
|
@ -72,6 +75,7 @@ import java.io.File;
|
|||
@AutoMigration(from = 56, to = 58), // translationEnabled in InstanceEntity/InstanceInfoEntity
|
||||
@AutoMigration(from = 62, to = 64), // filterV2Available in InstanceEntity
|
||||
@AutoMigration(from = 64, to = 66), // added profileHeaderUrl to AccountEntity
|
||||
@AutoMigration(from = 66, to = 68, spec = AppDatabase.MIGRATION_66_68.class), // added event and moderationAction to NotificationEntity, new NotificationPolicyEntity
|
||||
}
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
|
@ -84,6 +88,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
@NonNull public abstract NotificationsDao notificationsDao();
|
||||
@NonNull public abstract TimelineStatusDao timelineStatusDao();
|
||||
@NonNull public abstract TimelineAccountDao timelineAccountDao();
|
||||
@NonNull public abstract NotificationPolicyDao notificationPolicyDao();
|
||||
|
||||
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||
@Override
|
||||
|
|
@ -850,4 +855,8 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultReplyPrivacy` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
};
|
||||
|
||||
@DeleteColumn(tableName = "AccountEntity", columnName = "notificationsSignUps")
|
||||
@DeleteColumn(tableName = "AccountEntity", columnName = "notificationsReports")
|
||||
static class MIGRATION_66_68 implements AutoMigrationSpec { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import androidx.room.ProvidedTypeConverter
|
|||
import androidx.room.TypeConverter
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.createTabDataFromId
|
||||
import com.keylesspalace.tusky.db.entity.DraftAttachment
|
||||
import com.keylesspalace.tusky.entity.AccountWarning
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.FilterResult
|
||||
|
|
@ -29,7 +31,9 @@ import com.keylesspalace.tusky.entity.NewPoll
|
|||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.PreviewCard
|
||||
import com.keylesspalace.tusky.entity.RelationshipSeveranceEvent
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.notificationTypeFromString
|
||||
import com.keylesspalace.tusky.settings.DefaultReplyVisibility
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
|
|
@ -229,27 +233,59 @@ class Converters @Inject constructor(
|
|||
}
|
||||
|
||||
@TypeConverter
|
||||
fun notificationTypeListToJson(data: Set<Notification.Type>?): String {
|
||||
fun notificationChannelDataListToJson(data: Set<NotificationChannelData>?): String {
|
||||
val array = JSONArray()
|
||||
data?.forEach {
|
||||
array.put(it.presentation)
|
||||
array.put(it.name)
|
||||
}
|
||||
return array.toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToNotificationTypeList(data: String?): Set<Notification.Type> {
|
||||
val ret = HashSet<Notification.Type>()
|
||||
fun jsonToNotificationChannelDataList(data: String?): Set<NotificationChannelData> {
|
||||
val ret = HashSet<NotificationChannelData>()
|
||||
data?.let {
|
||||
val array = JSONArray(data)
|
||||
for (i in 0 until array.length()) {
|
||||
val item = array.getString(i)
|
||||
val type = Notification.Type.byString(item)
|
||||
if (type != Notification.Type.UNKNOWN) {
|
||||
try {
|
||||
val type = NotificationChannelData.valueOf(item)
|
||||
ret.add(type)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// ignore, this can happen because we stored individual notification types and not channels before
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun relationshipSeveranceEventToJson(event: RelationshipSeveranceEvent?): String {
|
||||
return moshi.adapter<RelationshipSeveranceEvent?>().toJson(event)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToRelationshipSeveranceEvent(eventJson: String?): RelationshipSeveranceEvent? {
|
||||
return eventJson?.let { moshi.adapter<RelationshipSeveranceEvent?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountWarningToJson(accountWarning: AccountWarning?): String {
|
||||
return moshi.adapter<AccountWarning?>().toJson(accountWarning)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccountWarning(accountWarningJson: String?): AccountWarning? {
|
||||
return accountWarningJson?.let { moshi.adapter<AccountWarning?>().fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountWarningToJson(notificationType: Notification.Type): String {
|
||||
return notificationType.name
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToNotificationType(notificationTypeJson: String): Notification.Type {
|
||||
return notificationTypeFromString(notificationTypeJson)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
import com.keylesspalace.tusky.db.entity.NotificationPolicyEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface NotificationPolicyDao {
|
||||
@Query("SELECT * FROM NotificationPolicyEntity WHERE tuskyAccountId = :accountId")
|
||||
fun notificationPolicyForAccount(accountId: Long): Flow<NotificationPolicyEntity?>
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun update(entity: NotificationPolicyEntity)
|
||||
|
||||
@Query(
|
||||
"UPDATE NotificationPolicyEntity " +
|
||||
"SET pendingRequestsCount = max(0, pendingRequestsCount - 1)," +
|
||||
"pendingNotificationsCount = max(0, pendingNotificationsCount - :notificationCount) " +
|
||||
"WHERE tuskyAccountId = :accountId"
|
||||
)
|
||||
suspend fun updateCounts(
|
||||
accountId: Long,
|
||||
notificationCount: Int
|
||||
)
|
||||
}
|
||||
|
|
@ -35,11 +35,11 @@ abstract class NotificationsDao {
|
|||
|
||||
@Query(
|
||||
"""
|
||||
SELECT n.tuskyAccountId, n.type, n.id, n.loading,
|
||||
SELECT n.tuskyAccountId, n.type, n.id, n.loading, n.event, n.moderationWarning,
|
||||
a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId',
|
||||
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
||||
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
||||
a.emojis as 'a_emojis', a.bot as 'a_bot',
|
||||
a.note as 'a_note', a.emojis as 'a_emojis', a.bot as 'a_bot',
|
||||
s.serverId as 's_serverId', s.url as 's_url', s.tuskyAccountId as 's_tuskyAccountId',
|
||||
s.authorServerId as 's_authorServerId', s.inReplyToId as 's_inReplyToId', s.inReplyToAccountId as 's_inReplyToAccountId',
|
||||
s.content as 's_content', s.createdAt as 's_createdAt', s.editedAt as 's_editedAt', s.emojis as 's_emojis', s.reblogsCount as 's_reblogsCount',
|
||||
|
|
@ -51,14 +51,14 @@ s.pinned as 's_pinned', s.language as 's_language', s.filtered as 's_filtered',
|
|||
sa.serverId as 'sa_serverId', sa.tuskyAccountId as 'sa_tuskyAccountId',
|
||||
sa.localUsername as 'sa_localUsername', sa.username as 'sa_username',
|
||||
sa.displayName as 'sa_displayName', sa.url as 'sa_url', sa.avatar as 'sa_avatar',
|
||||
sa.emojis as 'sa_emojis', sa.bot as 'sa_bot',
|
||||
sa.note as 'sa_note', sa.emojis as 'sa_emojis', sa.bot as 'sa_bot',
|
||||
r.serverId as 'r_serverId', r.tuskyAccountId as 'r_tuskyAccountId',
|
||||
r.category as 'r_category', r.statusIds as 'r_statusIds',
|
||||
r.createdAt as 'r_createdAt', r.targetAccountId as 'r_targetAccountId',
|
||||
ra.serverId as 'ra_serverId', ra.tuskyAccountId as 'ra_tuskyAccountId',
|
||||
ra.localUsername as 'ra_localUsername', ra.username as 'ra_username',
|
||||
ra.displayName as 'ra_displayName', ra.url as 'ra_url', ra.avatar as 'ra_avatar',
|
||||
ra.emojis as 'ra_emojis', ra.bot as 'ra_bot'
|
||||
ra.note as 'ra_note', ra.emojis as 'ra_emojis', ra.bot as 'ra_bot'
|
||||
FROM NotificationEntity n
|
||||
LEFT JOIN TimelineAccountEntity a ON (n.tuskyAccountId = a.tuskyAccountId AND n.accountId = a.serverId)
|
||||
LEFT JOIN TimelineStatusEntity s ON (n.tuskyAccountId = s.tuskyAccountId AND n.statusId = s.serverId)
|
||||
|
|
@ -111,7 +111,7 @@ AND
|
|||
(accountId = :userId OR
|
||||
statusId IN (SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId = :userId)
|
||||
)
|
||||
AND type != "SIGN_UP" AND type != "REPORT"
|
||||
AND type != "admin.sign_up" AND type != "admin.report"
|
||||
"""
|
||||
)
|
||||
abstract suspend fun removeAllByUser(tuskyAccountId: Long, userId: String)
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@ s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing,
|
|||
a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId',
|
||||
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
||||
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
||||
a.emojis as 'a_emojis', a.bot as 'a_bot',
|
||||
a.note as 'a_note', a.emojis as 'a_emojis', a.bot as 'a_bot',
|
||||
rb.serverId as 'rb_serverId', rb.tuskyAccountId 'rb_tuskyAccountId',
|
||||
rb.localUsername as 'rb_localUsername', rb.username as 'rb_username',
|
||||
rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar',
|
||||
rb.emojis as 'rb_emojis', rb.bot as 'rb_bot',
|
||||
rb.note as 'rb_note', rb.emojis as 'rb_emojis', rb.bot as 'rb_bot',
|
||||
replied.serverId as 'replied_serverId', replied.tuskyAccountId 'replied_tuskyAccountId',
|
||||
replied.localUsername as 'replied_localUsername', replied.username as 'replied_username',
|
||||
replied.displayName as 'replied_displayName', replied.url as 'replied_url', replied.avatar as 'replied_avatar',
|
||||
replied.emojis as 'replied_emojis', replied.bot as 'replied_bot',
|
||||
replied.note as 'replied_note', replied.emojis as 'replied_emojis', replied.bot as 'replied_bot',
|
||||
h.loading
|
||||
FROM HomeTimelineEntity h
|
||||
LEFT JOIN TimelineStatusEntity s ON (h.statusId = s.serverId AND s.tuskyAccountId = :tuskyAccountId)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import androidx.room.Index
|
|||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.defaultTabs
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.settings.DefaultReplyVisibility
|
||||
|
||||
|
|
@ -54,14 +54,14 @@ data class AccountEntity(
|
|||
val notificationsEnabled: Boolean = true,
|
||||
val notificationsMentioned: Boolean = true,
|
||||
val notificationsFollowed: Boolean = true,
|
||||
val notificationsFollowRequested: Boolean = false,
|
||||
val notificationsFollowRequested: Boolean = true,
|
||||
val notificationsReblogged: Boolean = true,
|
||||
val notificationsFavorited: Boolean = true,
|
||||
val notificationsPolls: Boolean = true,
|
||||
val notificationsSubscriptions: Boolean = true,
|
||||
val notificationsSignUps: Boolean = true,
|
||||
val notificationsUpdates: Boolean = true,
|
||||
val notificationsReports: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "true") val notificationsAdmin: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "true") val notificationsOther: Boolean = true,
|
||||
val notificationSound: Boolean = true,
|
||||
val notificationVibration: Boolean = true,
|
||||
val notificationLight: Boolean = true,
|
||||
|
|
@ -94,7 +94,7 @@ data class AccountEntity(
|
|||
val notificationMarkerId: String = "0",
|
||||
val emojis: List<Emoji> = emptyList(),
|
||||
val tabPreferences: List<TabData> = defaultTabs(),
|
||||
val notificationsFilter: Set<Notification.Type> = setOf(Notification.Type.FOLLOW_REQUEST),
|
||||
val notificationsFilter: Set<NotificationChannelData> = emptySet(),
|
||||
// Scope cannot be changed without re-login, so store it in case
|
||||
// the scope needs to be changed in the future
|
||||
val oauthScopes: String = "",
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ import androidx.room.ForeignKey
|
|||
import androidx.room.Index
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.AccountWarning
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.RelationshipSeveranceEvent
|
||||
import java.util.Date
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
data class NotificationDataEntity(
|
||||
// id of the account logged into Tusky this notifications belongs to
|
||||
val tuskyAccountId: Long,
|
||||
|
|
@ -35,6 +38,8 @@ data class NotificationDataEntity(
|
|||
@Embedded(prefix = "sa_") val statusAccount: TimelineAccountEntity?,
|
||||
@Embedded(prefix = "r_") val report: NotificationReportEntity?,
|
||||
@Embedded(prefix = "ra_") val reportTargetAccount: TimelineAccountEntity?,
|
||||
val event: RelationshipSeveranceEvent?,
|
||||
val moderationWarning: AccountWarning?,
|
||||
// relevant when it is a placeholder
|
||||
val loading: Boolean = false
|
||||
)
|
||||
|
|
@ -76,6 +81,8 @@ data class NotificationEntity(
|
|||
val accountId: String?,
|
||||
val statusId: String?,
|
||||
val reportId: String?,
|
||||
val event: RelationshipSeveranceEvent?,
|
||||
val moderationWarning: AccountWarning?,
|
||||
// relevant when it is a placeholder
|
||||
val loading: Boolean = false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.keylesspalace.tusky.db.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class NotificationPolicyEntity(
|
||||
@PrimaryKey val tuskyAccountId: Long,
|
||||
val pendingRequestsCount: Int,
|
||||
val pendingNotificationsCount: Int
|
||||
)
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
|
|
@ -32,6 +33,7 @@ data class TimelineAccountEntity(
|
|||
val displayName: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
@ColumnInfo(defaultValue = "") val note: String,
|
||||
val emojis: List<Emoji>,
|
||||
val bot: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Attachment
|
|||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.json.GuardedAdapter
|
||||
import com.keylesspalace.tusky.json.NotificationTypeAdapter
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||
import com.keylesspalace.tusky.network.apiForAccount
|
||||
|
|
@ -91,8 +92,7 @@ object NetworkModule {
|
|||
)
|
||||
.add(
|
||||
Notification.Type::class.java,
|
||||
EnumJsonAdapter.create(Notification.Type::class.java)
|
||||
.withUnknownFallback(Notification.Type.UNKNOWN)
|
||||
NotificationTypeAdapter()
|
||||
)
|
||||
.add(
|
||||
Status.Visibility::class.java,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
/* Copyright 2025 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.entity
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountWarning(
|
||||
val id: String,
|
||||
val action: Action
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Action(@StringRes val text: Int) {
|
||||
@Json(name = "none")
|
||||
NONE(R.string.moderation_warning_action_none),
|
||||
|
||||
@Json(name = "disable")
|
||||
DISABLE(R.string.moderation_warning_action_disable),
|
||||
|
||||
@Json(name = "mark_statuses_as_sensitive")
|
||||
MARK_STATUSES_AS_SENSITIVE(R.string.moderation_warning_action_mark_statuses_as_sensitive),
|
||||
|
||||
@Json(name = "delete_statuses")
|
||||
DELETE_STATUSES(R.string.moderation_warning_action_delete_statuses),
|
||||
|
||||
@Json(name = "sensitive")
|
||||
SENSITIVE(R.string.moderation_warning_action_sensitive),
|
||||
|
||||
@Json(name = "silence")
|
||||
SILENCE(R.string.moderation_warning_action_silence),
|
||||
|
||||
@Json(name = "suspend")
|
||||
SUSPEND(R.string.moderation_warning_action_suspend),
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,17 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Notification.Type
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Favourite
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Follow
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.FollowRequest
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Mention
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.ModerationWarning
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Reblog
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.SeveredRelationship
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.SignUp
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Unknown
|
||||
import com.keylesspalace.tusky.entity.Notification.Type.Update
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
|
|
@ -28,78 +37,77 @@ data class Notification(
|
|||
val status: Status? = null,
|
||||
val report: Report? = null,
|
||||
val filtered: Boolean = false,
|
||||
val event: RelationshipSeveranceEvent? = null,
|
||||
@Json(name = "moderation_warning") val moderationWarning: AccountWarning? = null
|
||||
) {
|
||||
|
||||
/** From https://docs.joinmastodon.org/entities/Notification/#type */
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type(val presentation: String, @StringRes val uiString: Int) {
|
||||
UNKNOWN("unknown", R.string.notification_unknown_name),
|
||||
sealed class Type(val name: String) {
|
||||
data class Unknown(val unknownName: String) : Type(unknownName)
|
||||
|
||||
/** Someone mentioned you */
|
||||
@Json(name = "mention")
|
||||
MENTION("mention", R.string.notification_mention_name),
|
||||
object Mention : Type("mention")
|
||||
|
||||
/** Someone boosted one of your statuses */
|
||||
@Json(name = "reblog")
|
||||
REBLOG("reblog", R.string.notification_boost_name),
|
||||
object Reblog : Type("reblog")
|
||||
|
||||
/** Someone favourited one of your statuses */
|
||||
@Json(name = "favourite")
|
||||
FAVOURITE("favourite", R.string.notification_favourite_name),
|
||||
object Favourite : Type("favourite")
|
||||
|
||||
/** Someone followed you */
|
||||
@Json(name = "follow")
|
||||
FOLLOW("follow", R.string.notification_follow_name),
|
||||
object Follow : Type("follow")
|
||||
|
||||
/** Someone requested to follow you */
|
||||
@Json(name = "follow_request")
|
||||
FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name),
|
||||
object FollowRequest : Type("follow_request")
|
||||
|
||||
/** A poll you have voted in or created has ended */
|
||||
@Json(name = "poll")
|
||||
POLL("poll", R.string.notification_poll_name),
|
||||
object Poll : Type("poll")
|
||||
|
||||
/** Someone you enabled notifications for has posted a status */
|
||||
@Json(name = "status")
|
||||
STATUS("status", R.string.notification_subscription_name),
|
||||
object Status : Type("status")
|
||||
|
||||
/** Someone signed up (optionally sent to admins) */
|
||||
@Json(name = "admin.sign_up")
|
||||
SIGN_UP("admin.sign_up", R.string.notification_sign_up_name),
|
||||
object SignUp : Type("admin.sign_up")
|
||||
|
||||
/** A status you interacted with has been updated */
|
||||
@Json(name = "update")
|
||||
UPDATE("update", R.string.notification_update_name),
|
||||
object Update : Type("update")
|
||||
|
||||
/** A new report has been filed */
|
||||
@Json(name = "admin.report")
|
||||
REPORT("admin.report", R.string.notification_report_name);
|
||||
object Report : Type("admin.report")
|
||||
|
||||
companion object {
|
||||
fun byString(s: String): Type {
|
||||
return entries.firstOrNull { it.presentation == s } ?: UNKNOWN
|
||||
}
|
||||
/** Some of your follow relationships have been severed as a result of a moderation or block event **/
|
||||
object SeveredRelationship : Type("severed_relationships")
|
||||
|
||||
/** Notification types for UI display (omits UNKNOWN) */
|
||||
val visibleTypes =
|
||||
listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE, REPORT)
|
||||
}
|
||||
/** moderation_warning = A moderator has taken action against your account or has sent you a warning **/
|
||||
object ModerationWarning : Type("moderation_warning")
|
||||
|
||||
override fun toString() = presentation
|
||||
// can't use data objects or this wouldn't work
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
// for Pleroma compatibility that uses Mention type
|
||||
fun rewriteToStatusTypeIfNeeded(accountId: String): Notification {
|
||||
if (type == Type.MENTION && status != null) {
|
||||
if (type == Mention && status != null) {
|
||||
return if (status.mentions.any {
|
||||
it.id == accountId
|
||||
}
|
||||
) {
|
||||
this
|
||||
} else {
|
||||
copy(type = Type.STATUS)
|
||||
copy(type = Type.Status)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/** Notification types for UI display (omits UNKNOWN) */
|
||||
/** this is not in a companion object so it gets initialized earlier,
|
||||
* otherwise it might get initialized when a subclass is loaded,
|
||||
* which leds to crash since those subclasses are referenced here */
|
||||
val visibleNotificationTypes = listOf(Mention, Reblog, Favourite, Follow, FollowRequest, Type.Poll, Type.Status, SignUp, Update, Type.Report, SeveredRelationship, ModerationWarning)
|
||||
|
||||
fun notificationTypeFromString(s: String): Type {
|
||||
return visibleNotificationTypes.firstOrNull { it.name == s.lowercase() } ?: Unknown(s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ import com.squareup.moshi.JsonClass
|
|||
data class NotificationRequest(
|
||||
val id: String,
|
||||
val account: Account,
|
||||
@Json(name = "notifications_count") val notificationsCount: String
|
||||
@Json(name = "notifications_count") val notificationsCount: Int
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/* Copyright 2025 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.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RelationshipSeveranceEvent(
|
||||
val id: String,
|
||||
val type: Type,
|
||||
@Json(name = "target_name") val targetName: String,
|
||||
@Json(name = "followers_count") val followersCount: Int,
|
||||
@Json(name = "following_count") val followingCount: Int
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type {
|
||||
@Json(name = "domain_block")
|
||||
DOMAIN_BLOCK,
|
||||
|
||||
@Json(name = "user_domain_block")
|
||||
USER_DOMAIN_BLOCK,
|
||||
|
||||
@Json(name = "account_suspension")
|
||||
ACCOUNT_SUSPENSION,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2025 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.json
|
||||
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.notificationTypeFromString
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class NotificationTypeAdapter : JsonAdapter<Notification.Type>() {
|
||||
|
||||
override fun fromJson(reader: JsonReader): Notification.Type {
|
||||
return notificationTypeFromString(reader.nextString())
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: Notification.Type?) {
|
||||
writer.value(value?.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
|
@ -47,7 +48,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
val senderId = intent.getLongExtra(NotificationService.KEY_SENDER_ACCOUNT_ID, -1)
|
||||
val senderIdentifier = intent.getStringExtra(
|
||||
NotificationService.KEY_SENDER_ACCOUNT_IDENTIFIER
|
||||
)
|
||||
)!!
|
||||
val senderFullName = intent.getStringExtra(
|
||||
NotificationService.KEY_SENDER_ACCOUNT_FULL_NAME
|
||||
)
|
||||
|
|
@ -68,7 +69,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
val notification = NotificationCompat.Builder(
|
||||
context,
|
||||
NotificationService.CHANNEL_MENTION + senderIdentifier
|
||||
NotificationChannelData.MENTION.getChannelId(senderIdentifier)
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setColor(context.getColor(R.color.tusky_blue))
|
||||
|
|
@ -113,7 +114,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
// Notifications with remote input active can't be cancelled, so let's replace it with another one that will dismiss automatically
|
||||
val notification = NotificationCompat.Builder(
|
||||
context,
|
||||
NotificationService.CHANNEL_MENTION + senderIdentifier
|
||||
NotificationChannelData.MENTION.getChannelId(senderIdentifier)
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setColor(context.getColor(R.color.notification_color))
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ enum class AppTheme(val value: String) {
|
|||
*
|
||||
* - Adding a new preference that does not change the interpretation of an existing preference
|
||||
*/
|
||||
const val SCHEMA_VERSION = 2025021701
|
||||
const val SCHEMA_VERSION = 2025022001
|
||||
|
||||
/** The schema version for fresh installs */
|
||||
const val NEW_INSTALL_SCHEMA_VERSION = 0
|
||||
|
|
@ -98,15 +98,6 @@ object PrefKeys {
|
|||
const val NOTIFICATION_ALERT_LIGHT = "notificationAlertLight"
|
||||
const val NOTIFICATION_ALERT_VIBRATE = "notificationAlertVibrate"
|
||||
const val NOTIFICATION_ALERT_SOUND = "notificationAlertSound"
|
||||
const val NOTIFICATION_FILTER_POLLS = "notificationFilterPolls"
|
||||
const val NOTIFICATION_FILTER_FAVS = "notificationFilterFavourites"
|
||||
const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs"
|
||||
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
|
||||
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
|
||||
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
|
||||
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
|
||||
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
|
||||
const val NOTIFICATION_FILTER_REPORTS = "notificationFilterReports"
|
||||
|
||||
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default.
|
||||
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
||||
|
|
|
|||
|
|
@ -3,21 +3,31 @@ package com.keylesspalace.tusky.usecase
|
|||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import at.connyduck.calladapter.networkresult.onSuccess
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.entity.NotificationPolicyEntity
|
||||
import com.keylesspalace.tusky.entity.NotificationPolicy
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import retrofit2.HttpException
|
||||
|
||||
class NotificationPolicyUsecase @Inject constructor(
|
||||
private val api: MastodonApi
|
||||
private val api: MastodonApi,
|
||||
private val db: AppDatabase,
|
||||
accountManager: AccountManager
|
||||
) {
|
||||
|
||||
private val accountId = accountManager.activeAccount!!.id
|
||||
|
||||
private val _state: MutableStateFlow<NotificationPolicyState> = MutableStateFlow(NotificationPolicyState.Loading)
|
||||
val state: StateFlow<NotificationPolicyState> = _state.asStateFlow()
|
||||
|
||||
val info: Flow<NotificationPolicyEntity?> = db.notificationPolicyDao().notificationPolicyForAccount(accountId)
|
||||
|
||||
suspend fun getNotificationPolicy() {
|
||||
_state.value.let { state ->
|
||||
if (state is NotificationPolicyState.Loaded) {
|
||||
|
|
@ -29,6 +39,13 @@ class NotificationPolicyUsecase @Inject constructor(
|
|||
|
||||
api.notificationPolicy().fold(
|
||||
{ policy ->
|
||||
db.notificationPolicyDao().update(
|
||||
NotificationPolicyEntity(
|
||||
tuskyAccountId = accountId,
|
||||
pendingRequestsCount = policy.summary.pendingRequestsCount,
|
||||
pendingNotificationsCount = policy.summary.pendingNotificationsCount,
|
||||
)
|
||||
)
|
||||
_state.value = NotificationPolicyState.Loaded(refreshing = false, policy = policy)
|
||||
},
|
||||
{ t ->
|
||||
|
|
@ -58,6 +75,9 @@ class NotificationPolicyUsecase @Inject constructor(
|
|||
_state.value = NotificationPolicyState.Loaded(false, notificationPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateCounts(notificationCount: Int) =
|
||||
db.notificationPolicyDao().updateCounts(accountId, notificationCount)
|
||||
}
|
||||
|
||||
sealed interface NotificationPolicyState {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.viewdata
|
||||
|
||||
import com.keylesspalace.tusky.entity.AccountWarning
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.RelationshipSeveranceEvent
|
||||
import com.keylesspalace.tusky.entity.Report
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
|
||||
|
|
@ -30,7 +32,9 @@ sealed class NotificationViewData {
|
|||
val type: Notification.Type,
|
||||
val account: TimelineAccount,
|
||||
val statusViewData: StatusViewData.Concrete?,
|
||||
val report: Report?
|
||||
val report: Report?,
|
||||
val event: RelationshipSeveranceEvent?,
|
||||
val moderationWarning: AccountWarning?
|
||||
) : NotificationViewData() {
|
||||
override fun asStatusOrNull() = statusViewData
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue