Code review

This commit is contained in:
Valere 2021-05-05 14:55:02 +02:00
parent ecceb0fb03
commit 168633b466
7 changed files with 293 additions and 103 deletions

View file

@ -81,7 +81,7 @@ import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet
import im.vector.app.features.spaces.ShareSpaceBottomSheet
import im.vector.app.features.spaces.SpaceCreationActivity
import im.vector.app.features.spaces.SpaceExploreActivity
import im.vector.app.features.spaces.SpaceInviteBottomSheet
import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet
import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet
import im.vector.app.features.spaces.manage.SpaceManageActivity
import im.vector.app.features.terms.ReviewTermsActivity

View file

@ -60,9 +60,9 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.ShareSpaceBottomSheet
import im.vector.app.features.spaces.SpaceCreationActivity
import im.vector.app.features.spaces.SpaceInviteBottomSheet
import im.vector.app.features.spaces.SpacePreviewActivity
import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet
import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
@ -89,7 +89,8 @@ class HomeActivity :
UnknownDeviceDetectorSharedViewModel.Factory,
ServerBackupStatusViewModel.Factory,
UnreadMessagesSharedViewModel.Factory,
NavigationInterceptor {
NavigationInterceptor,
SpaceInviteBottomSheet.InteractionListener {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -212,15 +213,7 @@ class HomeActivity :
.show(supportFragmentManager, "SPACE_SETTINGS")
}
is HomeActivitySharedAction.OpenSpaceInvite -> {
SpaceInviteBottomSheet.newInstance(sharedAction.spaceId, object : SpaceInviteBottomSheet.InteractionListener {
override fun onAccept(spaceId: String) {
navigator.switchToSpace(this@HomeActivity, spaceId, Navigator.PostSwitchSpaceAction.None)
}
override fun onDecline(spaceId: String) {
// nop
}
})
SpaceInviteBottomSheet.newInstance(sharedAction.spaceId)
.show(supportFragmentManager, "SPACE_INVITE")
}
}.exhaustive
@ -527,6 +520,14 @@ class HomeActivity :
return true
}
override fun spaceInviteBottomSheetOnAccept(spaceId: String) {
navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None)
}
override fun spaceInviteBottomSheetOnDecline(spaceId: String) {
// nop
}
companion object {
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
val args = HomeActivityArgs(

View file

@ -14,8 +14,9 @@
* limitations under the License.
*/
package im.vector.app.features.spaces
package im.vector.app.features.spaces.invite
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -23,30 +24,30 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.ButtonStateView
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.utils.toast
import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding
import im.vector.app.features.home.AvatarRenderer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetInvitedToSpaceBinding>() {
class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetInvitedToSpaceBinding>(), SpaceInviteBottomSheetViewModel.Factory {
interface InteractionListener {
fun onAccept(spaceId: String)
fun onDecline(spaceId: String)
fun spaceInviteBottomSheetOnAccept(spaceId: String)
fun spaceInviteBottomSheetOnDecline(spaceId: String)
}
var interactionListener: InteractionListener? = null
@ -56,14 +57,14 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
val spaceId: String
) : Parcelable
@Inject
lateinit var activeSessionHolder: ActiveSessionHolder
@Inject
lateinit var avatarRenderer: AvatarRenderer
@Inject
lateinit var errorFormatter: ErrorFormatter
private val viewModel: SpaceInviteBottomSheetViewModel by fragmentViewModel(SpaceInviteBottomSheetViewModel::class)
@Inject lateinit var viewModelFactory: SpaceInviteBottomSheetViewModel.Factory
override fun create(initialState: SpaceInviteBottomSheetState) = viewModelFactory.create(initialState)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@ -75,13 +76,50 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val session = activeSessionHolder.getSafeActiveSession() ?: return
val summary = session.getRoomSummary(inviteArgs.spaceId) ?: return Unit.also {
dismiss()
views.spaceCard.matrixToCardMainButton.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
// quick local echo
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Loading)
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true
viewModel.handle(SpaceInviteBottomSheetAction.DoJoin)
}
val inviter = summary.inviterId?.let { session.getUser(it) }?.toMatrixItem()
override fun onRetryClicked() = onButtonClicked()
}
views.spaceCard.matrixToCardSecondaryButton.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Loading)
views.spaceCard.matrixToCardMainButton.button.isEnabled = true
viewModel.handle(SpaceInviteBottomSheetAction.DoReject)
}
override fun onRetryClicked() = onButtonClicked()
}
viewModel.observeViewEvents {
when (it) {
is SpaceInviteBottomSheetEvents.ShowError -> requireActivity().toast(it.message)
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is InteractionListener) {
interactionListener = context
}
}
override fun onDetach() {
interactionListener = null
super.onDetach()
}
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
val summary = state.summary.invoke()
val inviter = state.inviterUser.invoke()?.toMatrixItem()
if (inviter != null) {
views.inviterAvatarImage.isVisible = true
views.inviterText.isVisible = true
@ -96,85 +134,52 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
}
views.spaceCard.matrixToCardContentVisibility.isVisible = true
avatarRenderer.renderSpace(summary.toMatrixItem(), views.spaceCard.matrixToCardAvatar)
views.spaceCard.matrixToCardNameText.text = summary.displayName
summary?.toMatrixItem()?.let { avatarRenderer.renderSpace(it, views.spaceCard.matrixToCardAvatar) }
views.spaceCard.matrixToCardNameText.text = summary?.displayName
views.spaceCard.matrixToBetaTag.isVisible = true
views.spaceCard.matrixToCardAliasText.setTextOrHide(summary.canonicalAlias)
views.spaceCard.matrixToCardDescText.setTextOrHide(summary.topic)
views.spaceCard.matrixToCardAliasText.setTextOrHide(summary?.canonicalAlias)
views.spaceCard.matrixToCardDescText.setTextOrHide(summary?.topic)
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Button)
views.spaceCard.matrixToCardMainButton.button.text = getString(R.string.accept)
views.spaceCard.matrixToCardMainButton.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
doJoin()
}
views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.decline)
override fun onRetryClicked() {
doJoin()
when (state.joinActionState) {
Uninitialized -> {
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Button)
}
private fun doJoin() {
is Loading -> {
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Loading)
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = false
GlobalScope.launch(Dispatchers.IO) {
try {
activeSessionHolder.getSafeActiveSession()?.getRoom(inviteArgs.spaceId)?.join()
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Loaded)
views.spaceCard.matrixToCardSecondaryButton.isEnabled = true
interactionListener?.onAccept(inviteArgs.spaceId)
}
is Success -> {
interactionListener?.spaceInviteBottomSheetOnAccept(inviteArgs.spaceId)
dismiss()
}
} catch (failure: Throwable) {
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
requireActivity().toast(errorFormatter.toHumanReadable(failure))
is Fail -> {
views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Error)
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true
}
}
}
}
}
when (state.rejectActionState) {
Uninitialized -> {
views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Button)
views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.reject)
views.spaceCard.matrixToCardSecondaryButton.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
doReject()
}
override fun onRetryClicked() {
doReject()
}
private fun doReject() {
is Loading -> {
views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Loading)
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = false
GlobalScope.launch(Dispatchers.IO) {
try {
activeSessionHolder.getSafeActiveSession()?.getRoom(inviteArgs.spaceId)?.leave()
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Loaded)
views.spaceCard.matrixToCardMainButton.button.isEnabled = true
interactionListener?.onDecline(inviteArgs.spaceId)
views.spaceCard.matrixToCardMainButton.button.isEnabled = false
}
is Success -> {
interactionListener?.spaceInviteBottomSheetOnDecline(inviteArgs.spaceId)
dismiss()
}
} catch (failure: Throwable) {
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
requireActivity().toast(errorFormatter.toHumanReadable(failure))
is Fail -> {
views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Error)
views.spaceCard.matrixToCardMainButton.button.isEnabled = true
}
}
}
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true
}
}
val memberCount = summary.otherMemberIds.size
val memberCount = summary?.otherMemberIds?.size ?: 0
if (memberCount != 0) {
views.spaceCard.matrixToMemberPills.isVisible = true
views.spaceCard.spaceChildMemberCountText.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)
@ -183,12 +188,7 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
views.spaceCard.matrixToMemberPills.isVisible = false
}
val knownMembers = summary.otherMemberIds.filter {
session.getExistingDirectRoomWithUser(it) != null
}.mapNotNull { session.getUser(it) }
// put one with avatar first, and take 5
val peopleYouKnow = (knownMembers.filter { it.avatarUrl != null } + knownMembers.filter { it.avatarUrl == null })
.take(5)
val peopleYouKnow = state.peopleYouKnow.invoke().orEmpty()
val images = listOf(
views.spaceCard.knownMember1,
@ -220,10 +220,9 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
companion object {
fun newInstance(spaceId: String, interactionListener: InteractionListener)
fun newInstance(spaceId: String)
: SpaceInviteBottomSheet {
return SpaceInviteBottomSheet().apply {
this.interactionListener = interactionListener
setArguments(Args(spaceId))
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.invite
import im.vector.app.core.platform.VectorViewModelAction
sealed class SpaceInviteBottomSheetAction : VectorViewModelAction {
object DoJoin : SpaceInviteBottomSheetAction()
object DoReject : SpaceInviteBottomSheetAction()
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.invite
import im.vector.app.core.platform.VectorViewEvents
sealed class SpaceInviteBottomSheetEvents : VectorViewEvents {
data class ShowError(val message: String) : SpaceInviteBottomSheetEvents()
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.invite
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.user.model.User
data class SpaceInviteBottomSheetState(
val spaceId: String,
val summary: Async<RoomSummary> = Uninitialized,
val inviterUser: Async<User> = Uninitialized,
val peopleYouKnow: Async<List<User>> = Uninitialized,
val joinActionState: Async<Unit> = Uninitialized,
val rejectActionState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: SpaceInviteBottomSheet.Args) : this(
spaceId = args.spaceId
)
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.invite
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
class SpaceInviteBottomSheetViewModel @AssistedInject constructor(
@Assisted private val initialState: SpaceInviteBottomSheetState,
private val session: Session,
private val errorFormatter: ErrorFormatter
) : VectorViewModel<SpaceInviteBottomSheetState, SpaceInviteBottomSheetAction, SpaceInviteBottomSheetEvents>(initialState) {
init {
session.getRoomSummary(initialState.spaceId)?.let { roomSummary ->
val knownMembers = roomSummary.otherMemberIds.filter {
session.getExistingDirectRoomWithUser(it) != null
}.mapNotNull { session.getUser(it) }
// put one with avatar first, and take 5
val peopleYouKnow = (knownMembers.filter { it.avatarUrl != null } + knownMembers.filter { it.avatarUrl == null })
.take(5)
setState {
copy(
summary = Success(roomSummary),
inviterUser = roomSummary.inviterId?.let { session.getUser(it) }?.let { Success(it) } ?: Uninitialized,
peopleYouKnow = Success(peopleYouKnow)
)
}
}
}
@AssistedFactory
interface Factory {
fun create(initialState: SpaceInviteBottomSheetState): SpaceInviteBottomSheetViewModel
}
companion object : MvRxViewModelFactory<SpaceInviteBottomSheetViewModel, SpaceInviteBottomSheetState> {
override fun create(viewModelContext: ViewModelContext, state: SpaceInviteBottomSheetState): SpaceInviteBottomSheetViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: SpaceInviteBottomSheetAction) {
when (action) {
SpaceInviteBottomSheetAction.DoJoin -> {
setState { copy(joinActionState = Loading()) }
session.coroutineScope.launch(Dispatchers.IO) {
try {
session.getRoom(initialState.spaceId)?.join()
setState { copy(joinActionState = Success(Unit)) }
} catch (failure: Throwable) {
setState { copy(joinActionState = Fail(failure)) }
_viewEvents.post(SpaceInviteBottomSheetEvents.ShowError(errorFormatter.toHumanReadable(failure)))
}
}
}
SpaceInviteBottomSheetAction.DoReject -> {
setState { copy(rejectActionState = Loading()) }
session.coroutineScope.launch(Dispatchers.IO) {
try {
session.getRoom(initialState.spaceId)?.leave()
setState { copy(rejectActionState = Success(Unit)) }
} catch (failure: Throwable) {
setState { copy(rejectActionState = Fail(failure)) }
_viewEvents.post(SpaceInviteBottomSheetEvents.ShowError(errorFormatter.toHumanReadable(failure)))
}
}
}
}
}
}