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

Spaces | beta Browsing Member Directory In Space
This commit is contained in:
Benoit Marty 2021-05-06 10:38:40 +02:00 committed by GitHub
commit 096e95f9da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 926 additions and 36 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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()

View file

@ -0,0 +1,41 @@
/*
* 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)
}
}

View file

@ -0,0 +1,28 @@
/*
* 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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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")
}
}

View file

@ -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(

View file

@ -29,9 +29,11 @@ class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummar
// No filter
return true
}
return roomMemberSummary.displayName?.contains(filter, ignoreCase = true).orFalse()
// 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(filter, ignoreCase = true)
|| roomMemberSummary.userId.contains(it, ignoreCase = true)
}
}
}

View file

@ -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.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))
}
}
}
}

View file

@ -0,0 +1,158 @@
/*
* 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)
}
}

View file

@ -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<RoomMemberListViewState>() {
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
}
}
}

View file

@ -0,0 +1,27 @@
/*
* 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
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()
}

View file

@ -0,0 +1,22 @@
/*
* 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.VectorSharedActionViewModel
import javax.inject.Inject
class SpacePeopleSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<SpacePeopleSharedAction>()

View file

@ -0,0 +1,25 @@
/*
* 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.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
sealed class SpacePeopleViewAction : VectorViewModelAction {
data class ChatWith(val member: RoomMemberSummary) : SpacePeopleViewAction()
object InviteToSpace : SpacePeopleViewAction()
}

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.people
import im.vector.app.core.platform.VectorViewEvents
sealed class SpacePeopleViewEvents : VectorViewEvents {
data class OpenRoom(val roomId: String) : SpacePeopleViewEvents()
data class InviteToSpace(val spaceId: String) : SpacePeopleViewEvents()
}

View file

@ -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 androidx.lifecycle.viewModelScope
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.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.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
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.create.CreateRoomParams
class SpacePeopleViewModel @AssistedInject constructor(
@Assisted val initialState: SpacePeopleViewState,
private val rawService: RawService,
private val session: Session
) : VectorViewModel<SpacePeopleViewState, SpacePeopleViewAction, SpacePeopleViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel
}
companion object : MvRxViewModelFactory<SpacePeopleViewModel, SpacePeopleViewState> {
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)) }
}
}
}
}

View file

@ -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 com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.platform.GenericIdArgs
data class SpacePeopleViewState(
val spaceId: String,
val createAndInviteState: Async<String> = Uninitialized
) : MvRxState {
constructor(args: GenericIdArgs) : this(
spaceId = args.id
)
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/simpleFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:id="@+id/waiting_view"
layout="@layout/merge_overlay_waiting_view" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background"
android:overScrollMode="always"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_profile_matrix_item" />
<com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp">
<!-- minHeight="0dp" is important to collapse on scroll -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/addRoomToSpaceToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:minHeight="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:orientation="vertical">
<TextView
android:id="@+id/appBarTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/appBarSpaceInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="16sp"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
<androidx.appcompat.widget.SearchView
android:id="@+id/memberNameFilter"
style="@style/VectorSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addRoomToSpaceToolbar"
app:queryHint="@string/search_hint_room_name" />
<!-- <ProgressBar-->
<!-- android:id="@+id/listBuildingProgress"-->
<!-- android:indeterminate="true"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="8dp"-->
<!-- style="@style/Widget.AppCompat.ProgressBar.Horizontal"-->
<!-- />-->
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -37,44 +37,53 @@
<TextView
android:id="@+id/matrixItemTitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle"
app:layout_constraintEnd_toStartOf="@+id/matrixItemEditable"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp"
app:layout_goneMarginEnd="80dp"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/matrixItemSubtitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/matrixItemEditable"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toBottomOf="@id/matrixItemTitle"
app:layout_goneMarginStart="0dp"
app:layout_goneMarginEnd="8dp"
tools:text="@sample/matrix.json/data/mxid" />
<TextView
android:id="@+id/matrixItemPowerLevelLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/matrixItemEditable"
app:layout_constraintTop_toTopOf="parent"
tools:text="Admin"
tools:visibility="visible" />
<ImageView
android:id="@+id/matrixItemEditable"
android:layout_width="wrap_content"
@ -87,5 +96,4 @@
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -37,11 +37,10 @@
<TextView
android:id="@+id/matrixItemTitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
@ -49,19 +48,17 @@
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle"
app:layout_constraintEnd_toStartOf="@+id/matrixItemProgress"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp"
app:layout_goneMarginEnd="80dp"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/matrixItemSubtitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
@ -69,10 +66,9 @@
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/matrixItemProgress"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toBottomOf="@id/matrixItemTitle"
app:layout_goneMarginStart="0dp"
app:layout_goneMarginEnd="8dp"
tools:text="@sample/matrix.json/data/mxid" />
<ProgressBar
@ -93,12 +89,10 @@
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_arrow_right"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix"
tools:visibility="visible" />
tools:ignore="MissingPrefix" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3353,4 +3353,6 @@
<string name="labs_use_restricted_join_rule">Experimental Space - Restricted Room.</string>
<string name="labs_use_restricted_join_rule_desc">Warning requires server support and experimental room version</string>
<string name="user_invites_you">%s invites you</string>
<string name="looking_for_someone_not_in_space">Looking for someone not in %s?</string>
</resources>