Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to collaboration UI #1510

Merged
merged 18 commits into from
Sep 29, 2021
Merged
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
@@ -1,19 +1,20 @@
package com.automattic.simplenote

import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.os.Handler
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import com.automattic.simplenote.databinding.AddCollaboratorBinding
import com.automattic.simplenote.utils.DisplayUtils
import com.automattic.simplenote.viewmodels.AddCollaboratorViewModel
import com.automattic.simplenote.viewmodels.AddCollaboratorViewModel.Event
import com.automattic.simplenote.widgets.MorphCircleToRectangle
import dagger.hilt.android.AndroidEntryPoint

Expand Down Expand Up @@ -83,16 +84,17 @@ class AddCollaboratorFragment(private val noteId: String) : AppCompatDialogFragm
private fun setObservers() {
viewModel.event.observe(this, { event ->
when (event) {
AddCollaboratorViewModel.Event.Close,
AddCollaboratorViewModel.Event.NoteDeleted, // In case the note is deleted, the activity handles it.
AddCollaboratorViewModel.Event.NoteInTrash, // In case the note is trashed, the activity handles it.
AddCollaboratorViewModel.Event.CollaboratorAdded -> dismiss()
AddCollaboratorViewModel.Event.InvalidCollaborator -> setErrorInputField()
Event.Close,
Event.NoteDeleted, // In case the note is deleted, the activity handles it.
Event.NoteInTrash, // In case the note is trashed, the activity handles it.
Event.CollaboratorAdded -> dismiss()
Event.InvalidCollaborator -> setErrorInputField(R.string.invalid_collaborator)
Event.CollaboratorCurrentUser -> setErrorInputField(R.string.collaborator_is_current_user)
}
})
}

