Share filters with web client (#956)
* First step toward synchronized content filters * Add simple filter management UI * Remove old regex filter UI * More cleanup * Escape filter phrases when applying them via regex * Apply code review feedback * Fix live timeline update when filters change
This commit is contained in:
parent
463b008090
commit
5135daad2c
13 changed files with 439 additions and 91 deletions
|
@ -115,6 +115,7 @@
|
||||||
<activity android:name=".ListsActivity" />
|
<activity android:name=".ListsActivity" />
|
||||||
<activity android:name=".ModalTimelineActivity" />
|
<activity android:name=".ModalTimelineActivity" />
|
||||||
<activity android:name=".LicenseActivity" />
|
<activity android:name=".LicenseActivity" />
|
||||||
|
<activity android:name=".FiltersActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||||
|
|
||||||
|
|
180
app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt
Normal file
180
app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import kotlinx.android.synthetic.main.activity_filters.*
|
||||||
|
import kotlinx.android.synthetic.main.dialog_filter.*
|
||||||
|
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FiltersActivity: BaseActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var api: MastodonApi
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
|
private lateinit var context : String
|
||||||
|
private lateinit var filters: MutableList<Filter>
|
||||||
|
private lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FILTERS_CONTEXT = "filters_context"
|
||||||
|
const val FILTERS_TITLE = "filters_title"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFilter(filter: Filter, itemIndex: Int) {
|
||||||
|
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)
|
||||||
|
.enqueue(object: Callback<Filter>{
|
||||||
|
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
||||||
|
val updatedFilter = response.body()!!
|
||||||
|
if (updatedFilter.context.contains(context)) {
|
||||||
|
filters[itemIndex] = updatedFilter
|
||||||
|
} else {
|
||||||
|
filters.removeAt(itemIndex)
|
||||||
|
}
|
||||||
|
refreshFilterDisplay()
|
||||||
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteFilter(itemIndex: Int) {
|
||||||
|
val filter = filters[itemIndex]
|
||||||
|
if (filter.context.count() == 1) {
|
||||||
|
// This is the only context for this filter; delete it
|
||||||
|
api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback<ResponseBody> {
|
||||||
|
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
|
||||||
|
filters.removeAt(itemIndex)
|
||||||
|
refreshFilterDisplay()
|
||||||
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Keep the filter, but remove it from this context
|
||||||
|
val oldFilter = filters[itemIndex]
|
||||||
|
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||||
|
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
|
||||||
|
updateFilter(newFilter, itemIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFilter(phrase: String) {
|
||||||
|
api.createFilter(phrase, listOf(context), false, true, "").enqueue(object: Callback<Filter> {
|
||||||
|
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
||||||
|
filters.add(response.body()!!)
|
||||||
|
refreshFilterDisplay()
|
||||||
|
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
||||||
|
Toast.makeText(this@FiltersActivity, "Error creating filter '${phrase}'", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAddFilterDialog() {
|
||||||
|
dialog = AlertDialog.Builder(this@FiltersActivity)
|
||||||
|
.setTitle(R.string.filter_addition_dialog_title)
|
||||||
|
.setView(R.layout.dialog_filter)
|
||||||
|
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||||
|
createFilter(dialog.phraseEditText.text.toString())
|
||||||
|
}
|
||||||
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupEditDialogForItem(itemIndex: Int) {
|
||||||
|
dialog = AlertDialog.Builder(this@FiltersActivity)
|
||||||
|
.setTitle(R.string.filter_edit_dialog_title)
|
||||||
|
.setView(R.layout.dialog_filter)
|
||||||
|
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||||
|
val oldFilter = filters[itemIndex]
|
||||||
|
val newFilter = Filter(oldFilter.id, dialog.phraseEditText.text.toString(), oldFilter.context,
|
||||||
|
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
|
||||||
|
updateFilter(newFilter, itemIndex)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||||
|
deleteFilter(itemIndex)
|
||||||
|
}
|
||||||
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
// Need to show the dialog before referencing any elements from its view
|
||||||
|
dialog.phraseEditText.setText(filters[itemIndex].phrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshFilterDisplay() {
|
||||||
|
filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase })
|
||||||
|
filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFilters() {
|
||||||
|
api.filters.enqueue(object : Callback<List<Filter>> {
|
||||||
|
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
|
||||||
|
filters = response.body()!!.filter { filter -> filter.context.contains(context) }.toMutableList()
|
||||||
|
refreshFilterDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<Filter>>, t: Throwable) {
|
||||||
|
// Anything?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_filters)
|
||||||
|
setupToolbarBackArrow()
|
||||||
|
filter_floating_add.setOnClickListener {
|
||||||
|
showAddFilterDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
title = intent?.getStringExtra(FILTERS_TITLE)
|
||||||
|
context = intent?.getStringExtra(FILTERS_CONTEXT)!!
|
||||||
|
loadFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupToolbarBackArrow() {
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.run {
|
||||||
|
// Back button
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setDisplayShowHomeEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate back arrow in toolbar
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,4 +89,7 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity
|
abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesFiltersActivity(): FiltersActivity
|
||||||
|
|
||||||
}
|
}
|
47
app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt
Normal file
47
app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/* Copyright 2018 Levi Bard
|
||||||
|
*
|
||||||
|
* 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.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Filter (
|
||||||
|
val id: String,
|
||||||
|
val phrase: String,
|
||||||
|
val context: List<String>,
|
||||||
|
@SerializedName("expires_at") val expiresAt: String?,
|
||||||
|
val irreversible: Boolean,
|
||||||
|
@SerializedName("whole_word") val wholeWord: Boolean
|
||||||
|
) {
|
||||||
|
public companion object {
|
||||||
|
const val HOME = "home"
|
||||||
|
const val NOTIFICATIONS = "notifications"
|
||||||
|
const val PUBLIC = "public"
|
||||||
|
const val THREAD = "thread"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return id.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is Filter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val filter = other as Filter?
|
||||||
|
return filter?.id.equals(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -45,6 +46,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
|
import com.keylesspalace.tusky.entity.Filter;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
@ -64,7 +66,9 @@ import com.keylesspalace.tusky.view.BackgroundMessageView;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -312,6 +316,20 @@ public class TimelineFragment extends SFragment implements
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reloadFilters(boolean refresh) {
|
||||||
|
mastodonApi.getFilters().enqueue(new Callback<List<Filter>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<List<Filter>> call, Response<List<Filter>> response) {
|
||||||
|
applyFilters(response.body(), refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<List<Filter>> call, Throwable t) {
|
||||||
|
Log.e(TAG, "Error getting filters from server");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setupTimelinePreferences() {
|
private void setupTimelinePreferences() {
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||||
|
@ -325,16 +343,43 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
||||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
||||||
|
reloadFilters(false);
|
||||||
|
}
|
||||||
|
|
||||||
String regexFilter = preferences.getString("tabFilterRegex", "");
|
private static boolean filterContextMatchesKind(Kind kind, List<String> filterContext) {
|
||||||
filterRemoveRegex = (kind == Kind.HOME
|
// home, notifications, public, thread
|
||||||
|| kind == Kind.PUBLIC_LOCAL
|
switch(kind) {
|
||||||
|| kind == Kind.PUBLIC_FEDERATED)
|
case HOME:
|
||||||
&& !regexFilter.isEmpty();
|
return filterContext.contains(Filter.HOME);
|
||||||
|
case PUBLIC_FEDERATED:
|
||||||
|
case PUBLIC_LOCAL:
|
||||||
|
case TAG:
|
||||||
|
return filterContext.contains(Filter.PUBLIC);
|
||||||
|
case FAVOURITES:
|
||||||
|
return (filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS));
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String filterToRegexToken(Filter filter) {
|
||||||
|
String phrase = Pattern.quote(filter.getPhrase());
|
||||||
|
return filter.getWholeWord() ? String.format("\\b%s\\b", phrase) : phrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFilters(List<Filter> filters, boolean refresh) {
|
||||||
|
List<String> tokens = new ArrayList<>();
|
||||||
|
for (Filter filter : filters) {
|
||||||
|
if (filterContextMatchesKind(kind, filter.getContext())) {
|
||||||
|
tokens.add(filterToRegexToken(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterRemoveRegex = !tokens.isEmpty();
|
||||||
if (filterRemoveRegex) {
|
if (filterRemoveRegex) {
|
||||||
filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE)
|
filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher("");
|
||||||
.matcher("");
|
}
|
||||||
|
if (refresh) {
|
||||||
|
fullyRefresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -765,19 +810,12 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tabFilterRegex": {
|
case Filter.HOME:
|
||||||
boolean oldFilterRemoveRegex = filterRemoveRegex;
|
case Filter.NOTIFICATIONS:
|
||||||
String newFilterRemoveRegexPattern = sharedPreferences.getString("tabFilterRegex", "");
|
case Filter.THREAD:
|
||||||
boolean patternChanged;
|
case Filter.PUBLIC: {
|
||||||
if (filterRemoveRegexMatcher != null) {
|
if (filterContextMatchesKind(kind, Collections.singletonList(key))) {
|
||||||
patternChanged = !newFilterRemoveRegexPattern.equalsIgnoreCase(filterRemoveRegexMatcher.pattern().pattern());
|
reloadFilters(true);
|
||||||
} else {
|
|
||||||
patternChanged = !newFilterRemoveRegexPattern.isEmpty();
|
|
||||||
}
|
|
||||||
filterRemoveRegex = (kind == Kind.HOME || kind == Kind.PUBLIC_LOCAL || kind == Kind.PUBLIC_FEDERATED) && !newFilterRemoveRegexPattern.isEmpty();
|
|
||||||
if (oldFilterRemoveRegex != filterRemoveRegex || patternChanged) {
|
|
||||||
filterRemoveRegexMatcher = Pattern.compile(newFilterRemoveRegexPattern, Pattern.CASE_INSENSITIVE).matcher("");
|
|
||||||
fullyRefresh();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
|
@ -65,6 +66,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
private lateinit var defaultMediaSensitivityPreference: SwitchPreference
|
private lateinit var defaultMediaSensitivityPreference: SwitchPreference
|
||||||
private lateinit var alwaysShowSensitiveMediaPreference: SwitchPreference
|
private lateinit var alwaysShowSensitiveMediaPreference: SwitchPreference
|
||||||
private lateinit var mediaPreviewEnabledPreference: SwitchPreference
|
private lateinit var mediaPreviewEnabledPreference: SwitchPreference
|
||||||
|
private lateinit var homeFiltersPreference: Preference
|
||||||
|
private lateinit var notificationFiltersPreference: Preference
|
||||||
|
private lateinit var publicFiltersPreference: Preference
|
||||||
|
private lateinit var threadFiltersPreference: Preference
|
||||||
|
|
||||||
private val iconSize by lazy {resources.getDimensionPixelSize(R.dimen.preference_icon_size)}
|
private val iconSize by lazy {resources.getDimensionPixelSize(R.dimen.preference_icon_size)}
|
||||||
|
|
||||||
|
@ -79,6 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
defaultMediaSensitivityPreference = findPreference("defaultMediaSensitivity") as SwitchPreference
|
defaultMediaSensitivityPreference = findPreference("defaultMediaSensitivity") as SwitchPreference
|
||||||
mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") as SwitchPreference
|
mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") as SwitchPreference
|
||||||
alwaysShowSensitiveMediaPreference = findPreference("alwaysShowSensitiveMedia") as SwitchPreference
|
alwaysShowSensitiveMediaPreference = findPreference("alwaysShowSensitiveMedia") as SwitchPreference
|
||||||
|
homeFiltersPreference = findPreference("homeFilters")
|
||||||
|
notificationFiltersPreference = findPreference("notificationFilters")
|
||||||
|
publicFiltersPreference = findPreference("publicFilters")
|
||||||
|
threadFiltersPreference = findPreference("threadFilters")
|
||||||
|
|
||||||
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
|
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
|
||||||
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
|
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
|
||||||
|
@ -88,12 +97,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
tabPreference.onPreferenceClickListener = this
|
tabPreference.onPreferenceClickListener = this
|
||||||
mutedUsersPreference.onPreferenceClickListener = this
|
mutedUsersPreference.onPreferenceClickListener = this
|
||||||
blockedUsersPreference.onPreferenceClickListener = this
|
blockedUsersPreference.onPreferenceClickListener = this
|
||||||
|
homeFiltersPreference.onPreferenceClickListener = this
|
||||||
|
notificationFiltersPreference.onPreferenceClickListener = this
|
||||||
|
publicFiltersPreference.onPreferenceClickListener = this
|
||||||
|
threadFiltersPreference.onPreferenceClickListener = this
|
||||||
|
|
||||||
defaultPostPrivacyPreference.onPreferenceChangeListener = this
|
defaultPostPrivacyPreference.onPreferenceChangeListener = this
|
||||||
defaultMediaSensitivityPreference.onPreferenceChangeListener = this
|
defaultMediaSensitivityPreference.onPreferenceChangeListener = this
|
||||||
mediaPreviewEnabledPreference.onPreferenceChangeListener = this
|
mediaPreviewEnabledPreference.onPreferenceChangeListener = this
|
||||||
alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this
|
alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -144,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
|
|
||||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||||
|
|
||||||
when(preference) {
|
return when(preference) {
|
||||||
notificationPreference -> {
|
notificationPreference -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
|
@ -159,30 +171,42 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
tabPreference -> {
|
tabPreference -> {
|
||||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
mutedUsersPreference -> {
|
mutedUsersPreference -> {
|
||||||
val intent = Intent(context, AccountListActivity::class.java)
|
val intent = Intent(context, AccountListActivity::class.java)
|
||||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
blockedUsersPreference -> {
|
blockedUsersPreference -> {
|
||||||
val intent = Intent(context, AccountListActivity::class.java)
|
val intent = Intent(context, AccountListActivity::class.java)
|
||||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
return true
|
true
|
||||||
|
}
|
||||||
|
homeFiltersPreference -> {
|
||||||
|
launchFilterActivity(Filter.HOME, R.string.title_home)
|
||||||
|
}
|
||||||
|
notificationFiltersPreference -> {
|
||||||
|
launchFilterActivity(Filter.NOTIFICATIONS, R.string.title_notifications)
|
||||||
|
}
|
||||||
|
publicFiltersPreference -> {
|
||||||
|
launchFilterActivity(Filter.PUBLIC, R.string.pref_title_public_filter_keywords)
|
||||||
|
}
|
||||||
|
threadFiltersPreference -> {
|
||||||
|
launchFilterActivity(Filter.THREAD, R.string.pref_title_thread_filter_keywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -249,6 +273,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
return drawable
|
return drawable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun launchFilterActivity(filterContext: String, titleResource: Int): Boolean {
|
||||||
|
val intent = Intent(context, FiltersActivity::class.java)
|
||||||
|
intent.putExtra(FiltersActivity.FILTERS_CONTEXT, filterContext)
|
||||||
|
intent.putExtra(FiltersActivity.FILTERS_TITLE, getString(titleResource))
|
||||||
|
activity?.startActivity(intent)
|
||||||
|
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): AccountPreferencesFragment {
|
fun newInstance(): AccountPreferencesFragment {
|
||||||
return AccountPreferencesFragment()
|
return AccountPreferencesFragment()
|
||||||
|
|
|
@ -17,14 +17,8 @@ package com.keylesspalace.tusky.fragment.preference
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.widget.EditText
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
@ -32,50 +26,7 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.timeline_filter_preferences)
|
addPreferencesFromResource(R.xml.timeline_filter_preferences)
|
||||||
|
|
||||||
sharedPreferences = preferenceManager.sharedPreferences
|
sharedPreferences = preferenceManager.sharedPreferences
|
||||||
|
|
||||||
val regexPref: Preference = findPreference("tabFilterRegex")
|
|
||||||
|
|
||||||
regexPref.summary = sharedPreferences.getString("tabFilterRegex", "")
|
|
||||||
regexPref.setOnPreferenceClickListener {
|
|
||||||
|
|
||||||
val editText = EditText(requireContext())
|
|
||||||
editText.setText(sharedPreferences.getString("tabFilterRegex", ""))
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.pref_title_filter_regex)
|
|
||||||
.setView(editText)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
sharedPreferences
|
|
||||||
.edit()
|
|
||||||
.putString("tabFilterRegex", editText.text.toString())
|
|
||||||
.apply()
|
|
||||||
regexPref.summary = editText.text.toString()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
|
|
||||||
editText.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun afterTextChanged(newRegex: Editable) {
|
|
||||||
try {
|
|
||||||
Pattern.compile(newRegex.toString())
|
|
||||||
editText.error = null
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
editText.error = getString(R.string.error_invalid_regex)
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTextChanged(s1: CharSequence, start: Int, count: Int, after: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(s1: CharSequence, start: Int, before: Int, count: Int) {}
|
|
||||||
})
|
|
||||||
dialog.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Card;
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Conversation;
|
import com.keylesspalace.tusky.entity.Conversation;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
|
import com.keylesspalace.tusky.entity.Filter;
|
||||||
import com.keylesspalace.tusky.entity.Instance;
|
import com.keylesspalace.tusky.entity.Instance;
|
||||||
import com.keylesspalace.tusky.entity.MastoList;
|
import com.keylesspalace.tusky.entity.MastoList;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
@ -346,4 +347,37 @@ public interface MastodonApi {
|
||||||
|
|
||||||
@GET("/api/v1/conversations")
|
@GET("/api/v1/conversations")
|
||||||
Call<List<Conversation>> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
|
Call<List<Conversation>> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
|
||||||
|
@GET("api/v1/filters")
|
||||||
|
Call<List<Filter>> getFilters();
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("api/v1/filters")
|
||||||
|
Call<Filter> createFilter(
|
||||||
|
@Field("phrase") String phrase,
|
||||||
|
@Field("context[]") List<String> context,
|
||||||
|
@Field("irreversible") Boolean irreversible,
|
||||||
|
@Field("whole_word") Boolean wholeWord,
|
||||||
|
@Field("expires_in") String expiresIn
|
||||||
|
);
|
||||||
|
|
||||||
|
@GET("api/v1/filters/{id}")
|
||||||
|
Call<Filter> getFilter(
|
||||||
|
@Path("id") String id
|
||||||
|
);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("api/v1/filters/{id}")
|
||||||
|
Call<Filter> updateFilter(
|
||||||
|
@Path("id") String id,
|
||||||
|
@Field("phrase") String phrase,
|
||||||
|
@Field("context[]") List<String> context,
|
||||||
|
@Field("irreversible") Boolean irreversible,
|
||||||
|
@Field("whole_word") Boolean wholeWord,
|
||||||
|
@Field("expires_in") String expiresIn
|
||||||
|
);
|
||||||
|
|
||||||
|
@DELETE("api/v1/filters/{id}")
|
||||||
|
Call<ResponseBody> deleteFilter(
|
||||||
|
@Path("id") String id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
30
app/src/main/res/layout/activity_filters.xml
Normal file
30
app/src/main/res/layout/activity_filters.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/activityFilters"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.keylesspalace.tusky.FiltersActivity">
|
||||||
|
|
||||||
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/filtersView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/filter_floating_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/filter_addition_dialog_title"
|
||||||
|
app:layout_anchor="@id/filtersView"
|
||||||
|
app:layout_anchorGravity="bottom|end"
|
||||||
|
android:src="@drawable/ic_plus_24dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
18
app/src/main/res/layout/dialog_filter.xml
Normal file
18
app/src/main/res/layout/dialog_filter.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?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"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/phraseEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingEnd="24dp"
|
||||||
|
android:paddingStart="24dp"
|
||||||
|
android:hint="@string/filter_add_description"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -199,6 +199,7 @@
|
||||||
<string name="pref_title_appearance_settings">Appearance</string>
|
<string name="pref_title_appearance_settings">Appearance</string>
|
||||||
<string name="pref_title_app_theme">App Theme</string>
|
<string name="pref_title_app_theme">App Theme</string>
|
||||||
<string name="pref_title_timelines">Timelines</string>
|
<string name="pref_title_timelines">Timelines</string>
|
||||||
|
<string name="pref_title_timeline_filters">Filters</string>
|
||||||
|
|
||||||
<string-array name="app_theme_names">
|
<string-array name="app_theme_names">
|
||||||
<item>Dark</item>
|
<item>Dark</item>
|
||||||
|
@ -216,7 +217,6 @@
|
||||||
<string name="pref_title_status_tabs">Tabs</string>
|
<string name="pref_title_status_tabs">Tabs</string>
|
||||||
<string name="pref_title_show_boosts">Show boosts</string>
|
<string name="pref_title_show_boosts">Show boosts</string>
|
||||||
<string name="pref_title_show_replies">Show replies</string>
|
<string name="pref_title_show_replies">Show replies</string>
|
||||||
<string name="pref_title_filter_regex">Filter out by regular expressions</string>
|
|
||||||
<string name="pref_title_show_media_preview">Download media previews</string>
|
<string name="pref_title_show_media_preview">Download media previews</string>
|
||||||
<string name="pref_title_proxy_settings">Proxy</string>
|
<string name="pref_title_proxy_settings">Proxy</string>
|
||||||
<string name="pref_title_http_proxy_settings">HTTP proxy</string>
|
<string name="pref_title_http_proxy_settings">HTTP proxy</string>
|
||||||
|
@ -319,6 +319,14 @@
|
||||||
<string name="replying_to">Replying to @%s</string>
|
<string name="replying_to">Replying to @%s</string>
|
||||||
<string name="load_more_placeholder_text">load more</string>
|
<string name="load_more_placeholder_text">load more</string>
|
||||||
|
|
||||||
|
<string name="pref_title_public_filter_keywords">Public timelines</string>
|
||||||
|
<string name="pref_title_thread_filter_keywords">Conversations</string>
|
||||||
|
<string name="filter_addition_dialog_title">Add filter</string>
|
||||||
|
<string name="filter_edit_dialog_title">Edit filter</string>
|
||||||
|
<string name="filter_dialog_remove_button">Remove</string>
|
||||||
|
<string name="filter_dialog_update_button">Update</string>
|
||||||
|
<string name="filter_add_description">Phrase to filter</string>
|
||||||
|
|
||||||
<string name="add_account_name">Add Account</string>
|
<string name="add_account_name">Add Account</string>
|
||||||
<string name="add_account_description">Add new Mastodon Account</string>
|
<string name="add_account_description">Add new Mastodon Account</string>
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,23 @@
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="alwaysShowSensitiveMedia"
|
android:key="alwaysShowSensitiveMedia"
|
||||||
android:title="@string/pref_title_alway_show_sensitive_media" />
|
android:title="@string/pref_title_alway_show_sensitive_media" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
<PreferenceCategory android:title="@string/pref_title_timeline_filters">
|
||||||
|
<Preference
|
||||||
|
android:key="publicFilters"
|
||||||
|
android:title="@string/pref_title_public_filter_keywords"
|
||||||
|
/>
|
||||||
|
<Preference
|
||||||
|
android:key="notificationFilters"
|
||||||
|
android:title="@string/title_notifications"
|
||||||
|
/>
|
||||||
|
<Preference
|
||||||
|
android:key="homeFilters"
|
||||||
|
android:title="@string/title_home"
|
||||||
|
/>
|
||||||
|
<Preference
|
||||||
|
android:key="threadFilters"
|
||||||
|
android:title="@string/pref_title_thread_filter_keywords"
|
||||||
|
/>
|
||||||
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
|
@ -17,15 +17,4 @@
|
||||||
android:title="@string/pref_title_show_replies"
|
android:title="@string/pref_title_show_replies"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
|
||||||
android:title="@string/title_advanced"
|
|
||||||
app:iconSpaceReserved="false">
|
|
||||||
<Preference
|
|
||||||
android:inputType="textNoSuggestions"
|
|
||||||
android:key="tabFilterRegex"
|
|
||||||
android:title="@string/pref_title_filter_regex"
|
|
||||||
app:iconSpaceReserved="false" />
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in a new issue