Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import android.content.Intent
import android.os.Bundle
import android.text.SpannableString
import android.view.HapticFeedbackConstants
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager
import com.automattic.simplenote.databinding.ActivityCollaboratorsBinding
import com.automattic.simplenote.utils.CollaboratorsAdapter
import com.automattic.simplenote.utils.CollaboratorsAdapter.CollaboratorDataItem.*
import com.automattic.simplenote.utils.DisplayUtils
import com.automattic.simplenote.utils.DrawableUtils
import com.automattic.simplenote.utils.HtmlCompat
import com.automattic.simplenote.utils.IntentUtils
import com.automattic.simplenote.utils.SystemBarUtils
import com.automattic.simplenote.utils.getColorStr
import com.automattic.simplenote.utils.toast
import com.automattic.simplenote.viewmodels.CollaboratorsViewModel
import com.automattic.simplenote.viewmodels.CollaboratorsViewModel.Event
Expand Down Expand Up @@ -62,6 +69,47 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)

menuInflater.inflate(R.menu.collaborators_list, menu)
DrawableUtils.tintMenuWithAttribute(this, menu, R.attr.toolbarIconColor)
val searchMenuItem = menu.findItem(R.id.menu_search)
val searchView = searchMenuItem.actionView as SearchView
val searchEditFrame = searchView.findViewById<LinearLayout>(R.id.search_edit_frame)
(searchEditFrame.layoutParams as LinearLayout.LayoutParams).leftMargin = 0

val hintHexColor = getColorStr(R.color.text_title_disabled)
searchView.queryHint = HtmlCompat.fromHtml(String.format(
"<font color=\"%s\">%s</font>",
hintHexColor,
getString(R.string.search_collaborators_hint)
))

searchView.setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(query: String): Boolean {
if (searchMenuItem.isActionViewExpanded) {
viewModel.search(query)
}

return true
}

override fun onQueryTextSubmit(queryText: String): Boolean {
return true
}
}
)

searchView.setOnCloseListener {
viewModel.closeSearch()
false
}

return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
viewModel.close()
Expand All @@ -70,6 +118,17 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
return super.onOptionsItemSelected(item)
}

override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
viewModel.uiState.observe(this@CollaboratorsActivity, { uiState ->
menu?.findItem(R.id.menu_search)?.isVisible = when (uiState) {
is EmptyCollaborators -> !uiState.allCollaboratorsRemoved && !uiState.searchUpdate
else -> true
}
})

return super.onPrepareOptionsMenu(menu)
}

