From ae1a24e948a73c19866cdf05eabd4f64b0f469f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 13 Jan 2020 16:49:14 +0100 Subject: [PATCH] Room member profile: branch the UI and fix some UI issues --- .../room/members/RoomMemberQueryParams.kt | 7 +- .../membership/DefaultMembershipService.kt | 1 + .../MatrixItemAppBarStateChangeListener.kt | 36 ++++++ .../epoxy/profiles/ProfileItemExtensions.kt | 2 + .../riotx/core/platform/VectorBaseActivity.kt | 39 +++++-- .../riotx/core/platform/VectorBaseFragment.kt | 2 + .../features/navigation/DefaultNavigator.kt | 2 +- .../RoomMemberProfileAction.kt | 11 +- .../RoomMemberProfileController.kt | 107 ++++++++++++++++++ .../RoomMemberProfileFragment.kt | 93 +++++++++++++-- .../RoomMemberProfileViewModel.kt | 76 ++++++++++++- .../RoomMemberProfileViewState.kt | 24 +++- .../roomprofile/RoomProfileActivity.kt | 26 ++++- .../roomprofile/RoomProfileController.kt | 1 + .../roomprofile/RoomProfileFragment.kt | 49 ++++---- .../members/RoomMemberListController.kt | 5 + .../members/RoomMemberListFragment.kt | 7 +- ...rofile.xml => fragment_matrix_profile.xml} | 79 +++---------- .../res/layout/fragment_room_member_list.xml | 1 + .../layout/fragment_room_member_profile.xml | 36 ------ .../view_stub_room_member_profile_header.xml | 69 +++++++++++ .../layout/view_stub_room_profile_header.xml | 57 ++++++++++ vector/src/main/res/values/strings_riotX.xml | 7 ++ 23 files changed, 586 insertions(+), 151 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt rename vector/src/main/res/layout/{fragment_room_profile.xml => fragment_matrix_profile.xml} (54%) delete mode 100644 vector/src/main/res/layout/fragment_room_member_profile.xml create mode 100644 vector/src/main/res/layout/view_stub_room_member_profile_header.xml create mode 100644 vector/src/main/res/layout/view_stub_room_profile_header.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt index 19003632ca..7a8518c5de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -28,17 +28,20 @@ fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}) */ data class RoomMemberQueryParams( val displayName: QueryStringValue, - val memberships: List + val memberships: List, + val userId: QueryStringValue ) { class Builder { + var userId: QueryStringValue = QueryStringValue.NoCondition var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var memberships: List = Membership.all() fun build() = RoomMemberQueryParams( displayName = displayName, - memberships = memberships + memberships = memberships, + userId = userId ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 53e0ee729e..01646e4c17 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -93,6 +93,7 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { return RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) } diff --git a/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt b/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt new file mode 100644 index 0000000000..6e34983018 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt @@ -0,0 +1,36 @@ +/* + * 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.riotx.core.animations + +import android.view.View +import com.google.android.material.appbar.AppBarLayout + +class MatrixItemAppBarStateChangeListener(private val animationDuration: Long, private val views: List) : AppBarStateChangeListener() { + + override fun onStateChanged(appBarLayout: AppBarLayout, state: State) { + if (state == State.COLLAPSED) { + views.forEach { + it.animate().alpha(1f).duration = animationDuration + 100 + } + } else { + views.forEach { + it.animate().alpha(0f).duration = animationDuration - 100 + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt index 4314a5d97a..7b9e6973c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt @@ -32,6 +32,7 @@ fun EpoxyController.buildProfileAction( id: String, title: String, subtitle: String? = null, + editable: Boolean = true, @DrawableRes icon: Int = 0, destructive: Boolean = false, divider: Boolean = true, @@ -42,6 +43,7 @@ fun EpoxyController.buildProfileAction( iconRes(icon) id("action_$id") subtitle(subtitle) + editable(editable) destructive(destructive) title(title) listener { _ -> diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 40ce65e3ef..5c73fc97da 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -23,11 +23,18 @@ import android.os.Parcelable import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.annotation.* +import androidx.annotation.AttrRes +import androidx.annotation.LayoutRes +import androidx.annotation.MainThread +import androidx.annotation.MenuRes +import androidx.annotation.Nullable +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -41,7 +48,12 @@ import com.google.android.material.snackbar.Snackbar import im.vector.matrix.android.api.failure.GlobalError import im.vector.riotx.BuildConfig import im.vector.riotx.R -import im.vector.riotx.core.di.* +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.DaggerScreenComponent +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.di.HasVectorInjector +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.utils.toast @@ -92,6 +104,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { lateinit var rageShake: RageShake private set protected lateinit var navigator: Navigator + private lateinit var fragmentFactory: FragmentFactory private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences @@ -145,7 +158,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") ThemeUtils.setActivityTheme(this, getOtherThemes()) - supportFragmentManager.fragmentFactory = screenComponent.fragmentFactory() + fragmentFactory = screenComponent.fragmentFactory() + supportFragmentManager.fragmentFactory = fragmentFactory super.onCreate(savedInstanceState) viewModelFactory = screenComponent.viewModelFactory() configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) @@ -196,7 +210,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { handleInvalidToken(globalError) is GlobalError.ConsentNotGivenError -> consentNotGivenHelper.displayDialog(globalError.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host + ?: "") } } @@ -209,11 +224,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { mainActivityStarted = true MainActivity.restartApp(this, - MainActivityArgs( - clearCredentials = !globalError.softLogout, - isUserLoggedOut = true, - isSoftLogout = globalError.softLogout - ) + MainActivityArgs( + clearCredentials = !globalError.softLogout, + isUserLoggedOut = true, + isSoftLogout = globalError.softLogout + ) ) } @@ -276,6 +291,12 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected open fun injectWith(injector: ScreenComponent) = Unit + protected fun createFragment(fragmentClass: Class, args: Bundle?): Fragment { + return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply { + arguments = args + } + } + /* ========================================================================================== * PRIVATE METHODS * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index 3bb667593c..91ecf188dd 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -96,6 +96,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { } final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Timber.i("onCreateView Fragment ${this.javaClass.simpleName}") return inflater.inflate(getLayoutResId(), container, false) } @@ -117,6 +118,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { @CallSuper override fun onDestroyView() { super.onDestroyView() + Timber.i("onDestroyView Fragment ${this.javaClass.simpleName}") mUnBinder?.unbind() mUnBinder = null uiDisposables.clear() diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index df9a142a9f..018f8a0702 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -83,7 +83,7 @@ class DefaultNavigator @Inject constructor( } override fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean) { - val args = RoomMemberProfileArgs(userId = userId) + val args = RoomMemberProfileArgs(userId = userId, roomId = roomId) context.startActivity(RoomMemberProfileActivity.newIntent(context, args)) } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt index d61dbffcba..f4e27b5bc0 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt @@ -19,4 +19,13 @@ package im.vector.riotx.features.roommemberprofile import im.vector.riotx.core.platform.VectorViewModelAction -sealed class RoomMemberProfileAction : VectorViewModelAction +sealed class RoomMemberProfileAction : VectorViewModelAction { + + sealed class Displayable : RoomMemberProfileAction() { + object JumpToReadReceipt : Displayable() + object Ignore : Displayable() + object Mention : Displayable() + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt new file mode 100644 index 0000000000..58d2746dcb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt @@ -0,0 +1,107 @@ +/* + * 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.riotx.features.roommemberprofile + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.profiles.buildProfileAction +import im.vector.riotx.core.epoxy.profiles.buildProfileSection +import im.vector.riotx.core.resources.StringProvider +import javax.inject.Inject + +class RoomMemberProfileController @Inject constructor(private val stringProvider: StringProvider) + : TypedEpoxyController() { + + var callback: Callback? = null + + interface Callback { + fun onIgnoreClicked() + fun onLearnMoreClicked() + fun onJumpToReadReceiptClicked() + fun onMentionClicked() + } + + override fun buildModels(data: RoomMemberProfileViewState?) { + if (data == null) { + return + } + if (data.roomId == null) { + buildUserActions() + } else { + buildRoomMemberActions(data) + } + } + + private fun buildUserActions() { + // More + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "ignore", + title = stringProvider.getString(R.string.ignore), + destructive = true, + editable = false, + action = { callback?.onIgnoreClicked() } + ) + } + + private fun buildRoomMemberActions(data: RoomMemberProfileViewState) { + val roomSummaryEntity = data.roomSummary() ?: return + + // Security + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + val learnMoreSubtitle = if (roomSummaryEntity.isEncrypted) { + R.string.room_profile_encrypted_subtitle + } else { + R.string.room_profile_not_encrypted_subtitle + } + buildProfileAction( + id = "learn_more", + title = stringProvider.getString(R.string.room_profile_section_security_learn_more), + editable = false, + subtitle = stringProvider.getString(learnMoreSubtitle), + action = { callback?.onLearnMoreClicked() } + ) + + // More + if (!data.isMine) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "read_receipt", + editable = false, + title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), + action = { callback?.onJumpToReadReceiptClicked() } + ) + buildProfileAction( + id = "mention", + title = stringProvider.getString(R.string.room_participants_action_mention), + editable = false, + action = { callback?.onMentionClicked() } + ) + buildProfileAction( + id = "ignore", + title = stringProvider.getString(R.string.ignore), + destructive = true, + editable = false, + action = { callback?.onIgnoreClicked() } + ) + } + + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt index cc5c228058..eecfe65d88 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -19,14 +19,25 @@ package im.vector.riotx.features.roommemberprofile import android.os.Bundle import android.os.Parcelable -import kotlinx.android.parcel.Parcelize -import com.airbnb.mvrx.args import android.view.View +import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.room.powerlevers.PowerLevelsConstants +import im.vector.matrix.android.api.session.room.powerlevers.PowerLevelsHelper +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R +import im.vector.riotx.core.animations.AppBarStateChangeListener +import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.platform.VectorBaseFragment -import timber.log.Timber +import im.vector.riotx.features.home.AvatarRenderer +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.fragment_matrix_profile.matrixProfileHeaderView +import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.* import javax.inject.Inject @Parcelize @@ -36,21 +47,87 @@ data class RoomMemberProfileArgs( ) : Parcelable class RoomMemberProfileFragment @Inject constructor( - val viewModelFactory: RoomMemberProfileViewModel.Factory -) : VectorBaseFragment() { + val viewModelFactory: RoomMemberProfileViewModel.Factory, + private val roomMemberProfileController: RoomMemberProfileController, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), RoomMemberProfileController.Callback { private val fragmentArgs: RoomMemberProfileArgs by args() private val viewModel: RoomMemberProfileViewModel by fragmentViewModel() - override fun getLayoutResId() = R.layout.fragment_room_member_profile + private lateinit var appBarStateChangeListener: AppBarStateChangeListener + + override fun getLayoutResId() = R.layout.fragment_matrix_profile override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Initialize your view, subscribe to viewModel... + setupToolbar(matrixProfileToolbar) + matrixProfileHeaderView.apply { + layoutResource = R.layout.view_stub_room_member_profile_header + inflate() + } + matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true) + roomMemberProfileController.callback = this + appBarStateChangeListener = MatrixItemAppBarStateChangeListener(matrixProfileCollapsingToolbarLayout.scrimAnimationDuration, listOf(matrixProfileToolbarAvatarImageView, matrixProfileToolbarTitleView)) + matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) } + override fun onDestroyView() { + matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) + roomMemberProfileController.callback = null + matrixProfileRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> - Timber.v("Invalidate: $state") + val memberMatrixItem = state.memberAsMatrixItem() ?: return@withState + memberProfileIdView.text = memberMatrixItem.id + val bestName = memberMatrixItem.getBestName() + memberProfileNameView.text = bestName + matrixProfileToolbarTitleView.text = bestName + avatarRenderer.render(memberMatrixItem, memberProfileAvatarView) + avatarRenderer.render(memberMatrixItem, matrixProfileToolbarAvatarImageView) + + val roomSummary = state.roomSummary() + val powerLevelsContent = state.powerLevelsContent() + if (powerLevelsContent == null || roomSummary == null) { + memberProfilePowerLevelView.visibility = View.GONE + } else { + val roomName = roomSummary.toMatrixItem().getBestName() + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + val userPowerLevel = powerLevelsHelper.getUserPowerLevel(state.userId) + val powerLevelText = if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL) { + getString(R.string.room_member_power_level_admin_in, roomName) + } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL) { + getString(R.string.room_member_power_level_moderator_in, roomName) + } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL) { + null + } else { + getString(R.string.room_member_power_level_custom_in, userPowerLevel, roomName) + } + memberProfilePowerLevelView.setTextOrHide(powerLevelText) + } + roomMemberProfileController.setData(state) } + // RoomMemberProfileController.Callback + + override fun onIgnoreClicked() { + vectorBaseActivity.notImplemented("Ignore") + } + + override fun onLearnMoreClicked() { + vectorBaseActivity.notImplemented("Learn more") + } + + override fun onJumpToReadReceiptClicked() { + vectorBaseActivity.notImplemented("Jump to read receipts") + } + + override fun onMentionClicked() { + vectorBaseActivity.notImplemented("Mention") + } + + } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt index d563ec37d2..74c946b5eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -22,11 +22,20 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel import timber.log.Timber -class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberProfileViewState, +class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState, private val session: Session) : VectorViewModel(initialState) { @@ -44,8 +53,73 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted initialSt } } + private val room = if (initialState.roomId != null) { + session.getRoom(initialState.roomId) + } else { + null + } + + init { + setState { copy(isMine = session.myUserId == this.userId) } + observeRoomSummary() + observeRoomMemberSummary() + observePowerLevel() + observeUserIfRequired() + } + + private fun observeUserIfRequired() { + if (initialState.roomId != null) { + return + } + session.rx().liveUser(initialState.userId) + .unwrap() + .execute { + copy(user = it) + } + } + override fun handle(action: RoomMemberProfileAction) { Timber.v("Handle $action") } + private fun observeRoomSummary() { + if (room == null) { + return + } + room.rx().liveRoomSummary() + .unwrap() + .execute { + copy(roomSummary = it) + } + } + + private fun observeRoomMemberSummary() { + if (room == null) { + return + } + val queryParams = roomMemberQueryParams { + this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE) + } + room.rx().liveRoomMembers(queryParams) + .map { it.firstOrNull().toOptional() } + .unwrap() + .execute { + copy(roomMemberSummary = it) + } + } + + private fun observePowerLevel() { + if (room == null) { + return + } + room.rx() + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + .mapOptional { it.content.toModel() } + .unwrap() + .execute { + copy(powerLevelsContent = it) + } + } + + } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt index 032bf853f4..a1f099c5d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -17,13 +17,35 @@ package im.vector.riotx.features.roommemberprofile +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem data class RoomMemberProfileViewState( val userId: String, - val roomId: String? + val roomId: String?, + val isMine: Boolean = false, + val roomSummary: Async = Uninitialized, + val roomMemberSummary: Async = Uninitialized, + val user: Async = Uninitialized, + val powerLevelsContent: Async = Uninitialized ) : MvRxState { constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId) + + fun memberAsMatrixItem(): MatrixItem? { + return if (roomId == null) { + user.invoke()?.toMatrixItem() + } else { + roomMemberSummary.invoke()?.toMatrixItem() + } + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt index 14c3421e7f..bc8bc2a959 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt @@ -21,8 +21,8 @@ import android.content.Context import android.content.Intent import androidx.appcompat.widget.Toolbar import im.vector.riotx.R -import im.vector.riotx.core.extensions.addFragment -import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment @@ -32,6 +32,9 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val EXTRA_ROOM_PROFILE_ARGS = "EXTRA_ROOM_PROFILE_ARGS" + private const val TAG_ROOM_PROFILE_FRAGMENT = "TAG_ROOM_PROFILE_FRAGMENT" + private const val TAG_ROOM_MEMBER_LIST_FRAGMENT = "TAG_ROOM_MEMBER_LIST_FRAGMENT" + fun newIntent(context: Context, roomId: String): Intent { val roomProfileArgs = RoomProfileArgs(roomId) @@ -50,7 +53,14 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) roomProfileArgs = intent?.extras?.getParcelable(EXTRA_ROOM_PROFILE_ARGS) ?: return if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + val argsBundle = roomProfileArgs.toMvRxBundle() + val roomProfileFragment = createFragment(RoomProfileFragment::class.java, argsBundle) + val roomMemberListFragment = createFragment(RoomMemberListFragment::class.java, argsBundle) + supportFragmentManager.commitTransactionNow { + add(R.id.simpleFragmentContainer, roomProfileFragment, TAG_ROOM_PROFILE_FRAGMENT) + add(R.id.simpleFragmentContainer, roomMemberListFragment, TAG_ROOM_MEMBER_LIST_FRAGMENT) + detach(roomMemberListFragment) + } } sharedActionViewModel .observe() @@ -73,7 +83,15 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { } private fun openRoomMembers() { - addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs) + val roomProfileFragment = supportFragmentManager.findFragmentByTag(TAG_ROOM_PROFILE_FRAGMENT) + ?: throw IllegalStateException("You should have a RoomProfileFragment") + val roomMemberListFragment = supportFragmentManager.findFragmentByTag(TAG_ROOM_MEMBER_LIST_FRAGMENT) + ?: throw IllegalStateException("You should have a RoomMemberListFragment") + supportFragmentManager.commitTransaction { + hide(roomProfileFragment) + attach(roomMemberListFragment) + addToBackStack(null) + } } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt index f2f266bc5a..814cdc7723 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt @@ -97,6 +97,7 @@ class RoomProfileController @Inject constructor(private val stringProvider: Stri title = stringProvider.getString(R.string.room_profile_section_more_leave), divider = false, destructive = true, + editable = false, action = { callback?.onLeaveRoomClicked() } ) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 50be4a2d87..b596c9725d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -24,16 +24,16 @@ import android.os.Bundle import android.os.Parcelable import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.appbar.AppBarLayout import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.animations.AppBarStateChangeListener +import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.AvatarRenderer @@ -42,7 +42,8 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBotto import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.fragment_room_profile.* +import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.view_stub_room_profile_header.* import timber.log.Timber import javax.inject.Inject @@ -63,26 +64,22 @@ class RoomProfileFragment @Inject constructor( private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel private val roomProfileViewModel: RoomProfileViewModel by fragmentViewModel() - override fun getLayoutResId() = R.layout.fragment_room_profile + private lateinit var appBarStateChangeListener: AppBarStateChangeListener + + + override fun getLayoutResId() = R.layout.fragment_matrix_profile override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) - setupToolbar(roomProfileToolbar) + matrixProfileHeaderView.apply { + layoutResource = R.layout.view_stub_room_profile_header + inflate() + } setupRecyclerView() - roomProfileAppBarLayout.addOnOffsetChangedListener(object : AppBarStateChangeListener() { - override fun onStateChanged(appBarLayout: AppBarLayout, state: State) { - val animationDuration = roomProfileCollapsingToolbarLayout.scrimAnimationDuration - if (state == State.COLLAPSED) { - roomProfileToolbarAvatarImageView.animate().alpha(1f).duration = animationDuration + 100 - roomProfileToolbarTitleView.animate().alpha(1f).duration = animationDuration + 100 - } else { - roomProfileToolbarAvatarImageView.animate().alpha(0f).duration = animationDuration - 100 - roomProfileToolbarTitleView.animate().alpha(0f).duration = animationDuration - 100 - } - } - }) + appBarStateChangeListener = MatrixItemAppBarStateChangeListener(matrixProfileCollapsingToolbarLayout.scrimAnimationDuration, listOf(matrixProfileToolbarAvatarImageView, matrixProfileToolbarTitleView)) + matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) roomProfileViewModel.viewEvents .observe() .subscribe { @@ -101,6 +98,11 @@ class RoomProfileFragment @Inject constructor( .disposeOnDestroyView() } + override fun onResume() { + super.onResume() + setupToolbar(matrixProfileToolbar) + } + private fun handleQuickActions(action: RoomListQuickActionsSharedAction) = when (action) { is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.ALL_MESSAGES_NOISY)) @@ -135,14 +137,13 @@ class RoomProfileFragment @Inject constructor( private fun setupRecyclerView() { roomProfileController.callback = this - roomProfileRecyclerView.setHasFixedSize(true) - roomProfileRecyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) - roomProfileRecyclerView.adapter = roomProfileController.adapter + matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true) } override fun onDestroyView() { super.onDestroyView() - roomProfileRecyclerView.adapter = null + matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) + matrixProfileRecyclerView.cleanup() } override fun invalidate() = withState(roomProfileViewModel) { state -> @@ -152,12 +153,12 @@ class RoomProfileFragment @Inject constructor( activity?.finish() } else { roomProfileNameView.text = it.displayName - roomProfileToolbarTitleView.text = it.displayName + matrixProfileToolbarTitleView.text = it.displayName roomProfileAliasView.setTextOrHide(it.canonicalAlias) roomProfileTopicView.setTextOrHide(it.topic) val matrixItem = it.toMatrixItem() avatarRenderer.render(matrixItem, roomProfileAvatarView) - avatarRenderer.render(matrixItem, roomProfileToolbarAvatarImageView) + avatarRenderer.render(matrixItem, matrixProfileToolbarAvatarImageView) } } roomProfileController.setData(state) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt index 857b03b263..baf7c4d2e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.roomprofile.members import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.epoxy.profiles.profileMatrixItem import im.vector.riotx.core.resources.StringProvider @@ -56,6 +57,10 @@ class RoomMemberListController @Inject constructor(private val avatarRenderer: A callback?.onRoomMemberClicked(roomMember) } } + + dividerItem { + id("divider_${roomMember.userId}") + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt index 4b9955bf2f..b2318259f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt @@ -30,7 +30,6 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs import kotlinx.android.synthetic.main.fragment_room_member_list.* -import kotlinx.android.synthetic.main.fragment_room_member_list.recyclerView import javax.inject.Inject @@ -48,11 +47,15 @@ class RoomMemberListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupToolbar(roomMemberListToolbar) roomMemberListController.callback = this recyclerView.configureWith(roomMemberListController, hasFixedSize = true) } + override fun onResume() { + super.onResume() + setupToolbar(roomMemberListToolbar) + } + override fun onDestroyView() { recyclerView.cleanup() super.onDestroyView() diff --git a/vector/src/main/res/layout/fragment_room_profile.xml b/vector/src/main/res/layout/fragment_matrix_profile.xml similarity index 54% rename from vector/src/main/res/layout/fragment_room_profile.xml rename to vector/src/main/res/layout/fragment_matrix_profile.xml index c9f79caec7..384c87cccc 100644 --- a/vector/src/main/res/layout/fragment_room_profile.xml +++ b/vector/src/main/res/layout/fragment_matrix_profile.xml @@ -7,82 +7,33 @@ android:background="?riotx_header_panel_background"> + app:toolbarId="@+id/matrixProfileToolbar"> - - - - - - - - - - - + app:layout_collapseParallaxMultiplier="0.9" /> @@ -130,7 +85,7 @@ - - - - - - - diff --git a/vector/src/main/res/layout/view_stub_room_member_profile_header.xml b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml new file mode 100644 index 0000000000..fc4836b80e --- /dev/null +++ b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_stub_room_profile_header.xml b/vector/src/main/res/layout/view_stub_room_profile_header.xml new file mode 100644 index 0000000000..6a0d0fc5de --- /dev/null +++ b/vector/src/main/res/layout/view_stub_room_profile_header.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 72f3113308..4fa6c5bbd4 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -41,4 +41,11 @@ Invites Users + Admin in %1$s + Moderator in %1$s + Custom (%1$d) in %2$s + + Jump to read receipt + +