Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar

This commit is contained in:
Benoit Marty 2020-10-22 14:34:15 +02:00 committed by Benoit Marty
parent 89fa345140
commit b78dac20c0
17 changed files with 175 additions and 251 deletions

View File

@ -10,6 +10,7 @@ Improvements 🙌:
- Add option to send with enter (#1195) - Add option to send with enter (#1195)
- Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440) - Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440)
- Edit and remove icons are now visible on image attachment preview screen (#2294) - Edit and remove icons are now visible on image attachment preview screen (#2294)
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
Bugfix 🐛: Bugfix 🐛:
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)

View File

@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> { fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it) room.updateAvatar(avatarUri, fileName, it)
} }
fun deleteAvatar(): Completable = completableBuilder<Unit> {
room.deleteAvatar(it)
}
} }
fun Room.rx(): RxRoom { fun Room.rx(): RxRoom {

View File

@ -58,6 +58,11 @@ interface StateService {
*/ */
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Delete the avatar of the room
*/
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
) )
} }
} }
override fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = emptyMap(),
callback = callback,
stateKey = null
)
}
}
} }

View File

@ -29,12 +29,16 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_editable_avatar) @EpoxyModelClass(layout = R.layout.item_editable_avatar)
abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarItem.Holder>() { abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer var avatarRenderer: AvatarRenderer? = null
@EpoxyAttribute
var matrixItem: MatrixItem? = null
@EpoxyAttribute @EpoxyAttribute
var enabled: Boolean = true var enabled: Boolean = true
@ -51,11 +55,15 @@ abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarI
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.imageContainer.onClick(clickListener?.takeIf { enabled }) holder.imageContainer.onClick(clickListener?.takeIf { enabled })
GlideApp.with(holder.image) if (matrixItem != null) {
.load(imageUri) avatarRenderer?.render(matrixItem!!, holder.image)
.apply(RequestOptions.circleCropTransform()) } else {
.into(holder.image) GlideApp.with(holder.image)
holder.delete.isVisible = imageUri != null .load(imageUri)
.apply(RequestOptions.circleCropTransform())
.into(holder.image)
}
holder.delete.isVisible = imageUri != null || matrixItem?.avatarUrl != null
holder.delete.onClick(deleteListener?.takeIf { enabled }) holder.delete.onClick(deleteListener?.takeIf { enabled })
} }

View File

@ -16,41 +16,25 @@
package im.vector.app.features.media package im.vector.app.features.media
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri import androidx.core.net.toUri
import com.yalantis.ucrop.UCrop
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.activity_big_image_viewer.* import kotlinx.android.synthetic.main.activity_big_image_viewer.*
import java.io.File
import javax.inject.Inject import javax.inject.Inject
/**
* Simple Activity to display an avatar in fullscreen
*/
class BigImageViewerActivity : VectorBaseActivity() { class BigImageViewerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
private var uri: Uri? = null
override fun getMenuRes() = R.menu.vector_big_avatar_viewer
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
@ -66,7 +50,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
} }
uri = sessionHolder.getSafeActiveSession() val uri = sessionHolder.getSafeActiveSession()
?.contentUrlResolver() ?.contentUrlResolver()
?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL)) ?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
?.toUri() ?.toUri()
@ -78,117 +62,14 @@ class BigImageViewerActivity : VectorBaseActivity() {
} }
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.bigAvatarEditAction).isVisible = shouldShowEditAction()
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.bigAvatarEditAction) {
showAvatarSelector()
return true
}
return super.onOptionsItemSelected(item)
}
private fun shouldShowEditAction(): Boolean {
return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false)
}
private fun showAvatarSelector() {
AlertDialog.Builder(this)
.setItems(arrayOf(
getString(R.string.attachment_type_camera),
getString(R.string.attachment_type_gallery)
)) { dialog, which ->
dialog.cancel()
onAvatarTypeSelected(isCamera = (which == 0))
}
.show()
}
private var avatarCameraUri: Uri? = null
private fun onAvatarTypeSelected(isCamera: Boolean) {
if (isCamera) {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this, takePhotoActivityResultLauncher)
}
} else {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
}
private fun onRoomAvatarSelected(image: MultiPickerImageType) {
val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri
createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName)
.withAspectRatio(1f, 1f)
.start(this)
}
private val takePhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
avatarCameraUri?.let { uri ->
MultiPicker.get(MultiPicker.CAMERA)
.getTakenPhoto(this, uri)
?.let {
onRoomAvatarSelected(it)
}
}
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(this, activityResult.data)
.firstOrNull()?.let {
onRoomAvatarSelected(it)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// TODO handle this one (Ucrop lib)
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (allGranted(grantResults)) {
when (requestCode) {
PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true)
}
}
}
private fun onAvatarCropped(uri: Uri?) {
if (uri != null) {
setResult(Activity.RESULT_OK, Intent().setData(uri))
this@BigImageViewerActivity.finish()
} else {
Toast.makeText(this, "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
}
}
companion object { companion object {
private const val EXTRA_TITLE = "EXTRA_TITLE" private const val EXTRA_TITLE = "EXTRA_TITLE"
private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL" private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL"
private const val EXTRA_CAN_EDIT_IMAGE = "EXTRA_CAN_EDIT_IMAGE"
fun newIntent(context: Context, title: String?, imageUrl: String, canEditImage: Boolean = false): Intent { fun newIntent(context: Context, title: String?, imageUrl: String): Intent {
return Intent(context, BigImageViewerActivity::class.java).apply { return Intent(context, BigImageViewerActivity::class.java).apply {
putExtra(EXTRA_TITLE, title) putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_IMAGE_URL, imageUrl) putExtra(EXTRA_IMAGE_URL, imageUrl)
putExtra(EXTRA_CAN_EDIT_IMAGE, canEditImage)
} }
} }
} }