private fun setErrorInputField() {
binding.collaboratorInput.error = getString(R.string.invalid_collaborator)
private fun setErrorInputField(@StringRes message: Int) {
binding.collaboratorInput.error = getString(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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.*
import com.automattic.simplenote.utils.CollaboratorsAdapter.CollaboratorDataItem.*
import com.automattic.simplenote.utils.toast
import com.automattic.simplenote.viewmodels.CollaboratorsViewModel
import com.automattic.simplenote.viewmodels.CollaboratorsViewModel.Event
Expand Down Expand Up @@ -73,8 +75,13 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
collaboratorsList.adapter = CollaboratorsAdapter(viewModel::clickRemoveCollaborator)
collaboratorsList.isNestedScrollingEnabled = false
collaboratorsList.layoutManager = LinearLayoutManager(this@CollaboratorsActivity)
collaboratorsList.setEmptyView(empty.root)

rowAddCollaborator.setOnClickListener { viewModel.clickAddCollaborator() }
buttonAddCollaborator.setOnClickListener { viewModel.clickAddCollaborator() }

empty.image.setImageResource(R.drawable.ic_collaborate_24dp)
empty.title.text = getString(R.string.no_collaborators)
empty.message.text = getString(R.string.add_email_collaborator_message)
}

private fun setupToolbar() {
Expand Down Expand Up @@ -113,21 +120,26 @@ class CollaboratorsActivity : ThemedAppCompatActivity() {
}

private fun ActivityCollaboratorsBinding.handleCollaboratorsList(collaborators: List<String>) {
sharedMessage.visibility = View.VISIBLE
collaboratorsList.visibility = View.VISIBLE
dividerLine.visibility = View.VISIBLE
buttonAddCollaborator.visibility = View.VISIBLE
emptyMessage.visibility = View.GONE

(collaboratorsList.adapter as CollaboratorsAdapter).submitList(collaborators)
hideEmptyView()
val items = listOf(HeaderItem) + collaborators.map { CollaboratorItem(it) }
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(items)
}

private fun ActivityCollaboratorsBinding.handleEmptyCollaborators() {
sharedMessage.visibility = View.GONE
collaboratorsList.visibility = View.GONE
dividerLine.visibility = View.GONE
buttonAddCollaborator.visibility = View.VISIBLE
emptyMessage.visibility = View.VISIBLE
showEmptyView()
(collaboratorsList.adapter as CollaboratorsAdapter).submitList(emptyList())
}

private fun ActivityCollaboratorsBinding.hideEmptyView() {
empty.image.visibility = View.GONE
empty.title.visibility = View.GONE
empty.message.visibility = View.GONE
}

private fun ActivityCollaboratorsBinding.showEmptyView() {
empty.image.visibility = View.VISIBLE
empty.title.visibility = View.VISIBLE
empty.message.visibility = View.VISIBLE
}

private fun showAddCollaboratorFragment(event: Event.AddCollaboratorEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.automattic.simplenote;

import static com.automattic.simplenote.Simplenote.SCROLL_POSITION_PREFERENCES;
import static com.automattic.simplenote.analytics.AnalyticsTracker.CATEGORY_NOTE;
import static com.automattic.simplenote.utils.SimplenoteLinkify.SIMPLENOTE_LINK_PREFIX;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
Expand Down Expand Up @@ -41,9 +46,6 @@
import java.lang.ref.SoftReference;
import java.util.Set;

import static com.automattic.simplenote.Simplenote.SCROLL_POSITION_PREFERENCES;
import static com.automattic.simplenote.utils.SimplenoteLinkify.SIMPLENOTE_LINK_PREFIX;

public class NoteMarkdownFragment extends Fragment implements Bucket.Listener<Note> {
public static final String ARG_ITEM_ID = "item_id";

Expand Down Expand Up @@ -185,6 +187,9 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
case R.id.menu_delete:
NoteUtils.showDialogDeletePermanently(requireActivity(), mNote);
return true;
case R.id.menu_collaborators:
navigateToCollaborators();
return true;
case R.id.menu_trash:
if (!isAdded()) {
return false;
Expand All @@ -194,9 +199,9 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
return true;
case R.id.menu_copy_internal:
AnalyticsTracker.track(
AnalyticsTracker.Stat.INTERNOTE_LINK_COPIED,
AnalyticsTracker.CATEGORY_LINK,
"internote_link_copied_markdown"
AnalyticsTracker.Stat.INTERNOTE_LINK_COPIED,
AnalyticsTracker.CATEGORY_LINK,
"internote_link_copied_markdown"
);

if (!isAdded()) {
Expand All @@ -215,6 +220,22 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
}
}

private void navigateToCollaborators() {
if (getActivity() == null || mNote == null) {
return;
}

Intent intent = new Intent(requireActivity(), CollaboratorsActivity.class);
intent.putExtra(CollaboratorsActivity.NOTE_ID_ARG, mNote.getSimperiumKey());
startActivity(intent);

AnalyticsTracker.track(
AnalyticsTracker.Stat.EDITOR_COLLABORATORS_ACCESSED,
CATEGORY_NOTE,
"collaborators_ui_accessed"
);
}

private void deleteNote() {
NoteUtils.deleteNote(mNote, getActivity());
requireActivity().finish();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.automattic.simplenote.authentication

import com.simperium.Simperium
import com.simperium.client.User
import javax.inject.Inject

sealed class UserSession {
object UnauthorizedUser : UserSession()
data class AuthorizedUser(val user: User) : UserSession()
}

class SessionManager @Inject constructor(private val simperium: Simperium) {
ParaskP7 marked this conversation as resolved.
Show resolved Hide resolved
fun getCurrentUser(): UserSession {
val currentUser = simperium.user ?: return UserSession.UnauthorizedUser

return when (currentUser.email != null && !currentUser.needsAuthorization()) {
true -> UserSession.AuthorizedUser(currentUser)
false -> UserSession.UnauthorizedUser
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package com.automattic.simplenote.di

import android.content.Context
import com.automattic.simplenote.Simplenote
import com.automattic.simplenote.models.Note
import com.automattic.simplenote.models.Tag
import com.automattic.simplenote.repositories.CollaboratorsRepository
import com.automattic.simplenote.repositories.SimperiumCollaboratorsRepository
import com.automattic.simplenote.repositories.SimperiumTagsRepository
import com.automattic.simplenote.repositories.TagsRepository
import com.simperium.Simperium
import com.simperium.client.Bucket
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.ExperimentalCoroutinesApi

@ExperimentalCoroutinesApi
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
Expand All @@ -25,6 +26,9 @@ abstract class DataModule {

@Provides
fun providesNotesBucket(simplenote: Simplenote): Bucket<Note> = simplenote.notesBucket

@Provides
fun providesSimperium(simplenote: Simplenote): Simperium = simplenote.simperium
}

@Binds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,73 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.automattic.simplenote.databinding.CollaboratorRowBinding
import com.automattic.simplenote.databinding.CollaboratorsHeaderBinding

class CollaboratorsAdapter(
private val onDeleteClick: (collaborator: String) -> Unit,
) : ListAdapter<String, CollaboratorsAdapter.CollaboratorViewHolder>(DIFF_CALLBACK) {
) : ListAdapter<CollaboratorsAdapter.CollaboratorDataItem, RecyclerView.ViewHolder>(DIFF_CALLBACK) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollaboratorViewHolder {
val binding = CollaboratorRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CollaboratorViewHolder(binding, onDeleteClick)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> {
val binding = CollaboratorsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CollaboratorHeaderViewHolder(binding)
}
ITEM_VIEW_TYPE_ITEM -> {
val binding = CollaboratorRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CollaboratorViewHolder(binding, onDeleteClick)
}
else -> throw ClassCastException("Unknown viewType $viewType")
}
}

override fun onBindViewHolder(holder: CollaboratorViewHolder, position: Int) {
holder.bind(getItem(position))
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is CollaboratorDataItem.HeaderItem -> ITEM_VIEW_TYPE_HEADER
is CollaboratorDataItem.CollaboratorItem -> ITEM_VIEW_TYPE_ITEM
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is CollaboratorViewHolder) {
holder.bind(getItem(position) as CollaboratorDataItem.CollaboratorItem)
}
}

sealed class CollaboratorDataItem {
abstract val id: Int
object HeaderItem : CollaboratorDataItem() {
override val id = Int.MIN_VALUE
}
data class CollaboratorItem(val email: String) : CollaboratorDataItem() {
override val id = email.hashCode()
}
}

class CollaboratorHeaderViewHolder(
binding: CollaboratorsHeaderBinding
) : RecyclerView.ViewHolder(binding.root)

class CollaboratorViewHolder(
private val binding: CollaboratorRowBinding,
private val onDeleteClick: (collaborator: String) -> Unit
): RecyclerView.ViewHolder(binding.root) {

fun bind(collaborator: String) {
binding.collaboratorText.text = collaborator
binding.collaboratorRemoveButton.setOnClickListener { onDeleteClick(collaborator) }
fun bind(collaborator: CollaboratorDataItem.CollaboratorItem) {
binding.collaboratorText.text = collaborator.email
binding.collaboratorRemoveButton.setOnClickListener { onDeleteClick(collaborator.email) }
}
}

companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
private const val ITEM_VIEW_TYPE_HEADER = 0
private const val ITEM_VIEW_TYPE_ITEM = 1
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<CollaboratorDataItem>() {
override fun areItemsTheSame(oldItem: CollaboratorDataItem, newItem: CollaboratorDataItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
override fun areContentsTheSame(oldItem: CollaboratorDataItem, newItem: CollaboratorDataItem): Boolean {
return oldItem == newItem
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.automattic.simplenote.analytics.AnalyticsTracker
import com.automattic.simplenote.authentication.SessionManager
import com.automattic.simplenote.authentication.UserSession
import com.automattic.simplenote.repositories.CollaboratorsActionResult
import com.automattic.simplenote.repositories.CollaboratorsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -12,39 +14,51 @@ import javax.inject.Inject

@HiltViewModel
class AddCollaboratorViewModel @Inject constructor(
private val collaboratorsRepository: CollaboratorsRepository
private val collaboratorsRepository: CollaboratorsRepository,
private val sessionManager: SessionManager
) : ViewModel() {
private val _event = SingleLiveEvent<Event>()
val event: LiveData<Event> = _event

fun addCollaborator(noteId: String, collaborator: String) {
viewModelScope.launch {
when (collaboratorsRepository.isValidCollaborator(collaborator)) {
true -> when (collaboratorsRepository.addCollaborator(noteId, collaborator)) {
is CollaboratorsActionResult.CollaboratorsList -> {
_event.value = Event.CollaboratorAdded
when (isCurrentUser(collaborator)) {
true -> _event.value = Event.CollaboratorCurrentUser
false -> viewModelScope.launch {
when (collaboratorsRepository.isValidCollaborator(collaborator)) {
true -> when (collaboratorsRepository.addCollaborator(noteId, collaborator)) {
is CollaboratorsActionResult.CollaboratorsList -> {
_event.value = Event.CollaboratorAdded

AnalyticsTracker.track(
AnalyticsTracker.Stat.COLLABORATOR_ADDED,
AnalyticsTracker.CATEGORY_NOTE,
"collaborator_added_to_note",
mapOf("source" to "collaborators")
)
AnalyticsTracker.track(
AnalyticsTracker.Stat.COLLABORATOR_ADDED,
AnalyticsTracker.CATEGORY_NOTE,
"collaborator_added_to_note",
mapOf("source" to "collaborators")
)
}
CollaboratorsActionResult.NoteDeleted -> _event.value = Event.NoteDeleted
CollaboratorsActionResult.NoteInTrash -> _event.value = Event.NoteInTrash
}
CollaboratorsActionResult.NoteDeleted -> _event.value = Event.NoteDeleted
CollaboratorsActionResult.NoteInTrash -> _event.value = Event.NoteInTrash
false -> _event.value = Event.InvalidCollaborator
}
false -> _event.value = Event.InvalidCollaborator
}
}
}

private fun isCurrentUser(collaborator: String): Boolean {
return when (val currentUser = sessionManager.getCurrentUser()) {
is UserSession.AuthorizedUser -> currentUser.user.email == collaborator
is UserSession.UnauthorizedUser -> false // This should not happen.
}
}

fun close() {
_event.value = Event.Close
}

sealed class Event {
object InvalidCollaborator : Event()
object CollaboratorCurrentUser : Event()
object CollaboratorAdded : Event()
object Close : Event()
object NoteInTrash : Event()
Expand Down
Loading