diff --git a/app/src/main/java/chat/rocket/android/analytics/event/ScreenViewEvent.kt b/app/src/main/java/chat/rocket/android/analytics/event/ScreenViewEvent.kt index a1f6fc6f8a..7335bb6ce7 100644 --- a/app/src/main/java/chat/rocket/android/analytics/event/ScreenViewEvent.kt +++ b/app/src/main/java/chat/rocket/android/analytics/event/ScreenViewEvent.kt @@ -20,6 +20,7 @@ sealed class ScreenViewEvent(val screenName: String) { object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment") object Files : ScreenViewEvent("FilesFragment") object Members : ScreenViewEvent("MembersFragment") + object InviteUsers : ScreenViewEvent("InviteUsersFragment") object Mentions : ScreenViewEvent("MentionsFragment") object MessageInfo : ScreenViewEvent("MessageInfoFragment") object Password : ScreenViewEvent("PasswordFragment") diff --git a/app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomNavigator.kt b/app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomNavigator.kt index 42f3458f7a..2582ac88f4 100644 --- a/app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomNavigator.kt +++ b/app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomNavigator.kt @@ -9,6 +9,7 @@ import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.chatRoomIntent import chat.rocket.android.favoritemessages.ui.TAG_FAVORITE_MESSAGES_FRAGMENT import chat.rocket.android.files.ui.TAG_FILES_FRAGMENT +import chat.rocket.android.inviteusers.ui.TAG_INVITE_USERS_FRAGMENT import chat.rocket.android.members.ui.TAG_MEMBERS_FRAGMENT import chat.rocket.android.mentions.ui.TAG_MENTIONS_FRAGMENT import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT @@ -87,6 +88,12 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) { } } + fun toInviteUsers(chatRoomId: String) { + activity.addFragmentBackStack(TAG_INVITE_USERS_FRAGMENT, R.id.fragment_container) { + chat.rocket.android.inviteusers.ui.newInstance(chatRoomId) + } + } + fun toMemberDetails(userId: String, chatRoomId: String) { activity.addFragmentBackStack(TAG_USER_DETAILS_FRAGMENT, R.id.fragment_container) { chat.rocket.android.userdetails.ui.newInstance(userId, chatRoomId) diff --git a/app/src/main/java/chat/rocket/android/dagger/module/ActivityBuilder.kt b/app/src/main/java/chat/rocket/android/dagger/module/ActivityBuilder.kt index 2893e634dd..d9c4919c3b 100644 --- a/app/src/main/java/chat/rocket/android/dagger/module/ActivityBuilder.kt +++ b/app/src/main/java/chat/rocket/android/dagger/module/ActivityBuilder.kt @@ -24,6 +24,7 @@ import chat.rocket.android.draw.main.di.DrawModule import chat.rocket.android.draw.main.ui.DrawingActivity import chat.rocket.android.favoritemessages.di.FavoriteMessagesFragmentProvider import chat.rocket.android.files.di.FilesFragmentProvider +import chat.rocket.android.inviteusers.di.InviteUsersFragmentProvider import chat.rocket.android.main.di.MainModule import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.members.di.MembersFragmentProvider @@ -84,6 +85,7 @@ abstract class ActivityBuilder { UserDetailsFragmentProvider::class, ChatDetailsFragmentProvider::class, MembersFragmentProvider::class, + InviteUsersFragmentProvider::class, MentionsFragmentProvider::class, PinnedMessagesFragmentProvider::class, FavoriteMessagesFragmentProvider::class, diff --git a/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentModule.kt b/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentModule.kt new file mode 100644 index 0000000000..321aeefd93 --- /dev/null +++ b/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentModule.kt @@ -0,0 +1,17 @@ +package chat.rocket.android.inviteusers.di + +import chat.rocket.android.dagger.scope.PerFragment +import chat.rocket.android.inviteusers.presentation.InviteUsersView +import chat.rocket.android.inviteusers.ui.InviteUsersFragment +import dagger.Module +import dagger.Provides + +@Module +class InviteUsersFragmentModule { + + @Provides + @PerFragment + fun inviteUsersView(frag: InviteUsersFragment): InviteUsersView { + return frag + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentProvider.kt b/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentProvider.kt new file mode 100644 index 0000000000..9a0b1f840e --- /dev/null +++ b/app/src/main/java/chat/rocket/android/inviteusers/di/InviteUsersFragmentProvider.kt @@ -0,0 +1,14 @@ +package chat.rocket.android.inviteusers.di + +import chat.rocket.android.dagger.scope.PerFragment +import chat.rocket.android.inviteusers.ui.InviteUsersFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class InviteUsersFragmentProvider { + + @ContributesAndroidInjector(modules = [InviteUsersFragmentModule::class]) + @PerFragment + abstract fun provideInviteUsersFragment(): InviteUsersFragment +} \ No newline at end of file diff --git a/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersPresenter.kt b/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersPresenter.kt new file mode 100644 index 0000000000..4fa9216771 --- /dev/null +++ b/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersPresenter.kt @@ -0,0 +1,85 @@ +package chat.rocket.android.inviteusers.presentation + +import chat.rocket.android.core.lifecycle.CancelStrategy +import chat.rocket.android.db.DatabaseManager +import chat.rocket.android.members.uimodel.MemberUiModelMapper +import chat.rocket.android.server.infrastructure.RocketChatClientFactory +import chat.rocket.android.util.extension.launchUI +import chat.rocket.common.RocketChatException +import chat.rocket.common.model.roomTypeOf +import chat.rocket.common.util.ifNull +import chat.rocket.core.RocketChatClient +import chat.rocket.core.internal.rest.invite +import chat.rocket.core.internal.rest.spotlight +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Named + +class InviteUsersPresenter @Inject constructor( + private val view: InviteUsersView, + private val dbManager: DatabaseManager, + @Named("currentServer") private val currentServer: String, + private val strategy: CancelStrategy, + private val mapper: MemberUiModelMapper, + val factory: RocketChatClientFactory +) { + private val client: RocketChatClient = factory.get(currentServer) + + fun inviteUsers(chatRoomId: String, usersList: List>) { + launchUI(strategy) { + view.disableUserInput() + view.showLoading() + + val stringBuilder = StringBuilder() + + try { + for (user in usersList) { + try { + client.invite(chatRoomId, roomTypeOf(getChatRoomType(chatRoomId)), user.first) + stringBuilder.append("Invited : ${user.second}\n") + } catch (exception: RocketChatException) { + exception.message?.let { + stringBuilder.append("Exception : ${user.second} : $it\n") + }.ifNull { + stringBuilder.append("Error : ${user.second} : Try again later\n") + } + } + } + } finally { + view.showMessage(stringBuilder.toString()) + view.hideLoading() + view.enableUserInput() + view.usersInvitedSuccessfully() + } + } + } + + fun searchUser(query: String) { + launchUI(strategy) { + view.showSuggestionViewInProgress() + try { + val users = client.spotlight(query).users + if (users.isEmpty()) { + view.showNoUserSuggestion() + } else { + view.showUserSuggestion(mapper.mapToUiModelList(users)) + } + } catch (ex: RocketChatException) { + ex.message?.let { + view.showMessage(it) + }.ifNull { + view.showGenericErrorMessage() + } + } finally { + view.hideSuggestionViewInProgress() + } + } + } + + private suspend fun getChatRoomType(chatRoomId: String): String { + return withContext(Dispatchers.IO + strategy.jobs) { + return@withContext dbManager.getRoom(chatRoomId)?.chatRoom.let { it?.type ?: "" } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersView.kt b/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersView.kt new file mode 100644 index 0000000000..6758a59924 --- /dev/null +++ b/app/src/main/java/chat/rocket/android/inviteusers/presentation/InviteUsersView.kt @@ -0,0 +1,45 @@ +package chat.rocket.android.inviteusers.presentation + +import chat.rocket.android.core.behaviours.LoadingView +import chat.rocket.android.core.behaviours.MessageView +import chat.rocket.android.members.uimodel.MemberUiModel + +interface InviteUsersView : LoadingView, MessageView { + + /** + * Shows the server's users suggestion (on the basis of the user typing - the query). + * + * @param dataSet The list of server's users to show. + */ + fun showUserSuggestion(dataSet: List) + + /** + * Shows no server's users suggestion. + */ + fun showNoUserSuggestion() + + /** + * Shows the SuggestionView in progress. + */ + fun showSuggestionViewInProgress() + + /** + * Hides the progress shown in the SuggestionView. + */ + fun hideSuggestionViewInProgress() + + /** + * Take actions after users are successfully invited. + */ + fun usersInvitedSuccessfully() + + /** + * Enables the user input. + */ + fun enableUserInput() + + /** + * Disables the user input. + */ + fun disableUserInput() +} \ No newline at end of file diff --git a/app/src/main/java/chat/rocket/android/inviteusers/ui/InviteUsersFragment.kt b/app/src/main/java/chat/rocket/android/inviteusers/ui/InviteUsersFragment.kt new file mode 100644 index 0000000000..9fe55003ae --- /dev/null +++ b/app/src/main/java/chat/rocket/android/inviteusers/ui/InviteUsersFragment.kt @@ -0,0 +1,252 @@ +package chat.rocket.android.inviteusers.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import chat.rocket.android.R +import chat.rocket.android.analytics.AnalyticsManager +import chat.rocket.android.analytics.event.ScreenViewEvent +import chat.rocket.android.chatroom.ui.ChatRoomActivity +import chat.rocket.android.inviteusers.presentation.InviteUsersPresenter +import chat.rocket.android.inviteusers.presentation.InviteUsersView +import chat.rocket.android.members.adapter.MembersAdapter +import chat.rocket.android.members.uimodel.MemberUiModel +import chat.rocket.android.util.extension.asObservable +import chat.rocket.android.util.extensions.inflate +import chat.rocket.android.util.extensions.showToast +import chat.rocket.android.util.extensions.ui +import com.google.android.material.chip.Chip +import dagger.android.support.AndroidSupportInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_invite_users.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +fun newInstance(chatRoomId: String): Fragment = InviteUsersFragment().apply { + arguments = Bundle(1).apply { + putString(BUNDLE_CHAT_ROOM_ID, chatRoomId) + } +} + +internal const val TAG_INVITE_USERS_FRAGMENT = "InviteUsersFragment" +private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id" + +class InviteUsersFragment : Fragment(), InviteUsersView { + + @Inject + lateinit var presenter: InviteUsersPresenter + @Inject + lateinit var analyticsManager: AnalyticsManager + private val compositeDisposable = CompositeDisposable() + private val adapter: MembersAdapter = MembersAdapter { + it.username?.run { processSelectedMember(Pair(it.userId, it.username)) } + } + + private lateinit var chatRoomId: String + // Pair of userId and userName + private var memberList = arrayListOf>() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidSupportInjection.inject(this) + + arguments?.run { + chatRoomId = getString(BUNDLE_CHAT_ROOM_ID, "") + } + ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = container?.inflate(R.layout.fragment_invite_users) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolBar() + setupViewListeners() + setupRecyclerView() + subscribeEditTexts() + + analyticsManager.logScreenView(ScreenViewEvent.InviteUsers) + } + + override fun onDestroyView() { + super.onDestroyView() + unsubscribeEditTexts() + } + + override fun showLoading() { + ui { + view_loading.isVisible = true + } + } + + override fun hideLoading() { + ui { + view_loading.isVisible = false + } + } + + override fun showMessage(resId: Int) { + ui { + showToast(resId) + } + } + + override fun showMessage(message: String) { + ui { + showToast(message) + } + } + + override fun showGenericErrorMessage() { + showMessage(getString(R.string.msg_generic_error)) + } + + override fun showUserSuggestion(dataSet: List) { + adapter.clearData() + adapter.prependData(dataSet) + text_member_not_found.isVisible = false + recycler_view.isVisible = true + view_member_suggestion.isVisible = true + } + + override fun showNoUserSuggestion() { + recycler_view.isVisible = false + text_member_not_found.isVisible = true + view_member_suggestion.isVisible = true + } + + override fun showSuggestionViewInProgress() { + recycler_view.isVisible = false + text_member_not_found.isVisible = false + view_member_suggestion_loading.isVisible = true + view_member_suggestion.isVisible = true + } + + override fun hideSuggestionViewInProgress() { + view_member_suggestion_loading.isVisible = false + } + + override fun usersInvitedSuccessfully() { + memberList.clear() + activity?.onBackPressed() + } + + override fun enableUserInput() { + edit_text_invite_users.isEnabled = true + } + + override fun disableUserInput() { + edit_text_invite_users.isEnabled = false + } + + private fun setupToolBar() { + (activity as ChatRoomActivity).setupToolbarTitle((getString(R.string.title_invite_users))) + } + + private fun setupRecyclerView() { + ui { + recycler_view.layoutManager = + LinearLayoutManager(context, RecyclerView.VERTICAL, false) + recycler_view.addItemDecoration( + DividerItemDecoration(it, DividerItemDecoration.HORIZONTAL) + ) + recycler_view.adapter = adapter + } + } + + private fun setupViewListeners() { + text_cancel.setOnClickListener { activity?.onBackPressed() } + text_invite_users.setOnClickListener { + if (memberList.isNotEmpty()) { + presenter.inviteUsers(chatRoomId, memberList) + } + } + } + + private fun subscribeEditTexts() { + + val inviteMembersDisposable = edit_text_invite_users.asObservable() + .debounce(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) + .filter { t -> t.isNotBlank() } + .subscribe { + if (it.length >= 3) { + presenter.searchUser(it.toString()) + } else { + view_member_suggestion.isVisible = false + } + } + + compositeDisposable.addAll(inviteMembersDisposable) + } + + private fun unsubscribeEditTexts() { + compositeDisposable.dispose() + } + + private fun processSelectedMember(member: Pair) { + if (memberList.any { it.second == member.second }) { + showMessage(getString(R.string.msg_member_already_added)) + } else { + view_member_suggestion.isVisible = false + edit_text_invite_users.setText("") + addMember(member) + addChip(member.second) + chip_group_member.isVisible = true + processBackgroundOfInviteUsersButton() + } + } + + private fun addMember(member: Pair) { + memberList.add(member) + } + + private fun removeMember(username: String) { + memberList.remove(memberList.find { it.second == username }) + } + + private fun addChip(chipText: String) { + val chip = Chip(context) + chip.text = chipText + chip.isCloseIconVisible = true + chip.setChipBackgroundColorResource(R.color.icon_grey) + setupChipOnCloseIconClickListener(chip) + chip_group_member.addView(chip) + } + + private fun setupChipOnCloseIconClickListener(chip: Chip) { + chip.setOnCloseIconClickListener { + removeChip(it) + removeMember((it as Chip).text.toString()) + // whenever we remove a chip we should process the chip group visibility. + processChipGroupVisibility() + processBackgroundOfInviteUsersButton() + } + } + + private fun removeChip(chip: View) { + chip_group_member.removeView(chip) + } + + private fun processChipGroupVisibility() { + chip_group_member.isVisible = memberList.isNotEmpty() + } + + private fun processBackgroundOfInviteUsersButton() { + if (memberList.isEmpty()) { + text_invite_users.alpha = 0.4F + } else { + text_invite_users.alpha = 1F + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/rocket/android/members/presentation/MembersPresenter.kt b/app/src/main/java/chat/rocket/android/members/presentation/MembersPresenter.kt index caee387154..dde3efe57b 100644 --- a/app/src/main/java/chat/rocket/android/members/presentation/MembersPresenter.kt +++ b/app/src/main/java/chat/rocket/android/members/presentation/MembersPresenter.kt @@ -86,6 +86,6 @@ class MembersPresenter @Inject constructor( } fun toInviteUsers(chatRoomId: String){ - + navigator.toInviteUsers(chatRoomId) } } diff --git a/app/src/main/java/chat/rocket/android/members/ui/MembersFragment.kt b/app/src/main/java/chat/rocket/android/members/ui/MembersFragment.kt index ec88311fb7..09c312bc30 100644 --- a/app/src/main/java/chat/rocket/android/members/ui/MembersFragment.kt +++ b/app/src/main/java/chat/rocket/android/members/ui/MembersFragment.kt @@ -112,11 +112,11 @@ class MembersFragment : Fragment(), MembersView { } override fun showInviteUsersButton() { - ui { layout_invite_users.isVisible = true } + ui { text_invite_users.isVisible = true } } override fun hideInviteUserButton() { - ui { layout_invite_users.isVisible = false } + ui { text_invite_users.isVisible = false } } override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) @@ -147,6 +147,6 @@ class MembersFragment : Fragment(), MembersView { } private fun setupListeners(){ - layout_invite_users.setOnClickListener { presenter.toInviteUsers(chatRoomId) } + text_invite_users.setOnClickListener { presenter.toInviteUsers(chatRoomId) } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_invite_users.xml b/app/src/main/res/layout/fragment_invite_users.xml new file mode 100644 index 0000000000..4bab91e6e7 --- /dev/null +++ b/app/src/main/res/layout/fragment_invite_users.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_members.xml b/app/src/main/res/layout/fragment_members.xml index 60c5b0a4b0..902151a508 100644 --- a/app/src/main/res/layout/fragment_members.xml +++ b/app/src/main/res/layout/fragment_members.xml @@ -12,7 +12,7 @@ android:layout_height="0dp" android:layout_marginBottom="8dp" android:scrollbars="vertical" - app:layout_constraintBottom_toTopOf="@+id/layout_invite_users" + app:layout_constraintBottom_toTopOf="@+id/text_invite_users" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -32,7 +32,7 @@ app:layout_constraintTop_toTopOf="parent" /> Logout from Rocket.Chat Delete account Change status - Removed user successfully English @@ -286,6 +285,14 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin Members + + Invite Users + Cancel + Invite Users + + + Removed user successfully + Mentions No mention