diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1e2bf1ab0f..ea4ea9fd12 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -295,6 +295,7 @@ + () { + + @EpoxyAttribute var powerLevelLabel: CharSequence? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.editableView.isVisible = false + holder.powerLabel.setTextOrHide(powerLevelLabel) + } + + class Holder : ProfileMatrixItem.Holder() { + val powerLabel by bind(R.id.matrixItemPowerLevelLabel) + } +} diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 735a29afa4..c38dccd63b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -75,6 +75,7 @@ import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.spaces.manage.SpaceManageActivity +import im.vector.app.features.spaces.people.SpacePeopleActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgsBuilder @@ -283,8 +284,21 @@ class DefaultNavigator @Inject constructor( } override fun openCreateDirectRoom(context: Context) { - val intent = CreateDirectRoomActivity.getIntent(context) - context.startActivity(intent) + when (val currentGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) { + is RoomGroupingMethod.ByLegacyGroup -> { + val intent = CreateDirectRoomActivity.getIntent(context) + context.startActivity(intent) + } + is RoomGroupingMethod.BySpace -> { + if (currentGroupingMethod.spaceSummary != null) { + val intent = SpacePeopleActivity.newIntent(context, currentGroupingMethod.spaceSummary.roomId) + context.startActivity(intent) + } else { + val intent = CreateDirectRoomActivity.getIntent(context) + context.startActivity(intent) + } + } + } } override fun openInviteUsersToRoom(context: Context, roomId: String) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 2ff89d6e54..591e3b84e0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -47,11 +47,16 @@ class RoomMemberListFragment @Inject constructor( private val roomMemberListController: RoomMemberListController, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), - RoomMemberListController.Callback { + RoomMemberListController.Callback, + RoomMemberListViewModel.Factory { private val viewModel: RoomMemberListViewModel by fragmentViewModel() private val roomProfileArgs: RoomProfileArgs by args() + override fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel { + return viewModelFactory.create(initialState) + } + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomMemberListBinding { return FragmentRoomMemberListBinding.inflate(inflater, container, false) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 270b3cb408..5c6ff48403 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -17,12 +17,13 @@ package im.vector.app.features.roomprofile.members import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -62,8 +63,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomMemberListViewState): RoomMemberListViewModel? { - val fragment: RoomMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) + 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") } } @@ -188,7 +192,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState override fun handle(action: RoomMemberListAction) { when (action) { is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action) - is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action) + is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action) }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt index e2cc3f7b99..a9e55f91c3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -29,9 +29,12 @@ class RoomMemberSummaryFilter @Inject constructor() : Predicate + acc + && (roomMemberSummary.displayName?.contains(s, ignoreCase = true).orFalse() + // We should maybe exclude the domain from the userId + || roomMemberSummary.userId.contains(s, ignoreCase = true)) + } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt new file mode 100644 index 0000000000..1cc2203042 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt @@ -0,0 +1,104 @@ +/* + * 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.people + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.airbnb.mvrx.MvRx +import im.vector.app.R +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleLoadingBinding +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.spaces.ShareSpaceBottomSheet + +class SpacePeopleActivity : VectorBaseActivity() { + + override fun getBinding() = ActivitySimpleLoadingBinding.inflate(layoutInflater) + + private lateinit var sharedActionViewModel: SpacePeopleSharedActionViewModel + + override fun initUiAndData() { + super.initUiAndData() + waitingView = views.waitingView.waitingView + } + + override fun showWaitingView(text: String?) { + hideKeyboard() + views.waitingView.waitingStatusText.isGone = views.waitingView.waitingStatusText.text.isNullOrBlank() + super.showWaitingView(text) + } + + override fun hideWaitingView() { + views.waitingView.waitingStatusText.text = null + views.waitingView.waitingStatusText.isGone = true + views.waitingView.waitingHorizontalProgress.progress = 0 + views.waitingView.waitingHorizontalProgress.isVisible = false + super.hideWaitingView() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (isFirstCreation()) { + val simpleName = SpacePeopleFragment::class.java.simpleName + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpacePeopleFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + + sharedActionViewModel = viewModelProvider.get(SpacePeopleSharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + SpacePeopleSharedAction.Dismiss -> finish() + is SpacePeopleSharedAction.NavigateToRoom -> navigateToRooms(sharedAction) + SpacePeopleSharedAction.HideModalLoading -> hideWaitingView() + SpacePeopleSharedAction.ShowModalLoading -> { + showWaitingView() + } + is SpacePeopleSharedAction.NavigateToInvite -> { + ShareSpaceBottomSheet.show(supportFragmentManager, sharedAction.spaceId) + } + } + }.disposeOnDestroy() + } + + private fun navigateToRooms(action: SpacePeopleSharedAction.NavigateToRoom) { + navigator.openRoom(this, action.roomId) + finish() + } + + companion object { + fun newIntent(context: Context, spaceId: String): Intent { + return Intent(context, SpacePeopleActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, RoomProfileArgs(spaceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt new file mode 100644 index 0000000000..7f5369a6ec --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -0,0 +1,152 @@ +/* + * 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.people + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.appcompat.queryTextChanges +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.OnBackPressed +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding +import im.vector.app.features.roomprofile.members.RoomMemberListAction +import im.vector.app.features.roomprofile.members.RoomMemberListViewModel +import im.vector.app.features.roomprofile.members.RoomMemberListViewState +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class SpacePeopleFragment @Inject constructor( + private val viewModelFactory: SpacePeopleViewModel.Factory, + private val roomMemberModelFactory: RoomMemberListViewModel.Factory, + private val epoxyController: SpacePeopleListController +) : VectorBaseFragment(), + SpacePeopleViewModel.Factory, + RoomMemberListViewModel.Factory, + OnBackPressed, SpacePeopleListController.InteractionListener { + + private val viewModel by fragmentViewModel(SpacePeopleViewModel::class) + private val membersViewModel by fragmentViewModel(RoomMemberListViewModel::class) + private lateinit var sharedActionViewModel: SpacePeopleSharedActionViewModel + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentRecyclerviewWithSearchBinding.inflate(inflater, container, false) + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + sharedActionViewModel.post(SpacePeopleSharedAction.Dismiss) + return true + } + + override fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel { + return viewModelFactory.create(initialState) + } + + override fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel { + return roomMemberModelFactory.create(initialState) + } + + override fun invalidate() = withState(viewModel, membersViewModel) { baseState, memberListState -> + views.appBarTitle.text = getString(R.string.bottom_action_people) + val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 + views.appBarSpaceInfo.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) +// views.listBuildingProgress.isVisible = true + epoxyController.setData(memberListState) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(SpacePeopleSharedActionViewModel::class.java) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + setupSearchView() + + views.addRoomToSpaceToolbar.navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close_24dp) + views.addRoomToSpaceToolbar.setNavigationOnClickListener { + sharedActionViewModel.post(SpacePeopleSharedAction.Dismiss) + } + + viewModel.observeViewEvents { + handleViewEvents(it) + } + + viewModel.subscribe(this) { + when (it.createAndInviteState) { + is Loading -> sharedActionViewModel.post(SpacePeopleSharedAction.ShowModalLoading) + Uninitialized, + is Fail -> sharedActionViewModel.post(SpacePeopleSharedAction.HideModalLoading) + is Success -> { + // don't hide on success, it will navigate out. If not the loading goes out before navigation + } + } + } + } + + override fun onDestroyView() { + epoxyController.listener = null + views.roomList.cleanup() + super.onDestroyView() + } + + private fun setupRecyclerView() { + views.roomList.configureWith(epoxyController, hasFixedSize = false, disableItemAnimation = false) + epoxyController.listener = this + } + + private fun setupSearchView() { + views.memberNameFilter.queryHint = getString(R.string.search_members_hint) + views.memberNameFilter.queryTextChanges() + .debounce(100, TimeUnit.MILLISECONDS) + .subscribeBy { + membersViewModel.handle(RoomMemberListAction.FilterMemberList(it.toString())) + } + .disposeOnDestroyView() + } + + private fun handleViewEvents(events: SpacePeopleViewEvents) { + when (events) { + is SpacePeopleViewEvents.OpenRoom -> { + sharedActionViewModel.post(SpacePeopleSharedAction.NavigateToRoom(events.roomId)) + } + is SpacePeopleViewEvents.InviteToSpace -> { + sharedActionViewModel.post(SpacePeopleSharedAction.NavigateToInvite(events.spaceId)) + } + } + } + + override fun onSpaceMemberClicked(roomMemberSummary: RoomMemberSummary) { + viewModel.handle(SpacePeopleViewAction.ChatWith(roomMemberSummary)) + } + + override fun onInviteToSpaceSelected() { + viewModel.handle(SpacePeopleViewAction.InviteToSpace) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt new file mode 100644 index 0000000000..fc982862a7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -0,0 +1,167 @@ +/* + * 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.people + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.epoxy.dividerItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.epoxy.profiles.profileMatrixItemWithPowerLevel +import im.vector.app.core.extensions.join +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.GenericItem +import im.vector.app.core.ui.list.genericItem +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.members.RoomMemberListCategories +import im.vector.app.features.roomprofile.members.RoomMemberListViewState +import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class SpacePeopleListController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val colorProvider: ColorProvider, + private val stringProvider: StringProvider, + private val dimensionConverter: DimensionConverter, + private val roomMemberSummaryFilter: RoomMemberSummaryFilter +) : TypedEpoxyController() { + + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) + + interface InteractionListener { + fun onSpaceMemberClicked(roomMemberSummary: RoomMemberSummary) + fun onInviteToSpaceSelected() + } + + var listener: InteractionListener? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomMemberListViewState?) { + val memberSummaries = data?.roomMemberSummaries?.invoke() + if (memberSummaries == null) { + loadingItem { id("loading") } + return + } + roomMemberSummaryFilter.filter = data.filter + var foundCount = 0 + memberSummaries.forEach { memberEntry -> + + val filtered = memberEntry.second + .filter { roomMemberSummaryFilter.test(it) } + if (filtered.isNotEmpty()) { + dividerItem { + id("divider_type_${memberEntry.first.titleRes}") + color(dividerColor) + } + } + foundCount += filtered.size + filtered + .join( + each = { _, roomMember -> + profileMatrixItemWithPowerLevel { + id(roomMember.userId) + matrixItem(roomMember.toMatrixItem()) + avatarRenderer(avatarRenderer) + userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + .apply { + val pl = memberEntry.first.toPowerLevelLabel() + if (memberEntry.first == RoomMemberListCategories.INVITE) { + powerLevelLabel( + span { + span(stringProvider.getString(R.string.invited)) { + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textStyle = "bold" + // fontFamily = "monospace" + } + } + ) + } else if (pl != null) { + powerLevelLabel( + span { + span(" $pl ") { + backgroundColor = colorProvider.getColor(R.color.notification_accent_color) + paddingTop = dimensionConverter.dpToPx(2) + paddingBottom = dimensionConverter.dpToPx(2) + textColor = colorProvider.getColor(R.color.white) + textStyle = "bold" + // fontFamily = "monospace" + } + } + ) + } else { + powerLevelLabel(null) + } + } + + clickListener { _ -> + listener?.onSpaceMemberClicked(roomMember) + } + } + }, + between = { _, roomMemberBefore -> + dividerItem { + id("divider_${roomMemberBefore.userId}") + color(dividerColor) + } + } + ) + } + + if (foundCount == 0 && data.filter.isNotEmpty()) { + // add the footer thing + genericItem { + id("not_found") + title( + span { + +"\n" + +stringProvider.getString(R.string.no_result_placeholder) + } + ) + description( + span { + +stringProvider.getString(R.string.looking_for_someone_not_in_space, data.roomSummary.invoke()?.displayName ?: "") + +"\n" + span("Invite them") { + textColor = colorProvider.getColorFromAttribute(R.attr.colorAccent) + textStyle = "bold" + } + } + ) + itemClickAction(GenericItem.Action("invite").apply { + perform = Runnable { + listener?.onInviteToSpaceSelected() + } + }) + } + } + } + + private fun RoomMemberListCategories.toPowerLevelLabel(): String? { + return when (this) { + RoomMemberListCategories.ADMIN -> stringProvider.getString(R.string.power_level_admin) + RoomMemberListCategories.MODERATOR -> stringProvider.getString(R.string.power_level_moderator) + else -> null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleSharedActionViewModel.kt new file mode 100644 index 0000000000..649f241bf9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleSharedActionViewModel.kt @@ -0,0 +1,31 @@ +/* + * 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.people + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +sealed class SpacePeopleSharedAction : VectorSharedAction { + object Dismiss : SpacePeopleSharedAction() + object ShowModalLoading : SpacePeopleSharedAction() + object HideModalLoading : SpacePeopleSharedAction() + data class NavigateToRoom(val roomId: String) : SpacePeopleSharedAction() + data class NavigateToInvite(val spaceId: String) : SpacePeopleSharedAction() +} + +class SpacePeopleSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt new file mode 100644 index 0000000000..71c3bcdda7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt @@ -0,0 +1,130 @@ +/* + * 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.people + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +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.extensions.exhaustive +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.roomprofile.RoomProfileArgs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +data class SpacePeopleViewState( + val spaceId: String, + val createAndInviteState: Async = Uninitialized +) : MvRxState { + constructor(args: RoomProfileArgs) : this( + spaceId = args.roomId + ) +} + +sealed class SpacePeopleViewAction : VectorViewModelAction { + data class ChatWith(val member: RoomMemberSummary) : SpacePeopleViewAction() + object InviteToSpace : SpacePeopleViewAction() +} + +sealed class SpacePeopleViewEvents : VectorViewEvents { + data class OpenRoom(val roomId: String) : SpacePeopleViewEvents() + data class InviteToSpace(val spaceId: String) : SpacePeopleViewEvents() +} + +class SpacePeopleViewModel @AssistedInject constructor( + @Assisted val initialState: SpacePeopleViewState, + private val rawService: RawService, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: SpacePeopleViewState): SpacePeopleViewModel? { + 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: SpacePeopleViewAction) { + when (action) { + is SpacePeopleViewAction.ChatWith -> handleChatWith(action) + SpacePeopleViewAction.InviteToSpace -> handleInviteToSpace() + }.exhaustive + } + + private fun handleInviteToSpace() { + _viewEvents.post(SpacePeopleViewEvents.InviteToSpace(initialState.spaceId)) + } + + private fun handleChatWith(action: SpacePeopleViewAction.ChatWith) { + val otherUserId = action.member.userId + if (otherUserId == session.myUserId) return + val existingRoomId = session.getExistingDirectRoomWithUser(otherUserId) + if (existingRoomId != null) { + // just open it + _viewEvents.post(SpacePeopleViewEvents.OpenRoom(existingRoomId)) + return + } + setState { copy(createAndInviteState = Loading()) } + + viewModelScope.launch(Dispatchers.IO) { + val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) + ?.isE2EByDefault() + ?: true + + val roomParams = CreateRoomParams() + .apply { + invitedUserIds.add(otherUserId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault + } + + try { + val roomId = session.createRoom(roomParams) + _viewEvents.post(SpacePeopleViewEvents.OpenRoom(roomId)) + setState { copy(createAndInviteState = Success(roomId)) } + } catch (failure: Throwable) { + setState { copy(createAndInviteState = Fail(failure)) } + } + } + } +} diff --git a/vector/src/main/res/layout/activity_simple_loading.xml b/vector/src/main/res/layout/activity_simple_loading.xml new file mode 100644 index 0000000000..b523d97c92 --- /dev/null +++ b/vector/src/main/res/layout/activity_simple_loading.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_recyclerview_with_search.xml b/vector/src/main/res/layout/fragment_recyclerview_with_search.xml new file mode 100644 index 0000000000..a9c26cff6f --- /dev/null +++ b/vector/src/main/res/layout/fragment_recyclerview_with_search.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_profile_matrix_item.xml b/vector/src/main/res/layout/item_profile_matrix_item.xml index d2ea7c01f5..29f537c315 100644 --- a/vector/src/main/res/layout/item_profile_matrix_item.xml +++ b/vector/src/main/res/layout/item_profile_matrix_item.xml @@ -40,7 +40,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" + android:layout_marginEnd="8dp" android:drawablePadding="16dp" android:ellipsize="end" android:maxLines="1" @@ -48,7 +48,7 @@ android:textSize="16sp" app:layout_constrainedWidth="true" app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle" - app:layout_constraintEnd_toStartOf="@+id/matrixItemEditable" + app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@id/matrixItemAvatar" app:layout_constraintTop_toTopOf="parent" @@ -60,7 +60,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" + android:layout_marginEnd="8dp" android:drawablePadding="16dp" android:ellipsize="end" android:maxLines="1" @@ -68,13 +68,27 @@ android:textSize="12sp" app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/matrixItemEditable" + app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@id/matrixItemAvatar" app:layout_constraintTop_toBottomOf="@id/matrixItemTitle" app:layout_goneMarginStart="0dp" tools:text="@sample/matrix.json/data/mxid" /> + + + Experimental Space - Restricted Room. Warning requires server support and experimental room version %s invites you + + Looking for someone not in %s?