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
1399
app/schemas/com.keylesspalace.tusky.db.AppDatabase/68.json
Normal file
1399
app/schemas/com.keylesspalace.tusky.db.AppDatabase/68.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
10
app/src/main/res/drawable/heart_broken_24.xml
Normal file
10
app/src/main/res/drawable/heart_broken_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.5,3c-0.96,0 -1.9,0.25 -2.73,0.69L12,9h3l-3,10l1,-9h-3l1.54,-5.39C10.47,3.61 9.01,3 7.5,3C4.42,3 2,5.42 2,8.5c0,4.13 4.16,7.18 10,12.5c5.47,-4.94 10,-8.26 10,-12.5C22,5.42 19.58,3 16.5,3z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/help_24dp.xml
Normal file
11
app/src/main/res/drawable/help_24dp.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M478,720Q499,720 513.5,705.5Q528,691 528,670Q528,649 513.5,634.5Q499,620 478,620Q457,620 442.5,634.5Q428,649 428,670Q428,691 442.5,705.5Q457,720 478,720ZM442,566L516,566Q516,533 523.5,514Q531,495 566,462Q592,436 607,412.5Q622,389 622,356Q622,300 581,270Q540,240 484,240Q427,240 391.5,270Q356,300 342,342L408,368Q413,350 430.5,329Q448,308 484,308Q516,308 532,325.5Q548,343 548,364Q548,384 536,401.5Q524,419 506,434Q462,473 452,493Q442,513 442,566ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_at_18dp.xml
Normal file
10
app/src/main/res/drawable/ic_at_18dp.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:textColorTertiary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,1.95c-5.52,0 -10,4.48 -10,10s4.48,10 10,10h5v-2h-5c-4.34,0 -8,-3.66 -8,-8s3.66,-8 8,-8 8,3.66 8,8v1.43c0,0.79 -0.71,1.57 -1.5,1.57s-1.5,-0.78 -1.5,-1.57v-1.43c0,-2.76 -2.24,-5 -5,-5s-5,2.24 -5,5 2.24,5 5,5c1.38,0 2.64,-0.56 3.54,-1.47 0.65,0.89 1.77,1.47 2.96,1.47 1.97,0 3.5,-1.6 3.5,-3.57v-1.43c0,-5.52 -4.48,-10 -10,-10zM12,14.95c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_gavel_24dp.xml
Normal file
11
app/src/main/res/drawable/ic_gavel_24dp.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,840L160,760L640,760L640,840L160,840ZM386,646L160,420L244,334L472,560L386,646ZM640,392L414,164L500,80L726,306L640,392ZM824,800L302,278L358,222L880,744L824,800Z" />
|
||||
</vector>
|
||||
|
|
@ -6,5 +6,5 @@
|
|||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorTertiary"
|
||||
android:pathData="M7,8L7,5l-7,7 7,7v-3l-4,-4 4,-4zM13,9L13,5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||
android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||
</vector>
|
||||
|
|
@ -4,13 +4,12 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:paddingBottom="10dp">
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_text"
|
||||
android:id="@+id/notificationText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
|
|
@ -27,60 +26,66 @@
|
|||
tools:text="Someone followed you" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_avatar"
|
||||
android:id="@+id/notificationAvatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentStart="false"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_marginTop="10dp"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:scaleType="centerCrop"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notification_text"
|
||||
app:layout_constraintTop_toBottomOf="@id/notificationText"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatarBadge"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@string/profile_badge_bot_text"
|
||||
app:layout_constraintBottom_toBottomOf="@id/notificationAvatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/notificationAvatar"
|
||||
app:srcCompat="@drawable/bot_badge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_display_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/notificationDisplayName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingEnd="@dimen/status_display_name_padding_end"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="normal|bold"
|
||||
app:layout_constraintStart_toEndOf="@id/notification_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notification_text"
|
||||
tools:text="Test User"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/notificationAvatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/notificationText"
|
||||
tools:text="Display name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/notificationUsername"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintStart_toEndOf="@+id/notification_display_name"
|
||||
app:layout_constraintTop_toTopOf="@+id/notification_display_name"
|
||||
tools:text="\@testuser" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/notificationDisplayName"
|
||||
tools:text="\@username" />
|
||||
|
||||
<com.keylesspalace.tusky.view.ClickableSpanTextView
|
||||
android:id="@+id/notification_account_note"
|
||||
android:id="@+id/accountNote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/notification_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notification_display_name"
|
||||
app:layout_constraintStart_toStartOf="@+id/notificationUsername"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notificationUsername"
|
||||
tools:text="Account note" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@
|
|||
app:drawableStartCompat="@drawable/ic_person_add_24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Someone requested to follow you"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="Someone requested to follow you" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
|
|
@ -109,10 +109,9 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textIsSelectable="true"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="@+id/acceptButton"
|
||||
app:layout_constraintStart_toStartOf="@+id/usernameTextView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rejectButton"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/moderation_warning_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="28dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_gavel_24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?android:textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/moderation_warning_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:text="@string/moderation_warning"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/moderation_warning_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/moderation_warning_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/moderation_warning_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/moderation_warning_text"
|
||||
tools:text="@string/moderation_warning_action_none" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/severed_relationship_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="28dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/heart_broken_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?android:textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/severed_relationship_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/severed_relationship_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="You have blocked mastodon.example, removing 2 of your followers and 7 accounts you follow." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -140,6 +140,25 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/notification_content_warning_button"
|
||||
tools:text="Example status here" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_attachment_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:drawableTint="?android:textColorTertiary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/notification_status_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_content"
|
||||
tools:drawableStart="@drawable/ic_attach_file_24dp"
|
||||
tools:text="3 media attachments" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_toggle_notification_content"
|
||||
style="@style/TuskyButton.Outlined"
|
||||
|
|
@ -154,10 +173,8 @@
|
|||
android:paddingRight="16dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@id/notification_status_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_content"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_attachment_info"
|
||||
tools:text="@string/post_content_show_less" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- paddingStart = 14dp+48dp+14dp = 76dp to align with avatars of other notifications -->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unknownNotificationTypeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:layout_marginTop="8dp"
|
||||
android:drawablePadding="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="28dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:paddingStart="76dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/unknown_notification_type"/>
|
||||
app:drawableStartCompat="@drawable/help_24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:text="@string/unknown_notification_type" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unknownNotificationType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="62dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/unknownNotificationTypeTitle"
|
||||
tools:text="unknown_type" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -287,17 +287,8 @@
|
|||
<string name="pref_title_notification_alert_sound">Notify with a sound</string>
|
||||
<string name="pref_title_notification_alert_vibrate">Notify with vibration</string>
|
||||
<string name="pref_title_notification_alert_light">Notify with light</string>
|
||||
<string name="pref_title_notification_filters">Notify me when</string>
|
||||
<string name="pref_title_notification_filter_mentions">mentioned</string>
|
||||
<string name="pref_title_notification_filter_follows">followed</string>
|
||||
<string name="pref_title_notification_filter_follow_requests">follow requested</string>
|
||||
<string name="pref_title_notification_filter_reblogs">my posts are boosted</string>
|
||||
<string name="pref_title_notification_filter_favourites">my posts are favorited</string>
|
||||
<string name="pref_title_notification_filter_poll">polls have ended</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">somebody I\'m subscribed to published a new post</string>
|
||||
<string name="pref_title_notification_filter_sign_ups">somebody signed up</string>
|
||||
<string name="pref_title_notification_filter_updates">a post I\'ve interacted with is edited</string>
|
||||
<string name="pref_title_notification_filter_reports">there\'s a new report</string>
|
||||
<string name="pref_title_notification_filters">Enabled notification types</string>
|
||||
|
||||
<string name="pref_title_appearance_settings">Appearance</string>
|
||||
<string name="pref_title_app_theme">App theme</string>
|
||||
<string name="pref_title_timelines">Timelines</string>
|
||||
|
|
@ -376,7 +367,7 @@
|
|||
<string name="notification_boost_description">Notifications when your posts get boosted</string>
|
||||
<string name="notification_favourite_name">Favorites</string>
|
||||
<string name="notification_favourite_description">Notifications when your posts get marked as favorite</string>
|
||||
<string name="notification_poll_name">Polls</string>
|
||||
<string name="notification_poll_name">Poll results</string>
|
||||
<string name="notification_poll_description">Notifications about polls that have ended</string>
|
||||
<string name="notification_subscription_name">New posts</string>
|
||||
<string name="notification_subscription_description">Notifications when somebody you\'re subscribed to published a new post</string>
|
||||
|
|
@ -385,10 +376,19 @@
|
|||
<string name="notification_update_name">Post edits</string>
|
||||
<string name="notification_update_description">Notifications when posts you\'ve interacted with are edited</string>
|
||||
<string name="notification_report_name">Reports</string>
|
||||
<string name="notification_severed_relationship_name">Lost connections</string>
|
||||
<string name="notification_moderation_warning_name">Moderation warnings</string>
|
||||
<string name="notification_report_description">Notifications about moderation reports</string>
|
||||
<string name="notification_listenable_worker_name">Background activity</string>
|
||||
<string name="notification_listenable_worker_description">Notifications when Tusky is working in the background</string>
|
||||
<string name="notification_unknown_name">Unknown</string>
|
||||
<string name="notification_unknown_description">Unknown notification type</string>
|
||||
|
||||
<string name="notification_channel_other">Other</string>
|
||||
<string name="notification_channel_other_description">Other notifications, like lost connections and moderation warnings</string>
|
||||
|
||||
<string name="notification_channel_admin">Admin</string>
|
||||
<string name="notification_channel_admin_description">Admin notifications, like new reports or signups</string>
|
||||
|
||||
<string name="notification_mention_format">%1$s mentioned you</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>
|
||||
|
|
@ -877,4 +877,35 @@
|
|||
<string name="action_accept_notification_request">Accept</string>
|
||||
<string name="action_dismiss_notification_request">Dismiss</string>
|
||||
|
||||
<string name="relationship_severance_event_title">Connection lost</string>
|
||||
<string name="relationship_severance_event_account_suspension">An admin from %1$s has suspended %2$s, which means you can no longer receive updates from them or interact with them.</string>
|
||||
<string name="relationship_severance_event_domain_block">An admin from %1$s has blocked %2$s, including %3$s of your followers and %4$s you follow.</string>
|
||||
<string name="relationship_severance_event_user_domain_block">You have blocked %1$s, removing %2$s of your followers and %3$s you follow.</string>
|
||||
<plurals name="accounts">
|
||||
<item quantity="one">%1$s account</item>
|
||||
<item quantity="other">%1$s accounts</item>
|
||||
</plurals>
|
||||
|
||||
<string name="moderation_warning">You have received a moderation warning.</string>
|
||||
|
||||
<string name="moderation_warning_action_delete_statuses">Some of your posts have been removed.</string>
|
||||
<string name="moderation_warning_action_disable">Your account has been disabled.</string>
|
||||
<string name="moderation_warning_action_mark_statuses_as_sensitive">Some of your posts have been marked as sensitive.</string>
|
||||
<string name="moderation_warning_action_none">Your account has received a moderation warning.</string>
|
||||
<string name="moderation_warning_action_sensitive">Your posts will be marked as sensitive from now on.</string>
|
||||
<string name="moderation_warning_action_silence">Your account has been limited.</string>
|
||||
<string name="moderation_warning_action_suspend">Your account has been suspended.</string>
|
||||
|
||||
<string name="unknown_notification_type_explanation">Tusky does not recognize this notification type and can\'t show it to you. Your server\'s web interface may be able to do so. Please also inform the Tusky developers so they can add support for this type! </string>
|
||||
|
||||
<string name="poll">Poll</string>
|
||||
<plurals name="media_attachments">
|
||||
<item quantity="one">%1$s media attachment</item>
|
||||
<item quantity="other">%1$s media attachments</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notification_info_reply">Reply</string>
|
||||
<string name="notification_info_private_mention">Private mention</string>
|
||||
<string name="notification_info_mention">Mention</string>
|
||||
<string name="notification_info_private_reply">Private reply</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class MainActivityTest {
|
|||
|
||||
@Test
|
||||
fun `clicking notification of type FOLLOW shows notification tab`() {
|
||||
val intent = showNotification(Notification.Type.FOLLOW)
|
||||
val intent = showNotification(Notification.Type.Follow)
|
||||
|
||||
val activity = startMainActivity(intent)
|
||||
val currentTab = activity.findViewById<ViewPager2>(R.id.viewPager).currentItem
|
||||
|
|
@ -86,7 +86,7 @@ class MainActivityTest {
|
|||
|
||||
@Test
|
||||
fun `clicking notification of type FOLLOW_REQUEST shows follow requests`() {
|
||||
val intent = showNotification(Notification.Type.FOLLOW_REQUEST)
|
||||
val intent = showNotification(Notification.Type.FollowRequest)
|
||||
|
||||
val activity = startMainActivity(intent)
|
||||
val nextActivity = shadowOf(activity).peekNextStartedActivity()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import java.util.Date
|
|||
import org.junit.Assert.assertEquals
|
||||
|
||||
fun fakeNotification(
|
||||
type: Notification.Type = Notification.Type.FAVOURITE,
|
||||
type: Notification.Type = Notification.Type.Favourite,
|
||||
id: String = "1",
|
||||
account: TimelineAccount = fakeAccount(id = id),
|
||||
status: Status? = fakeStatus(id = id),
|
||||
|
|
@ -61,7 +61,9 @@ fun Notification.toNotificationDataEntity(
|
|||
),
|
||||
statusAccount = status?.account?.toEntity(tuskyAccountId),
|
||||
report = report?.toEntity(tuskyAccountId),
|
||||
reportTargetAccount = report?.targetAccount?.toEntity(tuskyAccountId)
|
||||
reportTargetAccount = report?.targetAccount?.toEntity(tuskyAccountId),
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
)
|
||||
|
||||
fun Placeholder.toNotificationDataEntity(
|
||||
|
|
@ -74,7 +76,9 @@ fun Placeholder.toNotificationDataEntity(
|
|||
status = null,
|
||||
statusAccount = null,
|
||||
report = null,
|
||||
reportTargetAccount = null
|
||||
reportTargetAccount = null,
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
)
|
||||
|
||||
suspend fun AppDatabase.insert(notifications: List<Notification>, tuskyAccountId: Long = 1) = withTransaction {
|
||||
|
|
@ -115,6 +119,8 @@ suspend fun AppDatabase.insert(notifications: List<Notification>, tuskyAccountId
|
|||
accountId = notification.account.id,
|
||||
statusId = notification.status?.id,
|
||||
reportId = notification.report?.id,
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
loading = false
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -206,11 +206,11 @@ class NotificationsDaoTest {
|
|||
// will be removed because it references a status by account 1
|
||||
fakeNotification(id = "2", account = fakeAccount(id = "2"), status = fakeStatus(id = "2", authorServerId = "1")),
|
||||
// will not be removed because they are admin notifications
|
||||
fakeNotification(type = Notification.Type.REPORT, id = "3", account = fakeAccount(id = "3"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "1"))),
|
||||
fakeNotification(type = Notification.Type.SIGN_UP, id = "4", account = fakeAccount(id = "1"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "4"))),
|
||||
fakeNotification(type = Notification.Type.Report, id = "3", account = fakeAccount(id = "3"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "1"))),
|
||||
fakeNotification(type = Notification.Type.SignUp, id = "4", account = fakeAccount(id = "1"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "4"))),
|
||||
// will not be removed because it does not reference account 1
|
||||
fakeNotification(id = "5", account = fakeAccount(id = "5"), status = fakeStatus(id = "5", authorServerId = "100")),
|
||||
fakeNotification(type = Notification.Type.FOLLOW, id = "6", account = fakeAccount(id = "1"), status = null)
|
||||
fakeNotification(type = Notification.Type.Follow, id = "6", account = fakeAccount(id = "1"), status = null)
|
||||
)
|
||||
|
||||
db.insert(notificationsAccount1, tuskyAccountId = 1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue