diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 4e2c02a4e..8676e43d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -34,7 +34,6 @@ import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.BindingHolder -import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar @@ -103,12 +102,12 @@ class AccountsInListFragment : DialogFragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { state -> - adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) + adapter.submitList(state.accounts.getOrDefault(emptyList())) - when (state.accounts) { - is Either.Right -> binding.messageView.hide() - is Either.Left -> handleError(state.accounts.value) - } + state.accounts.fold( + onSuccess = { binding.messageView.hide() }, + onFailure = { handleError(it) } + ) setupSearchView(state) } @@ -137,7 +136,7 @@ class AccountsInListFragment : DialogFragment() { binding.accountsSearchRecycler.hide() binding.accountsRecycler.show() } else { - val listAccounts = state.accounts.asRightOrNull() ?: listOf() + val listAccounts = state.accounts.getOrDefault(emptyList()) val newList = state.searchResult.map { acc -> acc to listAccounts.contains(acc) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt index 1a9a7513c..ac327ac03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt @@ -21,7 +21,7 @@ import com.keylesspalace.tusky.databinding.ItemFooterBinding import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.BindingHolder -import com.keylesspalace.tusky.util.removeDuplicates +import com.keylesspalace.tusky.util.removeDuplicatesTo /** Generic adapter with bottom loading indicator. */ abstract class AccountAdapter internal constructor( @@ -74,7 +74,7 @@ abstract class AccountAdapter internal constructo } fun update(newAccounts: List) { - accountList = removeDuplicates(newAccounts) + accountList = newAccounts.removeDuplicatesTo(ArrayList()) notifyDataSetChanged() } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Either.kt b/app/src/main/java/com/keylesspalace/tusky/util/Either.kt deleted file mode 100644 index 3b26c3daa..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/Either.kt +++ /dev/null @@ -1,49 +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 . */ - -package com.keylesspalace.tusky.util - -/** - * Created by charlag on 05/11/17. - * - * Class to represent sum type/tagged union/variant/ADT e.t.c. - * It is either Left or Right. - */ -sealed interface Either { - data class Left(val value: L) : Either - data class Right(val value: R) : Either - - fun isRight(): Boolean = this is Right - - fun isLeft(): Boolean = this is Left - - fun asLeftOrNull(): L? = (this as? Left)?.value - - fun asRightOrNull(): R? = (this as? Right)?.value - - fun asLeft(): L = (this as Left).value - - fun asRight(): R = (this as Right).value - - companion object { - inline fun Either.map(mapper: (R) -> N): Either { - return if (this.isLeft()) { - Left(this.asLeft()) - } else { - Right(mapper(this.asRight())) - } - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt index fa6d4e92e..6737d9702 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -17,31 +17,36 @@ package com.keylesspalace.tusky.util -import java.util.ArrayList -import java.util.LinkedHashSet +/** + * Copies elements to destination, removing duplicates and preserving original order. + */ +fun > Iterable.removeDuplicatesTo(destination: C): C { + return filterTo(destination, HashSet()::add) +} /** - * @return a new ArrayList containing the elements without duplicates in the same order + * Copies elements to a new list, removing duplicates and preserving original order. */ -fun removeDuplicates(list: List): ArrayList { - val set = LinkedHashSet(list) - return ArrayList(set) +fun Iterable.removeDuplicates(): List { + return removeDuplicatesTo(ArrayList()) } inline fun List.withoutFirstWhich(predicate: (T) -> Boolean): List { - val newList = toMutableList() - val index = newList.indexOfFirst(predicate) - if (index != -1) { - newList.removeAt(index) + val index = indexOfFirst(predicate) + if (index == -1) { + return this } + val newList = toMutableList() + newList.removeAt(index) return newList } inline fun List.replacedFirstWhich(replacement: T, predicate: (T) -> Boolean): List { - val newList = toMutableList() - val index = newList.indexOfFirst(predicate) - if (index != -1) { - newList[index] = replacement + val index = indexOfFirst(predicate) + if (index == -1) { + return this } + val newList = toMutableList() + newList[index] = replacement return newList } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt index e62da81db..4f000219e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -19,22 +19,22 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.NetworkResult import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrDefault import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.Either -import com.keylesspalace.tusky.util.Either.Companion.map -import com.keylesspalace.tusky.util.Either.Left -import com.keylesspalace.tusky.util.Either.Right import com.keylesspalace.tusky.util.withoutFirstWhich import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch data class State( - val accounts: Either>, + val accounts: Result>, val searchResult: List? ) @@ -42,20 +42,19 @@ data class State( class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { val state: Flow get() = _state - private val _state = MutableStateFlow(State(Right(listOf()), null)) + private val _state = MutableStateFlow( + State( + accounts = Result.success(emptyList()), + searchResult = null + ) + ) fun load(listId: String) { val state = _state.value - if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) { + if (state.accounts.isFailure || state.accounts.getOrThrow().isEmpty()) { viewModelScope.launch { - api.getAccountsInList(listId, 0).fold( - { accounts -> - updateState { copy(accounts = Right(accounts)) } - }, - { e -> - updateState { copy(accounts = Left(e)) } - } - ) + val accounts = api.getAccountsInList(listId, 0) + _state.update { it.copy(accounts = accounts.toResult()) } } } } @@ -64,14 +63,14 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) viewModelScope.launch { api.addAccountToList(listId, listOf(account.id)) .fold( - { - updateState { - copy(accounts = accounts.map { it + account }) + onSuccess = { + _state.update { state -> + state.copy(accounts = state.accounts.map { it + account }) } }, - { + onFailure = { Log.i( - javaClass.simpleName, + AccountsInListViewModel::class.java.simpleName, "Failed to add account to list: ${account.username}" ) } @@ -83,18 +82,18 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) viewModelScope.launch { api.deleteAccountFromList(listId, listOf(accountId)) .fold( - { - updateState { - copy( - accounts = accounts.map { accounts -> + onSuccess = { + _state.update { state -> + state.copy( + accounts = state.accounts.map { accounts -> accounts.withoutFirstWhich { it.id == accountId } } ) } }, - { + onFailure = { Log.i( - javaClass.simpleName, + AccountsInListViewModel::class.java.simpleName, "Failed to remove account from list: $accountId" ) } @@ -102,25 +101,29 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) } } + private val currentQuery = MutableStateFlow("") + fun search(query: String) { - when { - query.isEmpty() -> updateState { copy(searchResult = null) } - query.isBlank() -> updateState { copy(searchResult = listOf()) } - else -> viewModelScope.launch { - api.searchAccounts(query, null, 10, true) - .fold( - { result -> - updateState { copy(searchResult = result) } - }, - { - updateState { copy(searchResult = listOf()) } - } - ) + currentQuery.value = query + } + + init { + viewModelScope.launch { + // Use collectLatest to automatically cancel the previous search + currentQuery.collectLatest { query -> + val searchResult = when { + query.isEmpty() -> null + query.isBlank() -> emptyList() + else -> api.searchAccounts(query, null, 10, true) + .getOrDefault(emptyList()) + } + _state.update { it.copy(searchResult = searchResult) } } } } - private inline fun updateState(fn: State.() -> State) { - _state.value = fn(_state.value) - } + private fun NetworkResult.toResult(): Result = fold( + onSuccess = { Result.success(it) }, + onFailure = { Result.failure(it) } + ) }