Merge pull request #2282 from vector-im/feature/bma/room_settings

Feature/bma/room settings
This commit is contained in:
Benoit Marty 2020-10-28 13:54:08 +01:00 committed by GitHub
commit 1e60d6f6e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 226 additions and 254 deletions

View File

@ -11,6 +11,7 @@ Improvements 🙌:
- Add option to send with enter (#1195)
- 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)
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
Bugfix 🐛:
- 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> {
room.updateAvatar(avatarUri, fileName, it)
}
fun deleteAvatar(): Completable = completableBuilder<Unit> {
room.deleteAvatar(it)
}
}
fun Room.rx(): RxRoom {

View File

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

View File

@ -16,41 +16,25 @@
package im.vector.app.features.media
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
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 com.yalantis.ucrop.UCrop
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
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.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 java.io.File
import javax.inject.Inject
/**
* Simple Activity to display an avatar in fullscreen
*/
class BigImageViewerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var colorProvider: ColorProvider
private var uri: Uri? = null
override fun getMenuRes() = R.menu.vector_big_avatar_viewer
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@ -66,7 +50,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
setDisplayHomeAsUpEnabled(true)
}
uri = sessionHolder.getSafeActiveSession()
val uri = sessionHolder.getSafeActiveSession()
?.contentUrlResolver()
?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
?.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 {
private const val EXTRA_TITLE = "EXTRA_TITLE"
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 {
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_IMAGE_URL, imageUrl)
putExtra(EXTRA_CAN_EDIT_IMAGE, canEditImage)
}
}
}

View File

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

View File

@ -17,35 +17,26 @@
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.Parcelable
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener
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.configureWith
import im.vector.app.core.extensions.copyOnLongClick
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.intent.getFilenameFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.copyToClipboard
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.RoomListQuickActionsSharedActionViewModel
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.synthetic.main.fragment_matrix_profile.*
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.toMatrixItem
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@Parcelize
@ -78,8 +66,7 @@ class RoomProfileFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
) : VectorBaseFragment(),
RoomProfileController.Callback,
GalleryOrCameraDialogHelper.Listener {
RoomProfileController.Callback {
private val roomProfileArgs: RoomProfileArgs by args()
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -92,8 +79,6 @@ class RoomProfileFragment @Inject constructor(
override fun getMenuRes() = R.menu.vector_room_profile
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
@ -116,7 +101,6 @@ class RoomProfileFragment @Inject constructor(
is RoomProfileViewEvents.Loading -> showLoading(it.message)
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
}.exhaustive
}
@ -158,14 +142,6 @@ class RoomProfileFragment @Inject constructor(
else -> Timber.v("$action not handled")
}
private fun onLeaveRoom() {
vectorBaseActivity.finish()
}
private fun showError(throwable: Throwable) {
showErrorInSnackbar(throwable)
}
private fun setupRecyclerView() {
roomProfileController.callback = this
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
@ -268,46 +244,12 @@ class RoomProfileFragment @Inject constructor(
}
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
if (matrixItem.avatarUrl?.isNotEmpty() == true) {
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar)
matrixItem.avatarUrl
?.takeIf { it.isNotEmpty() }
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), avatarUrl)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
bigImageStartForActivityResult.launch(intent, options)
} 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()
startActivity(intent, options.toBundle())
}
}
}

View File