View File

@ -17,14 +17,12 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
sealed class RoomProfileAction : VectorViewModelAction { sealed class RoomProfileAction : VectorViewModelAction {
object LeaveRoom : RoomProfileAction() object LeaveRoom : RoomProfileAction()
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction() object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction() object CreateShortcut : RoomProfileAction()
} }

View File

@ -17,35 +17,26 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener import im.vector.app.core.animations.AppBarStateChangeListener
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.copyOnLongClick import im.vector.app.core.extensions.copyOnLongClick
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
@ -56,8 +47,6 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomS
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.* import kotlinx.android.synthetic.main.fragment_matrix_profile.*
import kotlinx.android.synthetic.main.view_stub_room_profile_header.* import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
@ -65,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber import timber.log.Timber
import java.io.File
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -78,8 +66,7 @@ class RoomProfileFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory val roomProfileViewModelFactory: RoomProfileViewModel.Factory
) : VectorBaseFragment(), ) : VectorBaseFragment(),
RoomProfileController.Callback, RoomProfileController.Callback {
GalleryOrCameraDialogHelper.Listener {
private val roomProfileArgs: RoomProfileArgs by args() private val roomProfileArgs: RoomProfileArgs by args()
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -92,8 +79,6 @@ class RoomProfileFragment @Inject constructor(
override fun getMenuRes() = R.menu.vector_room_profile override fun getMenuRes() = R.menu.vector_room_profile
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
@ -113,11 +98,10 @@ class RoomProfileFragment @Inject constructor(
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
roomProfileViewModel.observeViewEvents { roomProfileViewModel.observeViewEvents {
when (it) { when (it) {
is RoomProfileViewEvents.Loading -> showLoading(it.message) is RoomProfileViewEvents.Loading -> showLoading(it.message)
is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog() is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
}.exhaustive }.exhaustive
} }
roomListQuickActionsSharedActionViewModel roomListQuickActionsSharedActionViewModel
@ -158,14 +142,6 @@ class RoomProfileFragment @Inject constructor(
else -> Timber.v("$action not handled") else -> Timber.v("$action not handled")
} }
private fun onLeaveRoom() {
vectorBaseActivity.finish()
}
private fun showError(throwable: Throwable) {
showErrorInSnackbar(throwable)
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
roomProfileController.callback = this roomProfileController.callback = this
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true) matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
@ -269,45 +245,9 @@ class RoomProfileFragment @Inject constructor(
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) { private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
if (matrixItem.avatarUrl?.isNotEmpty() == true) { if (matrixItem.avatarUrl?.isNotEmpty() == true) {
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar) val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "") val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
bigImageStartForActivityResult.launch(intent, options) startActivity(intent, options.toBundle())
} else if (it.canChangeAvatar) {
galleryOrCameraDialogHelper.show()
}
}
override fun onImageReady(image: MultiPickerImageType) {
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
.withAspectRatio(1f, 1f)
.start(requireContext(), this)
}
private val bigImageStartForActivityResult = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
activityResult.data?.let { onAvatarCropped(it.data) }
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// TODO handle this one (Ucrop lib)
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
}
}
}
private fun onAvatarCropped(uri: Uri?) {
if (uri != null) {
roomProfileViewModel.handle(RoomProfileAction.ChangeRoomAvatar(uri, getFilenameFromUri(context, uri)))
} else {
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
} }
} }
} }

View File

@ -26,7 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents() data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents() data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents() data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents()
} }

View File