override fun onResume() {
super.onResume()

Expand Down Expand Up @@ -117,8 +176,12 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
private fun ActivityCollaboratorsBinding.setObservers() {
viewModel.uiState.observe(this@CollaboratorsActivity, { uiState ->
when (uiState) {
is EmptyCollaborators -> handleEmptyCollaborators()
is CollaboratorsList -> handleCollaboratorsList(uiState.collaborators)
is EmptyCollaborators -> {
handleEmptyCollaborators(uiState.allCollaboratorsRemoved, uiState.searchUpdate)
}
is CollaboratorsList -> {
handleCollaboratorsList(uiState.collaborators, uiState.searchUpdate, uiState.searchQuery)
}
is NoteDeleted -> {
toast(R.string.collaborators_note_deleted, Toast.LENGTH_LONG)
navigateToNotesList()
Expand Down Expand Up @@ -156,15 +219,33 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
toast(R.string.remove_collaborator)
}

private fun ActivityCollaboratorsBinding.handleCollaboratorsList(collaborators: List<String>) {
private fun ActivityCollaboratorsBinding.handleCollaboratorsList(collaborators: List<String>, searchUpdate: Boolean, searchQuery: String?) {
hideEmptyView()
val items = listOf(HeaderItem) + collaborators.map { CollaboratorItem(it) }
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(items)
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(items) {
if (searchUpdate) {
collaboratorsList.scrollToPosition(0)

if (searchQuery != null) {
setEmptyViewSearch()
} else {
setEmptyViewDefault()
}
}
}
}

private fun ActivityCollaboratorsBinding.handleEmptyCollaborators() {
private fun ActivityCollaboratorsBinding.handleEmptyCollaborators(allCollaboratorsRemoved: Boolean, searchUpdate: Boolean) {
showEmptyView()
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(emptyList())
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(emptyList()) {
if (allCollaboratorsRemoved) {
invalidateOptionsMenu()
setEmptyViewDefault()
} else if (searchUpdate) {
collaboratorsList.scrollToPosition(0)
setEmptyViewSearch()
}
}
}

private fun ActivityCollaboratorsBinding.hideEmptyView() {
Expand All @@ -179,6 +260,53 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
empty.message.visibility = View.VISIBLE
}

private fun ActivityCollaboratorsBinding.setEmptyListImage(@DrawableRes image: Int) {
if (image != -1) {
empty.image.visibility = View.VISIBLE
empty.image.setImageResource(image)
} else {
empty.image.visibility = View.GONE
}
}

private fun ActivityCollaboratorsBinding.setEmptyListMessage(message: String?) {
message?.let {
empty.message.text = it
}
}

private fun ActivityCollaboratorsBinding.setEmptyListTitle(title: String?) {
title?.let {
empty.title.text = it
}
}

private fun ActivityCollaboratorsBinding.setEmptyViewDefault() {
if (DisplayUtils.isLandscape(this@CollaboratorsActivity) &&
!DisplayUtils.isLargeScreen(this@CollaboratorsActivity)
) {
setEmptyListImage(-1)
} else {
setEmptyListImage(R.drawable.ic_collaborate_24dp)
}

setEmptyListMessage(getString(R.string.add_email_collaborator_message))
setEmptyListTitle(getString(R.string.no_collaborators))
}

private fun ActivityCollaboratorsBinding.setEmptyViewSearch() {
if (DisplayUtils.isLandscape(this@CollaboratorsActivity) &&
!DisplayUtils.isLargeScreen(this@CollaboratorsActivity)
) {
setEmptyListImage(-1)
} else {
setEmptyListImage(R.drawable.ic_search_24dp)
}

setEmptyListMessage("")
setEmptyListTitle(getString(R.string.no_collaborators_search))
}

private fun showAddCollaboratorFragment(event: Event.AddCollaboratorEvent) {
val dialog = AddCollaboratorFragment(event.noteId)
dialog.show(supportFragmentManager.beginTransaction(), DIALOG_TAG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ interface CollaboratorsRepository {
fun isValidCollaborator(collaborator: String): Boolean

/**
* Get a list of collaborators for a given [noteId].
* Get list of collaborators for given [noteId] and {optional} [query].
*/
suspend fun getCollaborators(noteId: String): CollaboratorsActionResult
suspend fun getCollaborators(noteId: String, query: String? = null): CollaboratorsActionResult

suspend fun addCollaborator(noteId: String, collaborator: String): CollaboratorsActionResult

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named

Expand All @@ -32,14 +33,20 @@ class SimperiumCollaboratorsRepository @Inject constructor(
}

/**
* Return a list of collaborators (email addresses as tags) if the note for the given simperiumKey ([noteId]) is
* not in the trash and has not been deleted
* Get list of collaborators (email addresses as tags) for given [noteId] which is not deleted, not trashed, and
* contains given (optional) [query].
*
* @param noteId [String] Simperium key of note to retrieve collaborators from
* @param query [String] (optional) to filter list of collaborators with
*
* @return [List]<[String]> of collaborators for [noteId] containing [query]
*/
override suspend fun getCollaborators(
noteId: String
noteId: String,
query: String?
) = when (val result = getNote(noteId)) {
is Either.Left -> result.l
is Either.Right -> CollaboratorsActionResult.CollaboratorsList(filterCollaborators(result.r))
is Either.Right -> CollaboratorsActionResult.CollaboratorsList(filterCollaborators(result.r, query))
}

override suspend fun addCollaborator(
Expand Down Expand Up @@ -106,6 +113,11 @@ class SimperiumCollaboratorsRepository @Inject constructor(
}
}

private fun filterCollaborators(note: Note) = note.tags.filter { tag ->
isValidCollaborator(tag)
}

private fun filterCollaborators(note: Note) = note.tags.filter { tag -> isValidCollaborator(tag) }
private fun filterCollaborators(note: Note, query: String?) = note.tags.filter { tag ->
isValidCollaborator(tag) && tag.lowercase(Locale.ROOT).contains(query?.lowercase(Locale.ROOT) ?: "")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class CollaboratorsViewModel @Inject constructor(

private lateinit var noteId: String

private var jobTagsFlow: Job? = null
private var jobCollaborators: Job? = null

fun loadCollaborators(noteId: String) {
this.noteId = noteId
Expand All @@ -33,28 +33,28 @@ class CollaboratorsViewModel @Inject constructor(
}
}

private suspend fun updateUiState(noteId: String) {
when (val result = collaboratorsRepository.getCollaborators(noteId)) {
private suspend fun updateUiState(noteId: String, searchUpdate: Boolean = false, searchQuery: String? = null) {
when (val result = collaboratorsRepository.getCollaborators(noteId, searchQuery)) {
is CollaboratorsActionResult.CollaboratorsList ->
_uiState.value = when (result.collaborators.isEmpty()) {
true -> UiState.EmptyCollaborators
false -> UiState.CollaboratorsList(result.collaborators)
true -> UiState.EmptyCollaborators(allCollaboratorsRemoved = searchQuery.isNullOrEmpty(), searchUpdate)
false -> UiState.CollaboratorsList(result.collaborators, searchUpdate, searchQuery)
}
is CollaboratorsActionResult.NoteDeleted -> _uiState.value = UiState.NoteDeleted
is CollaboratorsActionResult.NoteInTrash -> _uiState.value = UiState.NoteInTrash
}
}

fun startListeningChanges() {
jobTagsFlow = viewModelScope.launch {
jobCollaborators = viewModelScope.launch {
collaboratorsRepository.collaboratorsChanged(noteId).collect {
updateUiState(noteId)
}
}
}

fun stopListeningChanges() {
jobTagsFlow?.cancel()
jobCollaborators?.cancel()
}

fun clickAddCollaborator() {
Expand All @@ -77,12 +77,24 @@ class CollaboratorsViewModel @Inject constructor(
_event.value = Event.CloseCollaboratorsEvent
}

fun closeSearch() {
viewModelScope.launch {
updateUiState(noteId, searchUpdate = false)
}
}

fun search(searchQuery: String) {
viewModelScope.launch {
updateUiState(noteId, searchUpdate = true, searchQuery)
}
}

fun removeCollaborator(collaborator: String) {
viewModelScope.launch {
when (val result = collaboratorsRepository.removeCollaborator(noteId, collaborator)) {
is CollaboratorsActionResult.CollaboratorsList -> {
_uiState.value = when (result.collaborators.isEmpty()) {
true -> UiState.EmptyCollaborators
true -> UiState.EmptyCollaborators(allCollaboratorsRemoved = true)
false -> UiState.CollaboratorsList(result.collaborators)
}

Expand All @@ -102,8 +114,8 @@ class CollaboratorsViewModel @Inject constructor(
sealed class UiState {
object NoteInTrash : UiState()
object NoteDeleted : UiState()
object EmptyCollaborators : UiState()
data class CollaboratorsList(val collaborators: List<String>) : UiState()
data class EmptyCollaborators(val allCollaboratorsRemoved: Boolean, val searchUpdate: Boolean = false) : UiState()
data class CollaboratorsList(val collaborators: List<String>, val searchUpdate: Boolean = false, val searchQuery: String? = null) : UiState()
}

sealed class Event {
Expand Down
17 changes: 17 additions & 0 deletions Simplenote/src/main/res/menu/collaborators_list.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>

<menu
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"
tools:ignore="AlwaysShowAction">

<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/search_collaborators"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView">
</item>

</menu>
3 changes: 3 additions & 0 deletions Simplenote/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
<string name="note_added">Note added</string>
<string name="search">Search Notes or Tags</string>
<string name="search_hint">Search notes or tags</string>
<string name="search_collaborators">Search Collaborators</string>
<string name="search_collaborators_hint">Search collaborators</string>
<string name="search_tags">Search Tags</string>
<string name="search_tags_hint">Search tags</string>
<string name="notes">Notes</string>
Expand Down Expand Up @@ -347,6 +349,7 @@
<string name="collaborators_note_deleted">Note was deleted. You cannot edit collaborators on a note that was deleted.</string>
<string name="collaborators_note_trashed">Note was trashed. You cannot edit collaborators on a note that is in the trash.</string>
<string name="no_collaborators">No Collaborators</string>
<string name="no_collaborators_search">No collaborators found</string>

<!-- PASSCODE -->
<string name="passcode_change_passcode">Change passcode</string>
Expand Down
Loading