mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* 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 headerpull/1127/head
23 changed files with 1046 additions and 221 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) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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); |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
@ -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,40 +1,56 @@
|
||||
<?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:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<include layout="@layout/toolbar_basic" /> |
||||
<androidx.constraintlayout.widget.ConstraintLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/listsRecycler" |
||||
android:layout_width="0dp" |
||||
android:layout_height="0dp" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toBottomOf="@id/appbar" /> |
||||
<include layout="@layout/toolbar_basic" /> |
||||
|
||||
<ProgressBar |
||||
android:id="@+id/progressBar" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:visibility="gone" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toTopOf="parent" |
||||
tools:visibility="visible" /> |
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView |
||||
android:id="@+id/messageView" |
||||
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/listsRecycler" |
||||
android:layout_width="0dp" |
||||
android:layout_height="0dp" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toBottomOf="@id/appbar" /> |
||||
|
||||
<ProgressBar |
||||
android:id="@+id/progressBar" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:visibility="gone" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toTopOf="parent" |
||||
tools:visibility="visible" /> |
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView |
||||
android:id="@+id/messageView" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:visibility="gone" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toTopOf="parent" /> |
||||
|
||||
</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:visibility="gone" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintLeft_toLeftOf="parent" |
||||
app:layout_constraintRight_toRightOf="parent" |
||||
app:layout_constraintTop_toTopOf="parent" /> |
||||
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.constraintlayout.widget.ConstraintLayout> |
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout> |
||||
|
||||
@ -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> |
||||
@ -1,13 +1,33 @@
|
||||
<?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" |
||||
android:id="@+id/list_name_textview" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="60dp" |
||||
android:paddingLeft="16dp" |
||||
android:paddingRight="16dp" |
||||
android:background="?selectableItemBackground" |
||||
android:drawablePadding="8dp" |
||||
android:layout_height="wrap_content" |
||||
android:gravity="center_vertical" |
||||
android:textSize="?attr/status_text_medium" |
||||
tools:text="Example list" /> |
||||
android:orientation="horizontal"> |
||||
|
||||
<TextView |
||||
android:id="@+id/list_name_textview" |
||||
android:layout_width="0dp" |
||||
android:layout_height="60dp" |
||||
android:layout_weight="1" |
||||
android:background="?selectableItemBackground" |
||||
android:drawablePadding="8dp" |
||||
android:gravity="center_vertical" |
||||
android:paddingLeft="16dp" |
||||
android:paddingRight="16dp" |
||||
android:textSize="?attr/status_text_medium" |
||||
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> |
||||
|
||||
@ -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> |
||||
Loading…
Reference in new issue