Room member profile: branch the UI and fix some UI issues

This commit is contained in:
ganfra 2020-01-13 16:49:14 +01:00
parent 171ec4fbdc
commit ae1a24e948
23 changed files with 586 additions and 151 deletions

View file

@ -28,17 +28,20 @@ fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {})
*/
data class RoomMemberQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
val memberships: List<Membership>,
val userId: QueryStringValue
) {
class Builder {
var userId: QueryStringValue = QueryStringValue.NoCondition
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = RoomMemberQueryParams(
displayName = displayName,
memberships = memberships
memberships = memberships,
userId = userId
)
}
}

View file

@ -93,6 +93,7 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery<RoomMemberSummaryEntity> {
return RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
.process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId)
.process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
.process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
}

View file

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

View file

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

View file

@ -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<out Fragment>, args: Bundle?): Fragment {
return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply {
arguments = args
}
}
/* ==========================================================================================
* PRIVATE METHODS
* ========================================================================================== */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<RoomMemberProfileViewState, RoomMemberProfileAction>(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<PowerLevelsContent>() }
.unwrap()
.execute {
copy(powerLevelsContent = it)
}
}
}

View file

@ -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<RoomSummary?> = Uninitialized,
val roomMemberSummary: Async<RoomMemberSummary> = Uninitialized,
val user: Async<User> = Uninitialized,
val powerLevelsContent: Async<PowerLevelsContent> = 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()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,82 +7,33 @@
android:background="?riotx_header_panel_background">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/roomProfileAppBarLayout"
android:id="@+id/matrixProfileAppBarLayout"
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/roomProfileCollapsingToolbarLayout"
android:id="@+id/matrixProfileCollapsingToolbarLayout"
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
app:contentScrim="?riotx_background"
app:scrimAnimationDuration="250"
android:layout_height="match_parent"
app:contentScrim="?riotx_background"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:scrimAnimationDuration="250"
app:titleEnabled="false"
app:toolbarId="@+id/roomProfileToolbar">
app:toolbarId="@+id/matrixProfileToolbar">
<LinearLayout
android:id="@+id/roomProfileHeaderView"
<ViewStub
android:id="@+id/matrixProfileHeaderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:orientation="vertical"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.9">
<ImageView
android:id="@+id/roomProfileAvatarView"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomProfileNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Random" />
<TextView
android:id="@+id/roomProfileAliasView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:singleLine="true"
android:layout_marginBottom="16dp"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="16sp"
tools:text="#random:matrix.org" />
<TextView
android:id="@+id/roomProfileTopicView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="40dp"
android:fontFamily="sans-serif"
android:gravity="center"
android:textSize="14sp"
android:textStyle="normal"
tools:text="Here is a room topic, it can be multi-line but should always be displayed in full 🍱" />
</LinearLayout>
app:layout_collapseParallaxMultiplier="0.9" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomProfileToolbar"
android:id="@+id/matrixProfileToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
@ -93,29 +44,33 @@
android:layout_height="match_parent">
<ImageView
android:id="@+id/roomProfileToolbarAvatarImageView"
android:id="@+id/matrixProfileToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:alpha="0"
tools:alpha="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomProfileToolbarTitleView"
android:id="@+id/matrixProfileToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:alpha="0"
tools:alpha="1"
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@+id/roomProfileToolbarAvatarImageView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/matrixProfileToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" />
@ -130,7 +85,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomProfileRecyclerView"
android:id="@+id/matrixProfileRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"

View file

@ -9,6 +9,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomMemberListToolbar"
style="@style/VectorToolbarStyle"
android:elevation="4dp"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
~
-->
<androidx.constraintlayout.widget.ConstraintLayout 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/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RoomMemberProfileFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/matrixProfileHeaderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="16dp"
android:orientation="vertical">
<ImageView
android:id="@+id/memberProfileAvatarView"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/memberProfileNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="20sp"
android:textStyle="bold"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/memberProfileIdView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:singleLine="true"
android:textStyle="bold"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="14sp"
tools:text="@sample/matrix.json/data/mxid" />
<TextView
android:id="@+id/memberProfilePowerLevelView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:textSize="12sp"
tools:text="Admin in Matrix" />
<TextView
android:id="@+id/memberProfileStatusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:gravity="center"
android:textSize="14sp"
android:visibility="gone"
tools:text="Here is a room topic, it can be multi-line but should always be displayed in full 🍱" />
</LinearLayout>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="16dp"
android:orientation="vertical">
<ImageView
android:id="@+id/roomProfileAvatarView"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomProfileNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Random" />
<TextView
android:id="@+id/roomProfileAliasView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:singleLine="true"
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="14sp"
android:textStyle="bold"
tools:text="#random:matrix.org" />
<TextView
android:id="@+id/roomProfileTopicView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:fontFamily="sans-serif"
android:gravity="center"
android:textSize="14sp"
android:textStyle="normal"
tools:text="Here is a room topic, it can be multi-line but should always be displayed in full 🍱" />
</LinearLayout>

View file

@ -41,4 +41,11 @@
<string name="room_member_power_level_invites">Invites</string>
<string name="room_member_power_level_users">Users</string>
<string name="room_member_power_level_admin_in">Admin in %1$s</string>
<string name="room_member_power_level_moderator_in">Moderator in %1$s</string>
<string name="room_member_power_level_custom_in">Custom (%1$d) in %2$s</string>
<string name="room_member_jump_to_read_receipt">Jump to read receipt</string>
</resources>