@ -28,18 +28,15 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.rx.RxRoom
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import java.util.UUID
class RoomProfileViewModel @AssistedInject constructor( class RoomProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomProfileViewState, @Assisted private val initialState: RoomProfileViewState,
@ -65,33 +62,23 @@ class RoomProfileViewModel @AssistedInject constructor(
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
init { init {
observeRoomSummary() val rxRoom = room.rx()
observeRoomSummary(rxRoom)
observeBannedRoomMembers(rxRoom)
} }
private fun observeRoomSummary() { private fun observeRoomSummary(rxRoom: RxRoom) {
val rxRoom = room.rx()
rxRoom.liveRoomSummary() rxRoom.liveRoomSummary()
.unwrap() .unwrap()
.execute { .execute {
copy(roomSummary = it) copy(roomSummary = it)
} }
}
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() private fun observeBannedRoomMembers(rxRoom: RxRoom) {
powerLevelsContentLive
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState {
copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR))
}
}
.disposeOnClear()
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) }) rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
.execute { .execute {
copy( copy(bannedMembership = it)
bannedMembership = it
)
} }
} }
@ -100,7 +87,6 @@ class RoomProfileViewModel @AssistedInject constructor(
RoomProfileAction.LeaveRoom -> handleLeaveRoom() RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
RoomProfileAction.CreateShortcut -> handleCreateShortcut() RoomProfileAction.CreateShortcut -> handleCreateShortcut()
}.exhaustive }.exhaustive
} }
@ -142,18 +128,4 @@ class RoomProfileViewModel @AssistedInject constructor(
_viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink)) _viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
} }
} }
private fun handleChangeAvatar(action: RoomProfileAction.ChangeRoomAvatar) {
_viewEvents.post(RoomProfileViewEvents.Loading())
room.rx().updateAvatar(action.uri, action.fileName ?: UUID.randomUUID().toString())
.subscribe(
{
_viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess)
},
{
_viewEvents.post(RoomProfileViewEvents.Failure(it))
}
)
.disposeOnClear()
}
} }

View File

@ -26,8 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomProfileViewState( data class RoomProfileViewState(
val roomId: String, val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized, val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized
val canChangeAvatar: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View File

@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
sealed class RoomSettingsAction : VectorViewModelAction { sealed class RoomSettingsAction : VectorViewModelAction {
data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction()
data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomName(val newName: String) : RoomSettingsAction()
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()

View File

@ -23,20 +23,27 @@ import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formEditableAvatarItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class RoomSettingsController @Inject constructor( class RoomSettingsController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
colorProvider: ColorProvider colorProvider: ColorProvider
) : TypedEpoxyController<RoomSettingsViewState>() { ) : TypedEpoxyController<RoomSettingsViewState>() {
interface Callback { interface Callback {
// Delete the avatar, or cancel an avatar change
fun onAvatarDelete()
fun onAvatarChange()
fun onEnableEncryptionClicked() fun onEnableEncryptionClicked()
fun onNameChanged(name: String) fun onNameChanged(name: String)
fun onTopicChanged(topic: String) fun onTopicChanged(topic: String)
@ -58,6 +65,24 @@ class RoomSettingsController @Inject constructor(
val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: ""
val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) } val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) }
formEditableAvatarItem {
id("avatar")
enabled(data.actionPermissions.canChangeAvatar)
when (val avatarAction = data.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> {
// Use the current value
avatarRenderer(avatarRenderer)
matrixItem(roomSummary.toMatrixItem())
}
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null)
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
imageUri(avatarAction.newAvatarUri)
}
clickListener { callback?.onAvatarChange() }
deleteListener { callback?.onAvatarDelete() }
}
buildProfileSection( buildProfileSection(
stringProvider.getString(R.string.settings) stringProvider.getString(R.string.settings)
) )

View File

@ -16,30 +16,40 @@
package im.vector.app.features.roomprofile.settings package im.vector.app.features.roomprofile.settings
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import java.io.File
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
class RoomSettingsFragment @Inject constructor( class RoomSettingsFragment @Inject constructor(
@ -47,10 +57,14 @@ class RoomSettingsFragment @Inject constructor(
private val controller: RoomSettingsController, private val controller: RoomSettingsController,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), RoomSettingsController.Callback { ) :
VectorBaseFragment(),
RoomSettingsController.Callback,
GalleryOrCameraDialogHelper.Listener {
private val viewModel: RoomSettingsViewModel by fragmentViewModel() private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args() private val roomProfileArgs: RoomProfileArgs by args()
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun getLayoutResId() = R.layout.fragment_room_setting_generic override fun getLayoutResId() = R.layout.fragment_room_setting_generic
@ -161,4 +175,56 @@ class RoomSettingsFragment @Inject constructor(
override fun onAliasChanged(alias: String) { override fun onAliasChanged(alias: String) {
viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias)) viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias))
} }
override fun onImageReady(image: MultiPickerImageType) {
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
.withAspectRatio(1f, 1f)
.start(requireContext(), this)
}
override fun onAvatarDelete() {
withState(viewModel) {
when (it.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> {
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
/* Should not happen */
Unit
}
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
// Cancel the update of the avatar
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.None))
}
}
}
}
override fun onAvatarChange() {
galleryOrCameraDialogHelper.show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// TODO handle this one (Ucrop lib)
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
UCrop.REQUEST_CROP -> {
val uri = data?.let { UCrop.getOutput(it) } ?: return
viewModel.handle(RoomSettingsAction.SetAvatarAction(
RoomSettingsViewState.AvatarAction.UpdateAvatar(
newAvatarUri = uri,
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
)
)
}
}
}
}
// TODO BMA Handle Back with unsaved data
} }

