Add support for moderation report notifications (#2887)
* Add support for moderation report notifications * Translate report categories * Apply tint inside flag drawable * Remove unused imports Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
This commit is contained in:
parent
86e5c92a05
commit
6b95790457
18 changed files with 1257 additions and 11 deletions
|
@ -42,6 +42,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
@ -80,7 +81,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private static final int VIEW_TYPE_FOLLOW = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 4;
|
||||
private static final int VIEW_TYPE_UNKNOWN = 5;
|
||||
private static final int VIEW_TYPE_REPORT = 5;
|
||||
private static final int VIEW_TYPE_UNKNOWN = 6;
|
||||
|
||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||
|
@ -137,6 +139,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
.inflate(R.layout.item_status_placeholder, parent, false);
|
||||
return new PlaceholderViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_REPORT: {
|
||||
ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false);
|
||||
return new ReportNotificationViewHolder(binding);
|
||||
}
|
||||
default:
|
||||
case VIEW_TYPE_UNKNOWN: {
|
||||
View view = new View(parent.getContext());
|
||||
|
@ -252,6 +258,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case VIEW_TYPE_REPORT: {
|
||||
if (payloadForHolder == null) {
|
||||
ReportNotificationViewHolder holder = (ReportNotificationViewHolder) viewHolder;
|
||||
holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
|
||||
holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId());
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -304,6 +317,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case FOLLOW_REQUEST: {
|
||||
return VIEW_TYPE_FOLLOW_REQUEST;
|
||||
}
|
||||
case REPORT: {
|
||||
return VIEW_TYPE_REPORT;
|
||||
}
|
||||
default: {
|
||||
return VIEW_TYPE_UNKNOWN;
|
||||
}
|
||||
|
@ -322,6 +338,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
|
||||
void onViewStatusForNotificationId(String notificationId);
|
||||
|
||||
void onViewReport(String reportId);
|
||||
|
||||
void onExpandedChange(boolean expanded, int position);
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/* Copyright 2021 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.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener
|
||||
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
|
||||
import com.keylesspalace.tusky.entity.Report
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.TimestampUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import java.util.Date
|
||||
|
||||
class ReportNotificationViewHolder(
|
||||
private val binding: ItemReportNotificationBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis)
|
||||
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis)
|
||||
val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
|
||||
|
||||
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
|
||||
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, TimestampUtils.getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
|
||||
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
|
||||
|
||||
// Fancy avatar inset
|
||||
val padding = Utils.dpToPx(binding.notificationReporteeAvatar.context, 12)
|
||||
binding.notificationReporteeAvatar.setPaddingRelative(0, 0, padding, padding)
|
||||
|
||||
loadAvatar(
|
||||
report.targetAccount.avatar,
|
||||
binding.notificationReporteeAvatar,
|
||||
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp),
|
||||
animateAvatar,
|
||||
)
|
||||
loadAvatar(
|
||||
reporter.avatar,
|
||||
binding.notificationReporterAvatar,
|
||||
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),
|
||||
animateAvatar,
|
||||
)
|
||||
}
|
||||
|
||||
fun setupActionListener(listener: NotificationActionListener, reporteeId: String, reporterId: String, reportId: String) {
|
||||
binding.notificationReporteeAvatar.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onViewAccount(reporteeId)
|
||||
}
|
||||
}
|
||||
binding.notificationReporterAvatar.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onViewAccount(reporterId)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener { listener.onViewReport(reportId) }
|
||||
}
|
||||
|
||||
private fun getTranslatedCategory(context: Context, rawCategory: String): String {
|
||||
return when (rawCategory) {
|
||||
"violation" -> context.getString(R.string.report_category_violation)
|
||||
"spam" -> context.getString(R.string.report_category_spam)
|
||||
"other" -> context.getString(R.string.report_category_other)
|
||||
else -> rawCategory
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,6 +120,7 @@ public class NotificationHelper {
|
|||
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
|
||||
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
|
||||
public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
|
||||
public static final String CHANNEL_REPORT = "CHANNEL_REPORT";
|
||||
|
||||
/**
|
||||
* WorkManager Tag
|
||||
|
@ -401,6 +402,7 @@ public class NotificationHelper {
|
|||
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
|
||||
CHANNEL_SIGN_UP + account.getIdentifier(),
|
||||
CHANNEL_UPDATES + account.getIdentifier(),
|
||||
CHANNEL_REPORT + account.getIdentifier(),
|
||||
};
|
||||
int[] channelNames = {
|
||||
R.string.notification_mention_name,
|
||||
|
@ -412,6 +414,7 @@ public class NotificationHelper {
|
|||
R.string.notification_subscription_name,
|
||||
R.string.notification_sign_up_name,
|
||||
R.string.notification_update_name,
|
||||
R.string.notification_report_name,
|
||||
};
|
||||
int[] channelDescriptions = {
|
||||
R.string.notification_mention_descriptions,
|
||||
|
@ -423,6 +426,7 @@ public class NotificationHelper {
|
|||
R.string.notification_subscription_description,
|
||||
R.string.notification_sign_up_description,
|
||||
R.string.notification_update_description,
|
||||
R.string.notification_report_description,
|
||||
};
|
||||
|
||||
List<NotificationChannel> channels = new ArrayList<>(6);
|
||||
|
@ -564,6 +568,8 @@ public class NotificationHelper {
|
|||
return account.getNotificationsSignUps();
|
||||
case UPDATE:
|
||||
return account.getNotificationsUpdates();
|
||||
case REPORT:
|
||||
return account.getNotificationsReports();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -593,6 +599,10 @@ public class NotificationHelper {
|
|||
return CHANNEL_POLL + account.getIdentifier();
|
||||
case SIGN_UP:
|
||||
return CHANNEL_SIGN_UP + account.getIdentifier();
|
||||
case UPDATE:
|
||||
return CHANNEL_UPDATES + account.getIdentifier();
|
||||
case REPORT:
|
||||
return CHANNEL_REPORT + account.getIdentifier();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -678,6 +688,8 @@ public class NotificationHelper {
|
|||
return String.format(context.getString(R.string.notification_sign_up_format), accountName);
|
||||
case UPDATE:
|
||||
return String.format(context.getString(R.string.notification_update_format), accountName);
|
||||
case REPORT:
|
||||
return context.getString(R.string.notification_report_format, account.getDomain());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -715,6 +727,12 @@ public class NotificationHelper {
|
|||
}
|
||||
return builder.toString();
|
||||
}
|
||||
case REPORT:
|
||||
return context.getString(
|
||||
R.string.notification_header_report_format,
|
||||
StringUtils.unicodeWrap(notification.getAccount().getName()),
|
||||
StringUtils.unicodeWrap(notification.getReport().getTargetAccount().getName())
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -144,6 +144,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_reports)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_REPORTS
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsReports
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAccount { it.notificationsReports = newValue as Boolean }
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
|
||||
|
|
|
@ -54,6 +54,7 @@ data class AccountEntity(
|
|||
var notificationsSubscriptions: Boolean = true,
|
||||
var notificationsSignUps: Boolean = true,
|
||||
var notificationsUpdates: Boolean = true,
|
||||
var notificationsReports: Boolean = true,
|
||||
var notificationSound: Boolean = true,
|
||||
var notificationVibration: Boolean = true,
|
||||
var notificationLight: Boolean = true,
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
|||
*/
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 43)
|
||||
}, version = 44)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
@ -617,4 +617,11 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_43_44 = new Migration(43, 44) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsReports` INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class AppModule {
|
|||
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
|
||||
AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41,
|
||||
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43,
|
||||
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ data class Notification(
|
|||
val type: Type,
|
||||
val id: String,
|
||||
val account: TimelineAccount,
|
||||
val status: Status?
|
||||
val status: Status?,
|
||||
val report: Report?,
|
||||
) {
|
||||
|
||||
@JsonAdapter(NotificationTypeAdapter::class)
|
||||
|
@ -40,6 +41,7 @@ data class Notification(
|
|||
STATUS("status"),
|
||||
SIGN_UP("admin.sign_up"),
|
||||
UPDATE("update"),
|
||||
REPORT("admin.report"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
@ -52,7 +54,7 @@ data class Notification(
|
|||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE)
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE, REPORT)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
12
app/src/main/java/com/keylesspalace/tusky/entity/Report.kt
Normal file
12
app/src/main/java/com/keylesspalace/tusky/entity/Report.kt
Normal file
|
@ -0,0 +1,12 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.Date
|
||||
|
||||
data class Report(
|
||||
val id: String,
|
||||
val category: String,
|
||||
val status_ids: List<String>?,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
@SerializedName("target_account") val targetAccount: TimelineAccount,
|
||||
)
|
|
@ -80,6 +80,7 @@ import com.keylesspalace.tusky.settings.PrefKeys;
|
|||
import com.keylesspalace.tusky.util.CardViewMode;
|
||||
import com.keylesspalace.tusky.util.Either;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
|
||||
|
@ -711,6 +712,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
return getString(R.string.notification_sign_up_name);
|
||||
case UPDATE:
|
||||
return getString(R.string.notification_update_name);
|
||||
case REPORT:
|
||||
return getString(R.string.notification_report_name);
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
|
@ -800,6 +803,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
Log.w(TAG, "Didn't find a notification for ID: " + notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewReport(String reportId) {
|
||||
LinkHelper.openLink(getContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId));
|
||||
}
|
||||
|
||||
private void onPreferenceChanged(String key) {
|
||||
switch (key) {
|
||||
case "fabHide": {
|
||||
|
|
|
@ -62,6 +62,7 @@ object PrefKeys {
|
|||
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"
|
||||
|
|
|
@ -47,6 +47,7 @@ fun Notification.toViewData(
|
|||
this.type,
|
||||
this.id,
|
||||
this.account,
|
||||
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed)
|
||||
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed),
|
||||
this.report,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ package com.keylesspalace.tusky.viewdata;
|
|||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Report;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
|
||||
import java.util.Objects;
|
||||
|
@ -48,13 +48,16 @@ public abstract class NotificationViewData {
|
|||
private final TimelineAccount account;
|
||||
@Nullable
|
||||
private final StatusViewData.Concrete statusViewData;
|
||||
@Nullable
|
||||
private final Report report;
|
||||
|
||||
public Concrete(Notification.Type type, String id, TimelineAccount account,
|
||||
@Nullable StatusViewData.Concrete statusViewData) {
|
||||
@Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.account = account;
|
||||
this.statusViewData = statusViewData;
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public Notification.Type getType() {
|
||||
|
@ -74,6 +77,11 @@ public abstract class NotificationViewData {
|
|||
return statusViewData;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Report getReport() {
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewDataId() {
|
||||
return id.hashCode();
|
||||
|
@ -87,7 +95,8 @@ public abstract class NotificationViewData {
|
|||
return type == concrete.type &&
|
||||
Objects.equals(id, concrete.id) &&
|
||||
account.getId().equals(concrete.account.getId()) &&
|
||||
(Objects.equals(statusViewData, concrete.statusViewData));
|
||||
(Objects.equals(statusViewData, concrete.statusViewData)) &&
|
||||
(Objects.equals(report, concrete.report));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -97,7 +106,7 @@ public abstract class NotificationViewData {
|
|||
}
|
||||
|
||||
public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) {
|
||||
return new Concrete(type, id, account, statusViewData);
|
||||
return new Concrete(type, id, account, statusViewData, report);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue