diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 0df41d106d..a2a242a3d9 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -24,6 +24,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.ui.UiStateRepository import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -68,7 +69,7 @@ class AppStateHandler @Inject constructor( val spaceSum = spaceId?.let { uSession?.getRoomSummary(spaceId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { - GlobalScope.launch { + GlobalScope.launch(Dispatchers.IO) { tryOrNull { uSession?.getRoom(spaceId)?.loadRoomMembersIfNeeded() } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index b6c75beb02..c16c602530 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -81,6 +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.invite.SpaceInviteBottomSheet import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet import im.vector.app.features.spaces.manage.SpaceManageActivity import im.vector.app.features.terms.ReviewTermsActivity @@ -185,6 +186,7 @@ interface ScreenComponent { fun inject(bottomSheet: ShareSpaceBottomSheet) fun inject(bottomSheet: SpaceSettingsMenuBottomSheet) fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet) + fun inject(bottomSheet: SpaceInviteBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index f01c27c30e..1de1ff1c3e 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -62,6 +62,7 @@ import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity 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 @@ -88,7 +89,8 @@ class HomeActivity : UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory, UnreadMessagesSharedViewModel.Factory, - NavigationInterceptor { + NavigationInterceptor, + SpaceInviteBottomSheet.InteractionListener { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -210,6 +212,10 @@ class HomeActivity : }) .show(supportFragmentManager, "SPACE_SETTINGS") } + is HomeActivitySharedAction.OpenSpaceInvite -> { + SpaceInviteBottomSheet.newInstance(sharedAction.spaceId) + .show(supportFragmentManager, "SPACE_INVITE") + } }.exhaustive } .disposeOnDestroy() @@ -514,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( diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index db0a9ba9eb..d79f24fc4c 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -27,5 +27,6 @@ sealed class HomeActivitySharedAction : VectorSharedAction { data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction() object AddSpace : HomeActivitySharedAction() data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() + data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction() data class ShowSpaceSettings(val spaceId: String) : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 7d303cfc5e..7a5a740b93 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail @@ -71,8 +72,10 @@ class MatrixToRoomSpaceFragment @Inject constructor( is RoomInfoResult.FullInfo -> { val matrixItem = peek.roomItem if (peek.roomType == RoomType.SPACE) { + views.matrixToBetaTag.isVisible = true avatarRenderer.renderSpace(matrixItem, views.matrixToCardAvatar) } else { + views.matrixToBetaTag.isVisible = false avatarRenderer.render(matrixItem, views.matrixToCardAvatar) } views.matrixToCardNameText.setTextOrHide(peek.name) @@ -97,19 +100,19 @@ class MatrixToRoomSpaceFragment @Inject constructor( Membership.LEAVE, Membership.NONE -> { views.matrixToCardMainButton.isVisible = true - views.matrixToCardMainButton.text = getString(joinTextRes) + views.matrixToCardMainButton.button.text = getString(joinTextRes) views.matrixToCardSecondaryButton.isVisible = false } Membership.INVITE -> { views.matrixToCardMainButton.isVisible = true views.matrixToCardSecondaryButton.isVisible = true - views.matrixToCardMainButton.text = getString(joinTextRes) - views.matrixToCardSecondaryButton.text = getString(R.string.decline) + views.matrixToCardMainButton.button.text = getString(joinTextRes) + views.matrixToCardSecondaryButton.button.text = getString(R.string.decline) } Membership.JOIN -> { views.matrixToCardMainButton.isVisible = true views.matrixToCardSecondaryButton.isVisible = false - views.matrixToCardMainButton.text = getString(R.string.action_open) + views.matrixToCardMainButton.button.text = getString(R.string.action_open) } Membership.KNOCK, Membership.BAN -> { @@ -126,7 +129,7 @@ class MatrixToRoomSpaceFragment @Inject constructor( views.matrixToMemberPills.isVisible = false views.matrixToCardDescText.setTextOrHide(getString(R.string.room_preview_no_preview)) - views.matrixToCardMainButton.text = getString(R.string.join_anyway) + views.matrixToCardMainButton.button.text = getString(R.string.join_anyway) views.matrixToCardSecondaryButton.isVisible = false } RoomInfoResult.NotFound -> { @@ -156,12 +159,10 @@ class MatrixToRoomSpaceFragment @Inject constructor( } } + val images = listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5) + .onEach { it.isGone = true } when (state.peopleYouKnow) { is Success -> { - views.matrixToCardPeopleYouKnowVisibility.isVisible = true - val images = listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5) - .onEach { it.isVisible = false } - val someYouKnow = state.peopleYouKnow.invoke() someYouKnow.forEachIndexed { index, item -> images[index].isVisible = true @@ -175,7 +176,7 @@ class MatrixToRoomSpaceFragment @Inject constructor( ) } else -> { - views.matrixToCardPeopleYouKnowVisibility.isVisible = false + views.peopleYouMayKnowText.isVisible = false } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index 672fb28928..f1627cc6b6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -60,6 +60,7 @@ class SpaceListFragment @Inject constructor( is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup(it.groupingMethodHasChanged)) is SpaceListViewEvents.AddSpace -> sharedActionViewModel.post(HomeActivitySharedAction.AddSpace) is SpaceListViewEvents.OpenGroup -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup(it.groupingMethodHasChanged)) + is SpaceListViewEvents.OpenSpaceInvite -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpaceInvite(it.id)) }.exhaustive } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewEvents.kt index b7e31d28f2..582f6cd144 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewEvents.kt @@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorViewEvents sealed class SpaceListViewEvents : VectorViewEvents { data class OpenSpace(val groupingMethodHasChanged: Boolean) : SpaceListViewEvents() data class OpenSpaceSummary(val id: String) : SpaceListViewEvents() + data class OpenSpaceInvite(val id: String) : SpaceListViewEvents() object AddSpace : SpaceListViewEvents() data class OpenGroup(val groupingMethodHasChanged: Boolean) : SpaceListViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt index 90ab771342..c79f6b12a4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -181,7 +181,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } private fun handleSelectSpaceInvite(action: SpaceListAction.OpenSpaceInvite) { - _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(action.spaceSummary.roomId)) + _viewEvents.post(SpaceListViewEvents.OpenSpaceInvite(action.spaceSummary.roomId)) } private fun handleToggleExpand(action: SpaceListAction.ToggleExpand) = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt new file mode 100644 index 0000000000..8e536459a3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt @@ -0,0 +1,230 @@ +/* + * 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 android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +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.ScreenComponent +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.parcelize.Parcelize +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceInviteBottomSheetViewModel.Factory { + + interface InteractionListener { + fun spaceInviteBottomSheetOnAccept(spaceId: String) + fun spaceInviteBottomSheetOnDecline(spaceId: String) + } + + var interactionListener: InteractionListener? = null + + @Parcelize + data class Args( + val spaceId: String + ) : Parcelable + + @Inject + lateinit var avatarRenderer: AvatarRenderer + + 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) + } + + override val showExpanded = true + + private val inviteArgs: Args by args() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + 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 = false + viewModel.handle(SpaceInviteBottomSheetAction.DoJoin) + } + + override fun onRetryClicked() = onButtonClicked() + } + views.spaceCard.matrixToCardSecondaryButton.callback = object : ButtonStateView.Callback { + override fun onButtonClicked() { + views.spaceCard.matrixToCardMainButton.button.isEnabled = false + views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Loading) + 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 + views.inviterMxid.isVisible = true + avatarRenderer.render(inviter, views.inviterAvatarImage) + views.inviterText.text = getString(R.string.user_invites_you, inviter.getBestName()) + views.inviterMxid.text = inviter.id + } else { + views.inviterAvatarImage.isVisible = false + views.inviterText.isVisible = false + views.inviterMxid.isVisible = false + } + + views.spaceCard.matrixToCardContentVisibility.isVisible = true + 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.matrixToCardMainButton.button.text = getString(R.string.accept) + views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.decline) + + when (state.joinActionState) { + Uninitialized -> { + views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Button) + } + is Loading -> { + views.spaceCard.matrixToCardMainButton.render(ButtonStateView.State.Loading) + views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = false + } + is Success -> { + interactionListener?.spaceInviteBottomSheetOnAccept(inviteArgs.spaceId) + dismiss() + } + 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) + } + is Loading -> { + views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Loading) + views.spaceCard.matrixToCardMainButton.button.isEnabled = false + } + is Success -> { + interactionListener?.spaceInviteBottomSheetOnDecline(inviteArgs.spaceId) + dismiss() + } + is Fail -> { + views.spaceCard.matrixToCardSecondaryButton.render(ButtonStateView.State.Error) + views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true + } + } + + 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) + } else { + // hide the pill + views.spaceCard.matrixToMemberPills.isVisible = false + } + + val peopleYouKnow = state.peopleYouKnow.invoke().orEmpty() + + val images = listOf( + views.spaceCard.knownMember1, + views.spaceCard.knownMember2, + views.spaceCard.knownMember3, + views.spaceCard.knownMember4, + views.spaceCard.knownMember5 + ).onEach { it.isGone = true } + + if (peopleYouKnow.isEmpty()) { + views.spaceCard.peopleYouMayKnowText.isVisible = false + } else { + peopleYouKnow.forEachIndexed { index, item -> + images[index].isVisible = true + avatarRenderer.render(item.toMatrixItem(), images[index]) + } + views.spaceCard.peopleYouMayKnowText.setTextOrHide( + resources.getQuantityString(R.plurals.space_people_you_know, + peopleYouKnow.count(), + peopleYouKnow.count() + ) + ) + } + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetInvitedToSpaceBinding { + return BottomSheetInvitedToSpaceBinding.inflate(inflater, container, false) + } + + companion object { + + fun newInstance(spaceId: String) + : SpaceInviteBottomSheet { + return SpaceInviteBottomSheet().apply { + setArguments(Args(spaceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetAction.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetAction.kt new file mode 100644 index 0000000000..b8e3283aff --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetEvents.kt new file mode 100644 index 0000000000..b097d19d9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetState.kt new file mode 100644 index 0000000000..d712cf9e8a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetState.kt @@ -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 = Uninitialized, + val inviterUser: Async = Uninitialized, + val peopleYouKnow: Async> = Uninitialized, + val joinActionState: Async = Uninitialized, + val rejectActionState: Async = Uninitialized +) : MvRxState { + constructor(args: SpaceInviteBottomSheet.Args) : this( + spaceId = args.spaceId + ) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt new file mode 100644 index 0000000000..4524b57004 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt @@ -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(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 { + + 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))) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt index eee8d1241f..8f2e7379c4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -37,16 +37,15 @@ class SpacePreviewController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildModels(data: SpacePreviewState?) { - val result = data?.childInfoList?.invoke() ?: return - - val memberCount = data.spaceInfo.invoke()?.memberCount ?: 0 + val memberCount = data?.spaceInfo?.invoke()?.memberCount ?: 0 spaceTopSummaryItem { id("info") formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) - topic(data.spaceInfo.invoke()?.topic ?: data.topic ?: "") + topic(data?.spaceInfo?.invoke()?.topic ?: data?.topic ?: "") } + val result = data?.childInfoList?.invoke() ?: return if (result.isNotEmpty()) { genericItemHeader { id("header_rooms") diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index 563b4f39e0..b6f1fb6a4e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -119,14 +119,17 @@ class SpacePreviewFragment @Inject constructor( } } updateToolbar(it) + + when (it.inviteTermination) { + is Loading -> sharedActionViewModel.post(SpacePreviewSharedAction.ShowModalLoading) + else -> sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) + } } private fun handleViewEvents(viewEvents: SpacePreviewViewEvents) { when (viewEvents) { SpacePreviewViewEvents.Dismiss -> { - } - SpacePreviewViewEvents.StartJoining -> { - sharedActionViewModel.post(SpacePreviewSharedAction.ShowModalLoading) + sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) } SpacePreviewViewEvents.JoinSuccess -> { sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt index cf64672046..d31d05cf96 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -26,7 +26,8 @@ data class SpacePreviewState( val topic: String? = null, val avatarUrl: String? = null, val spaceInfo: Async = Uninitialized, - val childInfoList: Async> = Uninitialized + val childInfoList: Async> = Uninitialized, + val inviteTermination: Async = Uninitialized ) : MvRxState { constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt index 04645e59ad..2f0eddb189 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt @@ -20,7 +20,6 @@ import im.vector.app.core.platform.VectorViewEvents sealed class SpacePreviewViewEvents : VectorViewEvents { object Dismiss: SpacePreviewViewEvents() - object StartJoining: SpacePreviewViewEvents() object JoinSuccess: SpacePreviewViewEvents() data class JoinFailure(val message: String?): SpacePreviewViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 7da6c8a053..61328d2a1c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -28,6 +28,7 @@ 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,6 +42,7 @@ import timber.log.Timber class SpacePreviewViewModel @AssistedInject constructor( @Assisted private val initialState: SpacePreviewState, + private val errorFormatter: ErrorFormatter, private val session: Session ) : VectorViewModel(initialState) { @@ -80,13 +82,15 @@ class SpacePreviewViewModel @AssistedInject constructor( private fun handleDismissInvite() { // Here we need to join the space himself as well as the default rooms in that space - // TODO modal loading + setState { copy(inviteTermination = Loading()) } viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().rejectInvite(initialState.idOrAlias, null) } catch (failure: Throwable) { Timber.e(failure, "## Space: Failed to reject invite") + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(errorFormatter.toHumanReadable(failure))) } + setState { copy(inviteTermination = Uninitialized) } } } @@ -98,18 +102,25 @@ class SpacePreviewViewModel @AssistedInject constructor( val spaceVia = spaceInfo?.viaServers ?: emptyList() // trigger modal loading - _viewEvents.post(SpacePreviewViewEvents.StartJoining) + setState { copy(inviteTermination = Loading()) } viewModelScope.launch(Dispatchers.IO) { - val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia) - when (joinResult) { - JoinSpaceResult.Success, - is JoinSpaceResult.PartialSuccess -> { - // For now we don't handle partial success, it's just success - _viewEvents.post(SpacePreviewViewEvents.JoinSuccess) - } - is JoinSpaceResult.Fail -> { - _viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error.toString())) + try { + val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia) + setState { copy(inviteTermination = Uninitialized) } + when (joinResult) { + JoinSpaceResult.Success, + is JoinSpaceResult.PartialSuccess -> { + // For now we don't handle partial success, it's just success + _viewEvents.post(SpacePreviewViewEvents.JoinSuccess) + } + is JoinSpaceResult.Fail -> { + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(errorFormatter.toHumanReadable(joinResult.error))) + } } + } catch (failure: Throwable) { + // should not throw + Timber.w(failure, "## Failed to join space") + _viewEvents.post(SpacePreviewViewEvents.JoinFailure(errorFormatter.toHumanReadable(failure))) } } } @@ -221,7 +232,21 @@ class SpacePreviewViewModel @AssistedInject constructor( } } catch (failure: Throwable) { setState { - copy(spaceInfo = Fail(failure), childInfoList = Fail(failure)) + copy( + spaceInfo = session.getRoomSummary(initialState.idOrAlias)?.let { + Success( + ChildInfo( + roomId = it.roomId, + avatarUrl = it.avatarUrl, + name = it.displayName, + topic = it.topic, + memberCount = it.joinedMembersCount, + isSubSpace = false, + viaServers = null, + children = Uninitialized + ) + ) + } ?: Fail(failure), childInfoList = Fail(failure)) } } } diff --git a/vector/src/main/res/layout/bottom_sheet_invited_to_space.xml b/vector/src/main/res/layout/bottom_sheet_invited_to_space.xml new file mode 100644 index 0000000000..6730a585db --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_invited_to_space.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml index c87a0d9e4b..85ec04ba50 100644 --- a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml +++ b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml @@ -1,224 +1,257 @@ - + android:layout_height="wrap_content"> - - - - - - - + tools:visibility="visible"> + android:id="@+id/matrixToCardAvatar" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_marginTop="20dp" + android:contentDescription="@string/avatar" + android:elevation="4dp" + android:transitionName="profile" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/avatars" /> - + + - + android:textSize="15sp" + android:textStyle="bold" + app:layout_constraintTop_toBottomOf="@+id/matrixToCardAvatar" + tools:text="@sample/matrix.json/data/roomName" /> - + - + - + - + + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + diff --git a/vector/src/main/res/layout/view_button_state.xml b/vector/src/main/res/layout/view_button_state.xml index 28a3f066e5..1e22d79b5e 100644 --- a/vector/src/main/res/layout/view_button_state.xml +++ b/vector/src/main/res/layout/view_button_state.xml @@ -1,14 +1,14 @@ Experimental Space - Restricted Room. Warning requires server support and experimental room version + %s invites you diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 89d5ac12e2..a265727522 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -152,7 +152,7 @@ wrap_content wrap_content @null - + ?colorAccent ?colorAccent @@ -162,7 +162,7 @@ wrap_content wrap_content @null - + ?colorAccent ?colorAccent @color/button_background_tint_selector 1dp