Unverified Commit 096e95f9 authored by Benoît Marty's avatar Benoît Marty Committed by GitHub
Browse files

Merge pull request #3279 from vector-im/feature/bca/space_beta_people

Spaces | beta Browsing Member Directory In Space
parents 89d71cee ea93261b
......@@ -295,6 +295,7 @@
<activity android:name=".features.spaces.SpaceExploreActivity" />
<activity android:name=".features.spaces.SpaceCreationActivity" />
<activity android:name=".features.spaces.manage.SpaceManageActivity" />
<activity android:name=".features.spaces.people.SpacePeopleActivity" />
<!-- Services -->
<service
......
......@@ -125,6 +125,7 @@ import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
import im.vector.app.features.spaces.manage.SpaceAddRoomFragment
import im.vector.app.features.spaces.people.SpacePeopleFragment
import im.vector.app.features.spaces.preview.SpacePreviewFragment
import im.vector.app.features.terms.ReviewTermsFragment
import im.vector.app.features.usercode.ShowUserCodeFragment
......@@ -678,4 +679,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SpaceAddRoomFragment::class)
fun bindSpaceAddRoomFragment(fragment: SpaceAddRoomFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpacePeopleFragment::class)
fun bindSpacePeopleFragment(fragment: SpacePeopleFragment): Fragment
}
......@@ -39,6 +39,7 @@ import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetShare
import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel
import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel
import im.vector.app.features.spaces.people.SpacePeopleSharedActionViewModel
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@Module
......@@ -148,4 +149,9 @@ interface ViewModelModule {
@IntoMap
@ViewModelKey(SpacePreviewSharedActionViewModel::class)
fun bindSpacePreviewSharedActionViewModel(viewModel: SpacePreviewSharedActionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(SpacePeopleSharedActionViewModel::class)
fun bindSpacePeopleSharedActionViewModel(viewModel: SpacePeopleSharedActionViewModel): ViewModel
}
......@@ -17,6 +17,7 @@
package im.vector.app.core.epoxy.profiles
import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.core.epoxy.VectorEpoxyModel
......@@ -34,6 +35,7 @@ abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder> : VectorEpoxy
var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
@CallSuper
override fun bind(holder: T) {
super.bind(holder)
val bestName = matrixItem.getBestName()
......
/*
* Copyright 2020 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.core.epoxy.profiles
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItemWithPowerLevel : BaseProfileMatrixItem<ProfileMatrixItemWithPowerLevel.Holder>() {
@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<TextView>(R.id.matrixItemPowerLevelLabel)
}
}
/*
* 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.core.platform
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Generic argument with one String. Can be an id (ex: roomId, spaceId, callId, etc.), or anything else
*/
@Parcelize
data class GenericIdArgs(
val id: String
) : Parcelable
......@@ -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,7 +284,19 @@ class DefaultNavigator @Inject constructor(
}
override fun openCreateDirectRoom(context: Context) {
val intent = CreateDirectRoomActivity.getIntent(context)
val intent = when (val currentGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) {
is RoomGroupingMethod.ByLegacyGroup -> {
CreateDirectRoomActivity.getIntent(context)
}
is RoomGroupingMethod.BySpace -> {
if (currentGroupingMethod.spaceSummary != null) {
SpacePeopleActivity.newIntent(context, currentGroupingMethod.spaceSummary.roomId)
} else {
CreateDirectRoomActivity.getIntent(context)
}
}
else -> null
} ?: return
context.startActivity(intent)
}
......
......@@ -47,11 +47,16 @@ class RoomMemberListFragment @Inject constructor(
private val roomMemberListController: RoomMemberListController,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentRoomMemberListBinding>(),
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)
}
......
......@@ -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
}
......
......@@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.platform.GenericIdArgs
import im.vector.app.features.roomprofile.RoomProfileArgs
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.events.model.Event
......@@ -38,6 +39,8 @@ data class RoomMemberListViewState(
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
constructor(args: GenericIdArgs) : this(roomId = args.id)
}
data class ActionPermissions(
......
......@@ -29,9 +29,11 @@ class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummar
// No filter
return true
}
return roomMemberSummary.displayName?.contains(filter, ignoreCase = true).orFalse()
// We should maybe exclude the domain from the userId
|| roomMemberSummary.userId.contains(filter, ignoreCase = true)
// if filter is "Jo Do", it should match "John Doe"
return filter.split(" ").all {
roomMemberSummary.displayName?.contains(it, ignoreCase = true).orFalse()
// We should maybe exclude the domain from the userId
|| roomMemberSummary.userId.contains(it, ignoreCase = true)
}
}
}
/*
* 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.GenericIdArgs
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleLoadingBinding
import im.vector.app.features.spaces.ShareSpaceBottomSheet
class SpacePeopleActivity : VectorBaseActivity<ActivitySimpleLoadingBinding>() {
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<GenericIdArgs>(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, GenericIdArgs(spaceId))
}
}
}
}
/*
* 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 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.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
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 drawableProvider: DrawableProvider,
private val colorProvider: ColorProvider,
private val epoxyController: SpacePeopleListController
) : VectorBaseFragment<FragmentRecyclerviewWithSearchBinding>(),
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(membersViewModel) { 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 = drawableProvider.getDrawable(
R.drawable.ic_close_24dp,
colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)
)
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)
}
}
/*
* 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<RoomMemberListViewState>() {
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
interface InteractionListener {
fun onSpaceMemberClicked(roomMemberSummary: RoomMemberSummary)
fun onInviteToSpaceSelected()
}