@ -26,7 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : 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.resources.StringProvider
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
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.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.unwrap
import java.util.UUID
class RoomProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomProfileViewState,
@ -65,33 +62,23 @@ class RoomProfileViewModel @AssistedInject constructor(
private val room = session.getRoom(initialState.roomId)!!
init {
observeRoomSummary()
val rxRoom = room.rx()
observeRoomSummary(rxRoom)
observeBannedRoomMembers(rxRoom)
}
private fun observeRoomSummary() {
val rxRoom = room.rx()
private fun observeRoomSummary(rxRoom: RxRoom) {
rxRoom.liveRoomSummary()
.unwrap()
.execute {
copy(roomSummary = it)
}
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
powerLevelsContentLive
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState {
copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR))
}
}
.disposeOnClear()
private fun observeBannedRoomMembers(rxRoom: RxRoom) {
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
.execute {
copy(
bannedMembership = it
)
copy(bannedMembership = it)
}
}
@ -100,7 +87,6 @@ class RoomProfileViewModel @AssistedInject constructor(
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
}.exhaustive
}
@ -142,18 +128,4 @@ class RoomProfileViewModel @AssistedInject constructor(
_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(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized,
val canChangeAvatar: Boolean = false
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized
) : MvRxState {
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
sealed class RoomSettingsAction : VectorViewModelAction {
data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction()
data class SetRoomName(val newName: String) : RoomSettingsAction()
data class SetRoomTopic(val newTopic: String) : 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.StringProvider
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 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.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSettingsController @Inject constructor(
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomSettingsViewState>() {
interface Callback {
// Delete the avatar, or cancel an avatar change
fun onAvatarDelete()
fun onAvatarChange()
fun onEnableEncryptionClicked()
fun onNameChanged(name: String)
fun onTopicChanged(topic: String)
@ -58,6 +65,25 @@ class RoomSettingsController @Inject constructor(
val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(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)
// We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null)
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
imageUri(avatarAction.newAvatarUri)
}
clickListener { callback?.onAvatarChange() }
deleteListener { callback?.onAvatarDelete() }
}
buildProfileSection(
stringProvider.getString(R.string.settings)
)

View File

@ -16,30 +16,41 @@
package im.vector.app.features.roomprofile.settings
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
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.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import im.vector.app.features.home.AvatarRenderer
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.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
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.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.util.toMatrixItem
import java.io.File
import java.util.UUID
import javax.inject.Inject
class RoomSettingsFragment @Inject constructor(
@ -47,10 +58,15 @@ class RoomSettingsFragment @Inject constructor(
private val controller: RoomSettingsController,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), RoomSettingsController.Callback {
) :
VectorBaseFragment(),
RoomSettingsController.Callback,
OnBackPressed,
GalleryOrCameraDialogHelper.Listener {
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
@ -161,4 +177,77 @@ class RoomSettingsFragment @Inject constructor(
override fun onAliasChanged(alias: String) {
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())
)
)
}
}
}
}
private var ignoreChanges = false
override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (ignoreChanges) return false
return withState(viewModel) {
return@withState if (it.showSaveAction) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.warning_unsaved_change)
.setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ ->
ignoreChanges = true
vectorBaseActivity.onBackPressed()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
} else {
false
}
}
}
}

View File

@ -27,9 +27,13 @@ import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Completable
import io.reactivex.Observable
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.query.QueryStringValue
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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.rx.mapOptional
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@ -55,16 +59,19 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
init {
observeRoomSummary()
observeRoomAvatar()
observeState()
}
private fun observeState() {
selectSubscribe(
RoomSettingsViewState::avatarAction,
RoomSettingsViewState::newName,
RoomSettingsViewState::newCanonicalAlias,
RoomSettingsViewState::newTopic,
RoomSettingsViewState::newHistoryVisibility,
RoomSettingsViewState::roomSummary) { newName,
RoomSettingsViewState::roomSummary) { avatarAction,
newName,
newCanonicalAlias,
newTopic,
newHistoryVisibility,
@ -72,7 +79,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = asyncSummary()
setState {
copy(
showSaveAction = summary?.name != newName
showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None
|| summary?.name != newName
|| summary?.topic != newTopic
|| summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() }
|| newHistoryVisibility != null
@ -101,6 +109,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = RoomSettingsViewState.ActionPermissions(
canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
@ -114,9 +123,24 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.disposeOnClear()
}
/**
* We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
*/
private fun observeRoomAvatar() {
room.rx()
.liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<RoomAvatarContent>() }
.unwrap()
.subscribe {
setState { copy(currentRoomAvatarUrl = it.avatarUrl) }
}
.disposeOnClear()
}
override fun handle(action: RoomSettingsAction) {
when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
is RoomSettingsAction.SetAvatarAction -> setState { copy(avatarAction = action.avatarAction) }
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
@ -132,6 +156,15 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
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) {
operationList.add(room.rx().updateName(state.newName ?: ""))
}

View File

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

View File

@ -2630,4 +2630,6 @@
<!-- Universal link -->
<string name="universal_link_malformed">The link was malformed</string>
<string name="warning_unsaved_change">There are unsaved changes. Discard the changes?</string>
<string name="warning_unsaved_change_discard">Discard changes</string>
</resources>