View File

@ -60,11 +60,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
private fun observeState() { private fun observeState() {
selectSubscribe( selectSubscribe(
RoomSettingsViewState::avatarAction,
RoomSettingsViewState::newName, RoomSettingsViewState::newName,
RoomSettingsViewState::newCanonicalAlias, RoomSettingsViewState::newCanonicalAlias,
RoomSettingsViewState::newTopic, RoomSettingsViewState::newTopic,
RoomSettingsViewState::newHistoryVisibility, RoomSettingsViewState::newHistoryVisibility,
RoomSettingsViewState::roomSummary) { newName, RoomSettingsViewState::roomSummary) { avatarAction,
newName,
newCanonicalAlias, newCanonicalAlias,
newTopic, newTopic,
newHistoryVisibility, newHistoryVisibility,
@ -72,7 +74,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = asyncSummary() val summary = asyncSummary()
setState { setState {
copy( copy(
showSaveAction = summary?.name != newName showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None
|| summary?.name != newName
|| summary?.topic != newTopic || summary?.topic != newTopic
|| summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() } || summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() }
|| newHistoryVisibility != null || newHistoryVisibility != null
@ -101,6 +104,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.subscribe { .subscribe {
val powerLevelsHelper = PowerLevelsHelper(it) val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = RoomSettingsViewState.ActionPermissions( val permissions = RoomSettingsViewState.ActionPermissions(
canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
@ -117,6 +121,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomSettingsAction) { override fun handle(action: RoomSettingsAction) {
when (action) { when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
is RoomSettingsAction.SetAvatarAction -> setState { copy(avatarAction = action.avatarAction) }
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) } is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
@ -132,6 +137,15 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = state.roomSummary.invoke() val summary = state.roomSummary.invoke()
when (val avatarAction = state.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> Unit
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
operationList.add(room.rx().deleteAvatar())
}
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
operationList.add(room.rx().updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName))
}
}
if (summary?.name != state.newName) { if (summary?.name != state.newName) {
operationList.add(room.rx().updateName(state.newName ?: "")) operationList.add(room.rx().updateName(state.newName ?: ""))
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.roomprofile.settings package im.vector.app.features.roomprofile.settings
import android.net.Uri
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
@ -29,6 +30,7 @@ data class RoomSettingsViewState(
val historyVisibilityEvent: Event? = null, val historyVisibilityEvent: Event? = null,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val avatarAction: AvatarAction = AvatarAction.None,
val newName: String? = null, val newName: String? = null,
val newTopic: String? = null, val newTopic: String? = null,
val newHistoryVisibility: RoomHistoryVisibility? = null, val newHistoryVisibility: RoomHistoryVisibility? = null,
@ -40,10 +42,18 @@ data class RoomSettingsViewState(
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
data class ActionPermissions( data class ActionPermissions(
val canChangeAvatar: Boolean = false,
val canChangeName: Boolean = false, val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false, val canChangeTopic: Boolean = false,
val canChangeCanonicalAlias: Boolean = false, val canChangeCanonicalAlias: Boolean = false,
val canChangeHistoryReadability: Boolean = false, val canChangeHistoryReadability: Boolean = false,
val canEnableEncryption: Boolean = false val canEnableEncryption: Boolean = false
) )
sealed class AvatarAction {
object None : AvatarAction()
object DeleteAvatar : AvatarAction()
data class UpdateAvatar(val newAvatarUri: Uri,
val newAvatarFileName: String) : AvatarAction()
}
} }

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/bigAvatarEditAction"
android:title="@string/edit"
android:icon="@drawable/ic_edit"
app:iconTint="?attr/colorAccent"
app:showAsAction="ifRoom" />
</menu>