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 changemain
parent
b4f3dcf67f
commit
b75c92b795
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
|
@ -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> |
@ -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> |
Loading…
Reference in new issue