From 5135daad2ccf42c6bea573f3efe01e0d7010a2d9 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Wed, 20 Mar 2019 19:25:26 +0100 Subject: [PATCH] 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 --- app/src/main/AndroidManifest.xml | 1 + .../keylesspalace/tusky/FiltersActivity.kt | 180 ++++++++++++++++++ .../tusky/di/ActivitiesModule.kt | 5 +- .../com/keylesspalace/tusky/entity/Filter.kt | 47 +++++ .../tusky/fragment/TimelineFragment.java | 78 ++++++-- .../preference/AccountPreferencesFragment.kt | 47 ++++- .../TabFilterPreferencesFragment.kt | 49 ----- .../tusky/network/MastodonApi.java | 34 ++++ app/src/main/res/layout/activity_filters.xml | 30 +++ app/src/main/res/layout/dialog_filter.xml | 18 ++ app/src/main/res/values/strings.xml | 10 +- app/src/main/res/xml/account_preferences.xml | 20 +- .../res/xml/timeline_filter_preferences.xml | 11 -- 13 files changed, 439 insertions(+), 91 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt create mode 100644 app/src/main/res/layout/activity_filters.xml create mode 100644 app/src/main/res/layout/dialog_filter.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 40397a94b..59bc17ecc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,6 +115,7 @@ + diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt new file mode 100644 index 000000000..eafad499f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -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 + 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{ + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + 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 { + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + 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 { + override fun onResponse(call: Call, response: Response) { + filters.add(response.body()!!) + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) + } + + override fun onFailure(call: Call, 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> { + override fun onResponse(call: Call>, response: Response>) { + filters = response.body()!!.filter { filter -> filter.context.contains(context) }.toMutableList() + refreshFilterDisplay() + } + + override fun onFailure(call: Call>, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index a91a2da1f..9ad5e0863 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -89,4 +89,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity -} \ No newline at end of file + @ContributesAndroidInjector + abstract fun contributesFiltersActivity(): FiltersActivity + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt new file mode 100644 index 000000000..e0440be81 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -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 . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class Filter ( + val id: String, + val phrase: String, + val context: List, + @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) + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 0a3807cf0..edc415c0b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -45,6 +46,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent; import com.keylesspalace.tusky.appstore.UnfollowEvent; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.ActionButtonActivity; 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.viewdata.StatusViewData; +import java.util.ArrayList; import java.io.IOException; +import java.util.Collections; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -312,6 +316,20 @@ public class TimelineFragment extends SFragment implements }); } + private void reloadFilters(boolean refresh) { + mastodonApi.getFilters().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + applyFilters(response.body(), refresh); + } + + @Override + public void onFailure(Call> call, Throwable t) { + Log.e(TAG, "Error getting filters from server"); + } + }); + } + private void setupTimelinePreferences() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); @@ -325,16 +343,43 @@ public class TimelineFragment extends SFragment implements filter = preferences.getBoolean("tabFilterHomeBoosts", true); filterRemoveReblogs = kind == Kind.HOME && !filter; + reloadFilters(false); + } - String regexFilter = preferences.getString("tabFilterRegex", ""); - filterRemoveRegex = (kind == Kind.HOME - || kind == Kind.PUBLIC_LOCAL - || kind == Kind.PUBLIC_FEDERATED) - && !regexFilter.isEmpty(); + private static boolean filterContextMatchesKind(Kind kind, List filterContext) { + // home, notifications, public, thread + switch(kind) { + case HOME: + 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 filters, boolean refresh) { + List tokens = new ArrayList<>(); + for (Filter filter : filters) { + if (filterContextMatchesKind(kind, filter.getContext())) { + tokens.add(filterToRegexToken(filter)); + } + } + filterRemoveRegex = !tokens.isEmpty(); if (filterRemoveRegex) { - filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE) - .matcher(""); + filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher(""); + } + if (refresh) { + fullyRefresh(); } } @@ -765,19 +810,12 @@ public class TimelineFragment extends SFragment implements } break; } - case "tabFilterRegex": { - boolean oldFilterRemoveRegex = filterRemoveRegex; - String newFilterRemoveRegexPattern = sharedPreferences.getString("tabFilterRegex", ""); - boolean patternChanged; - if (filterRemoveRegexMatcher != null) { - patternChanged = !newFilterRemoveRegexPattern.equalsIgnoreCase(filterRemoveRegexMatcher.pattern().pattern()); - } 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(); + case Filter.HOME: + case Filter.NOTIFICATIONS: + case Filter.THREAD: + case Filter.PUBLIC: { + if (filterContextMatchesKind(kind, Collections.singletonList(key))) { + reloadFilters(true); } break; } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt index 7ce075ae6..3c7ea6d99 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.ThemeUtils @@ -65,6 +66,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), private lateinit var defaultMediaSensitivityPreference: SwitchPreference private lateinit var alwaysShowSensitiveMediaPreference: 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)} @@ -79,6 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), defaultMediaSensitivityPreference = findPreference("defaultMediaSensitivity") as SwitchPreference mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") 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)) mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp) @@ -88,12 +97,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), tabPreference.onPreferenceClickListener = this mutedUsersPreference.onPreferenceClickListener = this blockedUsersPreference.onPreferenceClickListener = this + homeFiltersPreference.onPreferenceClickListener = this + notificationFiltersPreference.onPreferenceClickListener = this + publicFiltersPreference.onPreferenceClickListener = this + threadFiltersPreference.onPreferenceClickListener = this defaultPostPrivacyPreference.onPreferenceChangeListener = this defaultMediaSensitivityPreference.onPreferenceChangeListener = this mediaPreviewEnabledPreference.onPreferenceChangeListener = this alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -144,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), override fun onPreferenceClick(preference: Preference): Boolean { - when(preference) { + return when(preference) { notificationPreference -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val intent = Intent() @@ -159,30 +171,42 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), } } - return true + true } tabPreference -> { val intent = Intent(context, TabPreferenceActivity::class.java) activity?.startActivity(intent) activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) - return true + true } mutedUsersPreference -> { val intent = Intent(context, AccountListActivity::class.java) intent.putExtra("type", AccountListActivity.Type.MUTES) activity?.startActivity(intent) activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) - return true + true } blockedUsersPreference -> { val intent = Intent(context, AccountListActivity::class.java) intent.putExtra("type", AccountListActivity.Type.BLOCKS) activity?.startActivity(intent) 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 } + 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 { fun newInstance(): AccountPreferencesFragment { return AccountPreferencesFragment() diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt index 3199f9f12..8d92d54bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt @@ -17,14 +17,8 @@ package com.keylesspalace.tusky.fragment.preference import android.content.SharedPreferences import android.os.Bundle -import androidx.appcompat.app.AlertDialog 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 java.util.regex.Pattern class TabFilterPreferencesFragment : PreferenceFragmentCompat() { @@ -32,50 +26,7 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.timeline_filter_preferences) - 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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 645330b9e..38574e147 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Conversation; import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Instance; import com.keylesspalace.tusky.entity.MastoList; import com.keylesspalace.tusky.entity.Notification; @@ -346,4 +347,37 @@ public interface MastodonApi { @GET("/api/v1/conversations") Call> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit); + @GET("api/v1/filters") + Call> getFilters(); + + @FormUrlEncoded + @POST("api/v1/filters") + Call createFilter( + @Field("phrase") String phrase, + @Field("context[]") List context, + @Field("irreversible") Boolean irreversible, + @Field("whole_word") Boolean wholeWord, + @Field("expires_in") String expiresIn + ); + + @GET("api/v1/filters/{id}") + Call getFilter( + @Path("id") String id + ); + + @FormUrlEncoded + @PUT("api/v1/filters/{id}") + Call updateFilter( + @Path("id") String id, + @Field("phrase") String phrase, + @Field("context[]") List context, + @Field("irreversible") Boolean irreversible, + @Field("whole_word") Boolean wholeWord, + @Field("expires_in") String expiresIn + ); + + @DELETE("api/v1/filters/{id}") + Call deleteFilter( + @Path("id") String id + ); } diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml new file mode 100644 index 000000000..ca25f54c0 --- /dev/null +++ b/app/src/main/res/layout/activity_filters.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml new file mode 100644 index 000000000..9c659cbce --- /dev/null +++ b/app/src/main/res/layout/dialog_filter.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca980abe4..c70f15adb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -199,6 +199,7 @@ Appearance App Theme Timelines + Filters Dark @@ -216,7 +217,6 @@ Tabs Show boosts Show replies - Filter out by regular expressions Download media previews Proxy HTTP proxy @@ -319,6 +319,14 @@ Replying to @%s load more + Public timelines + Conversations + Add filter + Edit filter + Remove + Update + Phrase to filter + Add Account Add new Mastodon Account diff --git a/app/src/main/res/xml/account_preferences.xml b/app/src/main/res/xml/account_preferences.xml index 081d87c5f..efec8e14d 100644 --- a/app/src/main/res/xml/account_preferences.xml +++ b/app/src/main/res/xml/account_preferences.xml @@ -45,7 +45,23 @@ - - + + + + + + diff --git a/app/src/main/res/xml/timeline_filter_preferences.xml b/app/src/main/res/xml/timeline_filter_preferences.xml index 38a82b2a4..03f4fa779 100644 --- a/app/src/main/res/xml/timeline_filter_preferences.xml +++ b/app/src/main/res/xml/timeline_filter_preferences.xml @@ -17,15 +17,4 @@ android:title="@string/pref_title_show_replies" app:iconSpaceReserved="false" /> - - - - - \ No newline at end of file