diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 40397a94..59bc17ec 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 00000000..eafad499
--- /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 a91a2da1..9ad5e086 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 00000000..e0440be8
--- /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 0a3807cf..edc415c0 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 7ce075ae..3c7ea6d9 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 3199f9f1..8d92d54b 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 645330b9..38574e14 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 00000000..ca25f54c
--- /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 00000000..9c659cbc
--- /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 ca980abe..c70f15ad 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 081d87c5..efec8e14 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 38a82b2a..03f4fa77 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