List editing (#1104)
* List editing groundwork * Add ability to add/remove accounts from list, delete lists * Rename list, improve lists UI * Add error handling, extract strings * Revert gradle.properties * Apply feedback suggestions * Apply feedback * Update license header
This commit is contained in:
parent
08c1bbd253
commit
520e0d6e7a
23 changed files with 1047 additions and 222 deletions
|
@ -0,0 +1,289 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.State
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import com.uber.autodispose.autoDisposable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.fragment_accounts_in_list.*
|
||||||
|
import kotlinx.android.synthetic.main.item_follow_request.*
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private typealias AccountInfo = Pair<Account, Boolean>
|
||||||
|
|
||||||
|
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LIST_ID_ARG = "listId"
|
||||||
|
private const val LIST_NAME_ARG = "listName"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(listId: String, listName: String): AccountsInListFragment {
|
||||||
|
val args = Bundle().apply {
|
||||||
|
putString(LIST_ID_ARG, listId)
|
||||||
|
putString(LIST_NAME_ARG, listName)
|
||||||
|
}
|
||||||
|
return AccountsInListFragment().apply { arguments = args }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
lateinit var viewModel: AccountsInListViewModel
|
||||||
|
|
||||||
|
private lateinit var listId: String
|
||||||
|
private lateinit var listName: String
|
||||||
|
private val adapter = Adapter()
|
||||||
|
private val searchAdapter = SearchAdapter()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setStyle(DialogFragment.STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
|
||||||
|
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
|
||||||
|
val args = arguments!!
|
||||||
|
listId = args.getString(LIST_ID_ARG)!!
|
||||||
|
listName = args.getString(LIST_NAME_ARG)!!
|
||||||
|
|
||||||
|
viewModel.load(listId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
dialog?.apply {
|
||||||
|
// Stretch dialog to the window
|
||||||
|
window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
accountsRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
accountsRecycler.adapter = adapter
|
||||||
|
|
||||||
|
accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
accountsSearchRecycler.adapter = searchAdapter
|
||||||
|
|
||||||
|
viewModel.state
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDisposable(from(this))
|
||||||
|
.subscribe { state ->
|
||||||
|
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||||
|
|
||||||
|
when (state.accounts) {
|
||||||
|
is Either.Right -> messageView.hide()
|
||||||
|
is Either.Left -> handleError(state.accounts.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSearchView(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchView.isSubmitButtonEnabled = true
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
viewModel.search(query ?: "")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
// Close event is not sent so we use this instead
|
||||||
|
if (newText.isNullOrBlank()) {
|
||||||
|
viewModel.search("")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSearchView(state: State) {
|
||||||
|
if (state.searchResult == null) {
|
||||||
|
searchAdapter.submitList(listOf())
|
||||||
|
accountsSearchRecycler.hide()
|
||||||
|
} else {
|
||||||
|
val listAccounts = state.accounts.asRightOrNull() ?: listOf()
|
||||||
|
val newList = state.searchResult.map { acc ->
|
||||||
|
acc to listAccounts.contains(acc)
|
||||||
|
}
|
||||||
|
searchAdapter.submitList(newList)
|
||||||
|
accountsSearchRecycler.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(error: Throwable) {
|
||||||
|
messageView.show()
|
||||||
|
val retryAction = { _: View ->
|
||||||
|
messageView.hide()
|
||||||
|
viewModel.load(listId)
|
||||||
|
}
|
||||||
|
if (error is IOException) {
|
||||||
|
messageView.setup(R.drawable.elephant_offline,
|
||||||
|
R.string.error_network, retryAction)
|
||||||
|
} else {
|
||||||
|
messageView.setup(R.drawable.elephant_error,
|
||||||
|
R.string.error_generic, retryAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRemoveFromList(accountId: String) {
|
||||||
|
viewModel.deleteAccountFromList(listId, accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAddToList(account: Account) {
|
||||||
|
viewModel.addAccountToList(listId, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||||
|
return oldItem.deepEquals(newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Adapter : ListAdapter<Account, Adapter.ViewHolder>(AccountDiffer) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_follow_request, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||||
|
View.OnClickListener, LayoutContainer {
|
||||||
|
|
||||||
|
override val containerView = itemView
|
||||||
|
|
||||||
|
init {
|
||||||
|
acceptButton.hide()
|
||||||
|
rejectButton.setOnClickListener(this)
|
||||||
|
rejectButton.contentDescription =
|
||||||
|
itemView.context.getString(R.string.action_remove_from_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(account: Account) {
|
||||||
|
usernameTextView.text = account.username
|
||||||
|
displayNameTextView.text = account.displayName
|
||||||
|
Picasso.with(avatar.context)
|
||||||
|
.load(account.avatar)
|
||||||
|
.fit()
|
||||||
|
.placeholder(R.drawable.avatar_default)
|
||||||
|
.into(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
onRemoveFromList(getItem(adapterPosition).id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||||
|
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
|
return oldItem.second == newItem.second
|
||||||
|
&& oldItem.first.deepEquals(newItem.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SearchAdapter : ListAdapter<AccountInfo, SearchAdapter.ViewHolder>(SearchDiffer) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_follow_request, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val (account, inAList) = getItem(position)
|
||||||
|
holder.bind(account, inAList)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||||
|
View.OnClickListener, LayoutContainer {
|
||||||
|
|
||||||
|
override val containerView = itemView
|
||||||
|
|
||||||
|
fun bind(account: Account, inAList: Boolean) {
|
||||||
|
usernameTextView.text = account.username
|
||||||
|
displayNameTextView.text = account.displayName
|
||||||
|
Picasso.with(avatar.context)
|
||||||
|
.load(account.avatar)
|
||||||
|
.fit()
|
||||||
|
.placeholder(R.drawable.avatar_default)
|
||||||
|
.into(avatar)
|
||||||
|
rejectButton.apply {
|
||||||
|
if (inAList) {
|
||||||
|
setImageResource(R.drawable.ic_reject_24dp)
|
||||||
|
contentDescription = getString(R.string.action_remove_from_list)
|
||||||
|
} else {
|
||||||
|
setImageResource(R.drawable.ic_plus_24dp)
|
||||||
|
contentDescription = getString(R.string.action_add_to_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
acceptButton.hide()
|
||||||
|
rejectButton.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
val (account, inAList) = getItem(adapterPosition)
|
||||||
|
if (inAList) {
|
||||||
|
onRemoveFromList(account.id)
|
||||||
|
} else {
|
||||||
|
onAddToList(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1551,15 +1551,15 @@ public final class ComposeActivity
|
||||||
try {
|
try {
|
||||||
switch (token.charAt(0)) {
|
switch (token.charAt(0)) {
|
||||||
case '@':
|
case '@':
|
||||||
ArrayList<Account> resultList = new ArrayList<>();
|
try {
|
||||||
List<Account> accountList = mastodonApi
|
List<Account> accountList = mastodonApi
|
||||||
.searchAccounts(token.substring(1), false, 20)
|
.searchAccounts(token.substring(1), false, 20, null)
|
||||||
.execute()
|
.blockingGet();
|
||||||
.body();
|
return CollectionsKt.map(accountList,
|
||||||
if (accountList != null) {
|
ComposeAutoCompleteAdapter.AccountResult::new);
|
||||||
resultList.addAll(accountList);
|
} catch (Throwable e) {
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return CollectionsKt.map(resultList, ComposeAutoCompleteAdapter.AccountResult::new);
|
|
||||||
case '#':
|
case '#':
|
||||||
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
|
|
@ -1,5 +1,22 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -7,96 +24,38 @@ import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.*
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.*
|
||||||
import com.keylesspalace.tusky.LoadingState.*
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.util.*
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import com.uber.autodispose.autoDisposable
|
||||||
|
import dagger.android.DispatchingAndroidInjector
|
||||||
|
import dagger.android.support.HasSupportFragmentInjector
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.android.synthetic.main.activity_lists.*
|
import kotlinx.android.synthetic.main.activity_lists.*
|
||||||
import retrofit2.Call
|
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 1/4/18.
|
* Created by charlag on 1/4/18.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface ListsView {
|
class ListsActivity : BaseActivity(), Injectable, HasSupportFragmentInjector {
|
||||||
fun update(state: State)
|
|
||||||
fun openTimeline(listId: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum class LoadingState {
|
|
||||||
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
|
||||||
}
|
|
||||||
|
|
||||||
data class State(val lists: List<MastoList>, val loadingState: LoadingState)
|
|
||||||
|
|
||||||
class ListsViewModel(private val api: MastodonApi) {
|
|
||||||
|
|
||||||
private var _view: WeakReference<ListsView>? = null
|
|
||||||
private val view: ListsView? get() = _view?.get()
|
|
||||||
private var state = State(listOf(), INITIAL)
|
|
||||||
|
|
||||||
fun attach(view: ListsView) {
|
|
||||||
this._view = WeakReference(view)
|
|
||||||
updateView()
|
|
||||||
loadIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun detach() {
|
|
||||||
this._view = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun didSelectItem(id: String) {
|
|
||||||
view?.openTimeline(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retryLoading() {
|
|
||||||
loadIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadIfNeeded() {
|
|
||||||
if (state.loadingState == LOADING || !state.lists.isEmpty()) return
|
|
||||||
updateState(state.copy(loadingState = LOADING))
|
|
||||||
|
|
||||||
api.getLists().enqueue(object : retrofit2.Callback<List<MastoList>> {
|
|
||||||
override fun onResponse(call: Call<List<MastoList>>, response: Response<List<MastoList>>) {
|
|
||||||
updateState(state.copy(lists = response.body() ?: listOf(), loadingState = LOADED))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<MastoList>>, err: Throwable?) {
|
|
||||||
updateState(state.copy(
|
|
||||||
loadingState = if (err is IOException) ERROR_NETWORK else ERROR_OTHER
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateState(state: State) {
|
|
||||||
this.state = state
|
|
||||||
view?.update(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateView() {
|
|
||||||
view?.update(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListsActivity : BaseActivity(), ListsView, Injectable {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -106,7 +65,10 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mastodonApi: MastodonApi
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
|
||||||
|
|
||||||
private lateinit var viewModel: ListsViewModel
|
private lateinit var viewModel: ListsViewModel
|
||||||
private val adapter = ListsAdapter()
|
private val adapter = ListsAdapter()
|
||||||
|
@ -115,14 +77,12 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_lists)
|
setContentView(R.layout.activity_lists)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
val bar = supportActionBar
|
supportActionBar?.apply {
|
||||||
if (bar != null) {
|
title = getString(R.string.title_lists)
|
||||||
bar.title = getString(R.string.title_lists)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
bar.setDisplayHomeAsUpEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
bar.setDisplayShowHomeEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listsRecycler.adapter = adapter
|
listsRecycler.adapter = adapter
|
||||||
|
@ -130,23 +90,62 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
listsRecycler.addItemDecoration(
|
listsRecycler.addItemDecoration(
|
||||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
viewModel = lastNonConfigurationInstance as? ListsViewModel ?: ListsViewModel(mastodonApi)
|
viewModel = viewModelFactory.create(ListsViewModel::class.java)
|
||||||
viewModel.attach(this)
|
viewModel.state
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDisposable(from(this))
|
||||||
|
.subscribe(this::update)
|
||||||
|
viewModel.retryLoading()
|
||||||
|
|
||||||
|
addListButton.setOnClickListener {
|
||||||
|
showlistNameDialog(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
viewModel.events.observeOn(AndroidSchedulers.mainThread())
|
||||||
viewModel.detach()
|
.autoDisposable(from(this))
|
||||||
super.onDestroy()
|
.subscribe { event ->
|
||||||
|
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||||
|
when (event) {
|
||||||
|
CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||||
|
RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||||
|
DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRetainCustomNonConfigurationInstance(): Any {
|
private fun showlistNameDialog(list: MastoList?) {
|
||||||
return viewModel
|
val layout = FrameLayout(this)
|
||||||
|
val editText = EditText(this)
|
||||||
|
editText.setHint(R.string.hint_list_name)
|
||||||
|
layout.addView(editText)
|
||||||
|
val margin = Utils.dpToPx(this, 8)
|
||||||
|
(editText.layoutParams as ViewGroup.MarginLayoutParams)
|
||||||
|
.setMargins(margin, margin, margin, 0)
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(layout)
|
||||||
|
.setPositiveButton(
|
||||||
|
if (list == null) R.string.action_create_list
|
||||||
|
else R.string.action_rename_list) { _, _ ->
|
||||||
|
onPickedDialogName(editText.text, list?.id)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { d, _ ->
|
||||||
|
d.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
|
||||||
|
editText.onTextChanged { s, _, _, _ ->
|
||||||
|
positiveButton.isEnabled = !s.isNullOrBlank()
|
||||||
|
}
|
||||||
|
editText.setText(list?.title)
|
||||||
|
editText.text?.let { editText.setSelection(it.length) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun update(state: State) {
|
private fun update(state: ListsViewModel.State) {
|
||||||
adapter.update(state.lists)
|
adapter.submitList(state.lists)
|
||||||
progressBar.visibility = if (state.loadingState == LOADING) View.VISIBLE else View.GONE
|
progressBar.visible(state.loadingState == LOADING)
|
||||||
when (state.loadingState) {
|
when (state.loadingState) {
|
||||||
INITIAL, LOADING -> messageView.hide()
|
INITIAL, LOADING -> messageView.hide()
|
||||||
ERROR_NETWORK -> {
|
ERROR_NETWORK -> {
|
||||||
|
@ -172,11 +171,44 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openTimeline(listId: String) {
|
private fun showMessage(@StringRes messageId: Int) {
|
||||||
|
Snackbar.make(
|
||||||
|
listsRecycler, messageId, Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onListSelected(listId: String) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithSlideInAnimation(
|
||||||
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
|
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openListSettings(list: MastoList) {
|
||||||
|
AccountsInListFragment.newInstance(list.id, list.title).show(supportFragmentManager, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renameListDialog(list: MastoList) {
|
||||||
|
showlistNameDialog(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMore(list: MastoList, view: View) {
|
||||||
|
PopupMenu(view.context, view).apply {
|
||||||
|
inflate(R.menu.list_actions)
|
||||||
|
setOnMenuItemClickListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.list_edit -> openListSettings(list)
|
||||||
|
R.id.list_rename -> renameListDialog(list)
|
||||||
|
R.id.list_delete -> viewModel.deleteList(list.id)
|
||||||
|
else -> return@setOnMenuItemClickListener false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun supportFragmentInjector() = dispatchingAndroidInjector
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == android.R.id.home) {
|
if (item.itemId == android.R.id.home) {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
|
@ -185,17 +217,18 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ListsAdapter : RecyclerView.Adapter<ListsAdapter.ListViewHolder>() {
|
private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() {
|
||||||
|
override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean {
|
||||||
private val items = mutableListOf<MastoList>()
|
return oldItem.id == newItem.id
|
||||||
|
|
||||||
fun update(list: List<MastoList>) {
|
|
||||||
this.items.clear()
|
|
||||||
this.items.addAll(list)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun areContentsTheSame(oldItem: MastoList, newItem: MastoList): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ListsAdapter
|
||||||
|
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
||||||
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
||||||
|
@ -210,20 +243,34 @@ class ListsActivity : BaseActivity(), ListsView, Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
|
||||||
holder.nameTextView.text = items[position].title
|
holder.nameTextView.text = getItem(position).title
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
||||||
|
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
view.setOnClickListener(this)
|
view.setOnClickListener(this)
|
||||||
|
moreButton.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View) {
|
||||||
viewModel.didSelectItem(items[adapterPosition].id)
|
if (v == itemView) {
|
||||||
|
onListSelected(getItem(adapterPosition).id)
|
||||||
|
} else {
|
||||||
|
onMore(getItem(adapterPosition), v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onPickedDialogName(name: CharSequence, listId: String?) {
|
||||||
|
if (listId == null) {
|
||||||
|
viewModel.createNewList(name.toString())
|
||||||
|
} else {
|
||||||
|
viewModel.renameList(listId, name.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,11 +73,11 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||||
|
|
||||||
FollowRequestViewHolder(View itemView) {
|
FollowRequestViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
avatar = itemView.findViewById(R.id.follow_request_avatar);
|
avatar = itemView.findViewById(R.id.avatar);
|
||||||
username = itemView.findViewById(R.id.follow_request_username);
|
username = itemView.findViewById(R.id.usernameTextView);
|
||||||
displayName = itemView.findViewById(R.id.follow_request_display_name);
|
displayName = itemView.findViewById(R.id.displayNameTextView);
|
||||||
accept = itemView.findViewById(R.id.follow_request_accept);
|
accept = itemView.findViewById(R.id.acceptButton);
|
||||||
reject = itemView.findViewById(R.id.follow_request_reject);
|
reject = itemView.findViewById(R.id.rejectButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithAccount(Account account) {
|
void setupWithAccount(Account account) {
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.AccountsInListFragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
import com.keylesspalace.tusky.fragment.*
|
import com.keylesspalace.tusky.fragment.*
|
||||||
import com.keylesspalace.tusky.fragment.preference.*
|
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
||||||
|
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -55,4 +57,7 @@ abstract class FragmentBuildersModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun directMessagesPreferencesFragment(): ConversationsFragment
|
abstract fun directMessagesPreferencesFragment(): ConversationsFragment
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun accountInListsFragment(): AccountsInListFragment
|
||||||
|
|
||||||
}
|
}
|
|
@ -6,7 +6,9 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.MapKey
|
import dagger.MapKey
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -48,5 +50,16 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(ConversationsViewModel::class)
|
@ViewModelKey(ConversationsViewModel::class)
|
||||||
internal abstract fun conversationsViewModel(viewModel: ConversationsViewModel): ViewModel
|
internal abstract fun conversationsViewModel(viewModel: ConversationsViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ListsViewModel::class)
|
||||||
|
internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel
|
||||||
|
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(AccountsInListViewModel::class)
|
||||||
|
internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
|
@ -65,6 +65,25 @@ data class Account(
|
||||||
return account?.id == this.id
|
return account?.id == this.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deepEquals(other: Account): Boolean {
|
||||||
|
return id == other.id
|
||||||
|
&& localUsername == other.localUsername
|
||||||
|
&& displayName == other.displayName
|
||||||
|
&& note == other.note
|
||||||
|
&& url == other.url
|
||||||
|
&& avatar == other.avatar
|
||||||
|
&& header == other.header
|
||||||
|
&& locked == other.locked
|
||||||
|
&& followersCount == other.followersCount
|
||||||
|
&& followingCount == other.followingCount
|
||||||
|
&& statusesCount == other.statusesCount
|
||||||
|
&& source == other.source
|
||||||
|
&& bot == other.bot
|
||||||
|
&& emojis == other.emojis
|
||||||
|
&& fields == other.fields
|
||||||
|
&& moved == other.moved
|
||||||
|
}
|
||||||
|
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
fun isRemote(): Boolean = this.username != this.localUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
|
@ -196,10 +197,11 @@ public interface MastodonApi {
|
||||||
@Nullable @Part(value="fields_attributes[3][value]") RequestBody fieldValue3);
|
@Nullable @Part(value="fields_attributes[3][value]") RequestBody fieldValue3);
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
Call<List<Account>> searchAccounts(
|
Single<List<Account>> searchAccounts(
|
||||||
@Query("q") String q,
|
@Query("q") String q,
|
||||||
@Query("resolve") Boolean resolve,
|
@Query("resolve") Boolean resolve,
|
||||||
@Query("limit") Integer limit);
|
@Query("limit") Integer limit,
|
||||||
|
@Query("following") Boolean following);
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}")
|
@GET("api/v1/accounts/{id}")
|
||||||
Call<Account> account(@Path("id") String accountId);
|
Call<Account> account(@Path("id") String accountId);
|
||||||
|
@ -312,7 +314,29 @@ public interface MastodonApi {
|
||||||
);
|
);
|
||||||
|
|
||||||
@GET("/api/v1/lists")
|
@GET("/api/v1/lists")
|
||||||
Call<List<MastoList>> getLists();
|
Single<List<MastoList>> getLists();
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("api/v1/lists")
|
||||||
|
Single<MastoList> createList(@Field("title") String title);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("api/v1/lists/{listId}")
|
||||||
|
Single<MastoList> updateList(@Path("listId") String listId, @Field("title") String title);
|
||||||
|
|
||||||
|
@DELETE("api/v1/lists/{listId}")
|
||||||
|
Completable deleteList(@Path("listId") String listId);
|
||||||
|
|
||||||
|
@GET("api/v1/lists/{listId}/accounts")
|
||||||
|
Single<List<Account>> getAccountsInList(@Path("listId") String listId, @Query("limit") int limit);
|
||||||
|
|
||||||
|
@DELETE("api/v1/lists/{listId}/accounts")
|
||||||
|
Completable deleteAccountFromList(@Path("listId") String listId,
|
||||||
|
@Query("account_ids[]") List<String> accountIds);
|
||||||
|
|
||||||
|
@POST("api/v1/lists/{listId}/accounts")
|
||||||
|
Completable addCountToList(@Path("listId") String listId,
|
||||||
|
@Query("account_ids[]") List<String> accountIds);
|
||||||
|
|
||||||
@GET("/api/v1/custom_emojis")
|
@GET("/api/v1/custom_emojis")
|
||||||
Call<List<Emoji>> getCustomEmojis();
|
Call<List<Emoji>> getCustomEmojis();
|
||||||
|
|
|
@ -27,6 +27,8 @@ sealed class Either<out L, out R> {
|
||||||
|
|
||||||
fun isRight() = this is Right
|
fun isRight() = this is Right
|
||||||
|
|
||||||
|
fun isLeft() = this is Left
|
||||||
|
|
||||||
fun asLeftOrNull() = (this as? Left<L, R>)?.value
|
fun asLeftOrNull() = (this as? Left<L, R>)?.value
|
||||||
|
|
||||||
fun asRightOrNull() = (this as? Right<L, R>)?.value
|
fun asRightOrNull() = (this as? Right<L, R>)?.value
|
||||||
|
@ -34,4 +36,12 @@ sealed class Either<out L, out R> {
|
||||||
fun asLeft(): L = (this as Left<L, R>).value
|
fun asLeft(): L = (this as Left<L, R>).value
|
||||||
|
|
||||||
fun asRight(): R = (this as Right<L, R>).value
|
fun asRight(): R = (this as Right<L, R>).value
|
||||||
|
|
||||||
|
inline fun <N> map(crossinline mapper: (R) -> N): Either<L, N> {
|
||||||
|
return if (this.isLeft()) {
|
||||||
|
Left(this.asLeft())
|
||||||
|
} else {
|
||||||
|
Right(mapper(this.asRight()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ListUtils {
|
|
||||||
/**
|
|
||||||
* @return true if list is null or else return list.isEmpty()
|
|
||||||
*/
|
|
||||||
public static boolean isEmpty(@Nullable List list) {
|
|
||||||
return list == null || list.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a new ArrayList containing the elements without duplicates in the same order
|
|
||||||
*/
|
|
||||||
public static <T> ArrayList<T> removeDuplicates(List<T> list) {
|
|
||||||
LinkedHashSet<T> set = new LinkedHashSet<>(list);
|
|
||||||
return new ArrayList<>(set);
|
|
||||||
}
|
|
||||||
}
|
|
55
app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt
Normal file
55
app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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>. */
|
||||||
|
|
||||||
|
@file:JvmName("ListUtils")
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if list is null or else return list.isEmpty()
|
||||||
|
*/
|
||||||
|
fun isEmpty(list: List<*>?): Boolean {
|
||||||
|
return list == null || list.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a new ArrayList containing the elements without duplicates in the same order
|
||||||
|
*/
|
||||||
|
fun <T> removeDuplicates(list: List<T>): ArrayList<T> {
|
||||||
|
val set = LinkedHashSet(list)
|
||||||
|
return ArrayList(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> List<T>.withoutFirstWhich(predicate: (T) -> Boolean): List<T> {
|
||||||
|
val newList = toMutableList()
|
||||||
|
val index = newList.indexOfFirst(predicate)
|
||||||
|
if (index != -1) {
|
||||||
|
newList.removeAt(index)
|
||||||
|
}
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> List<T>.replacedFirstWhich(replacement: T, predicate: (T) -> Boolean): List<T> {
|
||||||
|
val newList = toMutableList()
|
||||||
|
val index = newList.indexOfFirst(predicate)
|
||||||
|
if (index != -1) {
|
||||||
|
newList[index] = replacement
|
||||||
|
}
|
||||||
|
return newList
|
||||||
|
}
|
|
@ -1,6 +1,25 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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.util
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
|
||||||
fun View.show() {
|
fun View.show() {
|
||||||
this.visibility = View.VISIBLE
|
this.visibility = View.VISIBLE
|
||||||
|
@ -10,6 +29,26 @@ fun View.hide() {
|
||||||
this.visibility = View.GONE
|
this.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.visible(visible: Boolean) {
|
fun View.visible(visible: Boolean, or: Int = View.GONE) {
|
||||||
this.visibility = if (visible) View.VISIBLE else View.GONE
|
this.visibility = if (visible) View.VISIBLE else or
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DefaultTextWatcher : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.onTextChanged(
|
||||||
|
crossinline callback: (s: CharSequence?, start: Int, before: Int, count: Int) -> Unit) {
|
||||||
|
addTextChangedListener(object : DefaultTextWatcher() {
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
callback(s, start, before, count)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
import com.keylesspalace.tusky.util.Either.Left
|
||||||
|
import com.keylesspalace.tusky.util.Either.Right
|
||||||
|
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxkotlin.addTo
|
||||||
|
import io.reactivex.subjects.BehaviorSubject
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class State(val accounts: Either<Throwable, List<Account>>, val searchResult: List<Account>?)
|
||||||
|
|
||||||
|
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||||
|
|
||||||
|
val state: Observable<State> get() = _state
|
||||||
|
private val _state = BehaviorSubject.createDefault(State(Right(listOf()), null))
|
||||||
|
private val disposable = CompositeDisposable()
|
||||||
|
|
||||||
|
fun load(listId: String) {
|
||||||
|
val state = _state.value!!
|
||||||
|
if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) {
|
||||||
|
api.getAccountsInList(listId, 0).subscribe({ accounts ->
|
||||||
|
updateState { copy(accounts = Right(accounts)) }
|
||||||
|
}, { e ->
|
||||||
|
updateState { copy(accounts = Left(e)) }
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAccountToList(listId: String, account: Account) {
|
||||||
|
api.addCountToList(listId, listOf(account.id))
|
||||||
|
.subscribe({
|
||||||
|
updateState {
|
||||||
|
copy(accounts = accounts.map { it + account })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
Log.i(javaClass.simpleName,
|
||||||
|
"Failed to add account to the list: ${account.username}")
|
||||||
|
})
|
||||||
|
.addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAccountFromList(listId: String, accountId: String) {
|
||||||
|
api.deleteAccountFromList(listId, listOf(accountId))
|
||||||
|
.subscribe({
|
||||||
|
updateState {
|
||||||
|
copy(accounts = accounts.map { accounts ->
|
||||||
|
accounts.withoutFirstWhich { it.id == accountId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
Log.i(javaClass.simpleName, "Failed to remove account from thelist: $accountId")
|
||||||
|
})
|
||||||
|
.addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String) {
|
||||||
|
when {
|
||||||
|
query.isEmpty() -> updateState { copy(searchResult = null) }
|
||||||
|
query.isBlank() -> updateState { copy(searchResult = listOf()) }
|
||||||
|
else -> api.searchAccounts(query, null, 10, true)
|
||||||
|
.subscribe({ result ->
|
||||||
|
updateState { copy(searchResult = result) }
|
||||||
|
}, {
|
||||||
|
updateState { copy(searchResult = listOf()) }
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||||
|
_state.onNext(fn(_state.value!!))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||||
|
import com.keylesspalace.tusky.util.replacedFirstWhich
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxkotlin.addTo
|
||||||
|
import io.reactivex.subjects.BehaviorSubject
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.ConnectException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||||
|
enum class LoadingState {
|
||||||
|
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Event {
|
||||||
|
CREATE_ERROR, DELETE_ERROR, RENAME_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(val lists: List<MastoList>, val loadingState: LoadingState)
|
||||||
|
|
||||||
|
val state: Observable<State> get() = _state
|
||||||
|
val events: Observable<Event> get() = _events
|
||||||
|
private val _state = BehaviorSubject.createDefault(State(listOf(), LoadingState.INITIAL))
|
||||||
|
private val _events = PublishSubject.create<Event>()
|
||||||
|
private val disposable = CompositeDisposable()
|
||||||
|
|
||||||
|
fun retryLoading() {
|
||||||
|
loadIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadIfNeeded() {
|
||||||
|
val state = _state.value!!
|
||||||
|
if (state.loadingState == LoadingState.LOADING || !state.lists.isEmpty()) return
|
||||||
|
updateState {
|
||||||
|
copy(loadingState = LoadingState.LOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
api.getLists().subscribe({ lists ->
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
lists = lists,
|
||||||
|
loadingState = LoadingState.LOADED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, { err ->
|
||||||
|
updateState {
|
||||||
|
copy(loadingState = if (err is IOException || err is ConnectException)
|
||||||
|
LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER)
|
||||||
|
}
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNewList(listName: String) {
|
||||||
|
api.createList(listName).subscribe({ list ->
|
||||||
|
updateState {
|
||||||
|
copy(lists = lists + list)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sendEvent(Event.CREATE_ERROR)
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renameList(listId: String, listName: String) {
|
||||||
|
api.updateList(listId, listName).subscribe({ list ->
|
||||||
|
updateState {
|
||||||
|
copy(lists = lists.replacedFirstWhich(list) { it.id == listId })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sendEvent(Event.RENAME_ERROR)
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteList(listId: String) {
|
||||||
|
api.deleteList(listId).subscribe({
|
||||||
|
updateState {
|
||||||
|
copy(lists = lists.withoutFirstWhich { it.id == listId })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sendEvent(Event.DELETE_ERROR)
|
||||||
|
}).addTo(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||||
|
_state.onNext(fn(_state.value!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendEvent(event: Event) {
|
||||||
|
_events.onNext(event)
|
||||||
|
}
|
||||||
|
}
|
5
app/src/main/res/drawable/dialog_bg.xml
Normal file
5
app/src/main/res/drawable/dialog_bg.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
<solid android:color="?attr/window_background" />
|
||||||
|
</shape>
|
|
@ -1,10 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<include layout="@layout/toolbar_basic" />
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -38,3 +42,15 @@
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/addListButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/action_create_list"
|
||||||
|
app:layout_anchor="@id/listsRecycler"
|
||||||
|
app:layout_anchorGravity="bottom|end"
|
||||||
|
app:srcCompat="@drawable/ic_plus_24dp" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
58
app/src/main/res/layout/fragment_accounts_in_list.xml
Normal file
58
app/src/main/res/layout/fragment_accounts_in_list.xml
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:lines="1"
|
||||||
|
app:closeIcon="@drawable/ic_close_24dp"
|
||||||
|
app:defaultQueryHint="@string/hint_search_people_list"
|
||||||
|
app:iconifiedByDefault="false"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/accountsRecycler"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchView" />
|
||||||
|
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
|
android:id="@+id/messageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@android:color/transparent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@drawable/elephant_error"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/accountsSearchRecycler"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/window_background"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchView" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,7 +9,7 @@
|
||||||
android:paddingRight="16dp">
|
android:paddingRight="16dp">
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.RoundedImageView
|
<com.keylesspalace.tusky.view.RoundedImageView
|
||||||
android:id="@+id/follow_request_avatar"
|
android:id="@+id/avatar"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
|
@ -20,13 +20,13 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_toEndOf="@id/follow_request_avatar"
|
android:layout_toEndOf="@id/avatar"
|
||||||
android:layout_toStartOf="@id/follow_request_accept"
|
android:layout_toStartOf="@id/acceptButton"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<androidx.emoji.widget.EmojiTextView
|
||||||
android:id="@+id/follow_request_display_name"
|
android:id="@+id/displayNameTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
tools:text="Display name" />
|
tools:text="Display name" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/follow_request_username"
|
android:id="@+id/usernameTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
@ -49,20 +49,20 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/follow_request_accept"
|
android:id="@+id/acceptButton"
|
||||||
style="?attr/image_button_style"
|
style="?attr/image_button_style"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_toStartOf="@id/follow_request_reject"
|
android:layout_toStartOf="@id/rejectButton"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/action_accept"
|
android:contentDescription="@string/action_accept"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
app:srcCompat="@drawable/ic_check_24dp" />
|
app:srcCompat="@drawable/ic_check_24dp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/follow_request_reject"
|
android:id="@+id/rejectButton"
|
||||||
style="?attr/image_button_style"
|
style="?attr/image_button_style"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
|
|
@ -1,13 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/list_name_textview"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/list_name_textview"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
android:paddingLeft="16dp"
|
android:layout_weight="1"
|
||||||
android:paddingRight="16dp"
|
|
||||||
android:background="?selectableItemBackground"
|
android:background="?selectableItemBackground"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
tools:text="Example list" />
|
tools:text="Example list" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/editListButton"
|
||||||
|
style="?attr/image_button_style"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/action_more"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:src="@drawable/ic_more_horiz_24dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
13
app/src/main/res/menu/list_actions.xml
Normal file
13
app/src/main/res/menu/list_actions.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/list_edit"
|
||||||
|
android:title="@string/action_edit_list" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/list_rename"
|
||||||
|
android:title="@string/action_rename_list" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/list_delete"
|
||||||
|
android:title="@string/action_delete_list" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -325,6 +325,16 @@
|
||||||
<string name="action_lists">Lists</string>
|
<string name="action_lists">Lists</string>
|
||||||
<string name="title_lists">Lists</string>
|
<string name="title_lists">Lists</string>
|
||||||
<string name="title_list_timeline">List timeline</string>
|
<string name="title_list_timeline">List timeline</string>
|
||||||
|
<string name="error_create_list">Could not create list</string>
|
||||||
|
<string name="error_rename_list">Could not rename list</string>
|
||||||
|
<string name="error_delete_list">Could not delete list</string>
|
||||||
|
<string name="action_create_list">Create a list</string>
|
||||||
|
<string name="action_rename_list">Rename the list</string>
|
||||||
|
<string name="action_delete_list">Delete the list</string>
|
||||||
|
<string name="action_edit_list">Edit the list</string>
|
||||||
|
<string name="hint_search_people_list">Search for people you follow</string>
|
||||||
|
<string name="action_add_to_list">Add account to the list</string>
|
||||||
|
<string name="action_remove_from_list">Remove account from the list</string>
|
||||||
|
|
||||||
<string name="compose_active_account_description">Posting with account %1$s</string>
|
<string name="compose_active_account_description">Posting with account %1$s</string>
|
||||||
|
|
||||||
|
@ -435,4 +445,6 @@
|
||||||
%s; %s; %s, %s, %s; %s, %s, %s, %s; %s, %s, %s
|
%s; %s; %s, %s, %s; %s, %s, %s, %s; %s, %s, %s
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="hint_list_name">List name</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -47,6 +47,12 @@
|
||||||
|
|
||||||
<style name="TuskyDialogActivityTheme" parent="@style/TuskyTheme" />
|
<style name="TuskyDialogActivityTheme" parent="@style/TuskyTheme" />
|
||||||
|
|
||||||
|
<style name="TuskyDialogFragmentStyle" parent="@style/ThemeOverlay.MaterialComponents.Dialog">
|
||||||
|
<item name="dialogCornerRadius">8dp</item>
|
||||||
|
<item name="android:backgroundTint">?attr/window_background</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="TuskyBaseTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
<style name="TuskyBaseTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<item name="colorPrimary">@color/tusky_blue</item>
|
<item name="colorPrimary">@color/tusky_blue</item>
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
@ -82,21 +88,29 @@
|
||||||
<item name="status_favourite_active_drawable">@drawable/favourite_active_light</item>
|
<item name="status_favourite_active_drawable">@drawable/favourite_active_light</item>
|
||||||
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item>
|
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item>
|
||||||
<item name="content_warning_button">@drawable/toggle_small_light</item>
|
<item name="content_warning_button">@drawable/toggle_small_light</item>
|
||||||
<item name="sensitive_media_warning_background_color">@color/sensitive_media_warning_background_light</item>
|
<item name="sensitive_media_warning_background_color">
|
||||||
|
@color/sensitive_media_warning_background_light
|
||||||
|
</item>
|
||||||
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item>
|
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item>
|
||||||
<item name="android:listDivider">@drawable/status_divider_light</item>
|
<item name="android:listDivider">@drawable/status_divider_light</item>
|
||||||
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light</item>
|
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light
|
||||||
|
</item>
|
||||||
<item name="tab_icon_selected_tint">@color/tusky_blue</item>
|
<item name="tab_icon_selected_tint">@color/tusky_blue</item>
|
||||||
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_light</item>
|
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_light</item>
|
||||||
<item name="account_header_background_color">@color/color_primary_dark_light</item>
|
<item name="account_header_background_color">@color/color_primary_dark_light</item>
|
||||||
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark
|
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark
|
||||||
</item> <!--Default to dark on purpose, because header backgrounds with gradients are always dark.-->
|
</item> <!--Default to dark on purpose, because header backgrounds with gradients are always dark.-->
|
||||||
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_light</item>
|
<item name="account_toolbar_icon_tint_collapsed">
|
||||||
|
@color/account_toolbar_icon_collapsed_light
|
||||||
|
</item>
|
||||||
|
|
||||||
<item name="compose_close_button_tint">@color/toolbar_icon_light</item>
|
<item name="compose_close_button_tint">@color/toolbar_icon_light</item>
|
||||||
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_light</item>
|
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_light
|
||||||
|
</item>
|
||||||
<item name="compose_content_warning_bar_background">@drawable/border_background_light</item>
|
<item name="compose_content_warning_bar_background">@drawable/border_background_light</item>
|
||||||
<item name="compose_reply_content_background">@color/compose_reply_content_background_light</item>
|
<item name="compose_reply_content_background">
|
||||||
|
@color/compose_reply_content_background_light
|
||||||
|
</item>
|
||||||
|
|
||||||
<item name="report_status_background_color">@color/report_status_background_light</item>
|
<item name="report_status_background_color">@color/report_status_background_light</item>
|
||||||
|
|
||||||
|
@ -189,6 +203,7 @@
|
||||||
<item name="material_drawer_header_selection_text">@color/text_color_primary_black</item>
|
<item name="material_drawer_header_selection_text">@color/text_color_primary_black</item>
|
||||||
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_black</item>
|
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase" />
|
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue