diff --git a/CHANGES.md b/CHANGES.md index a3d4c445ce..52c3ceba3b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,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) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 228e83faff..86f2d26808 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -142,6 +142,10 @@ class RxRoom(private val room: Room) { fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { room.updateAvatar(avatarUri, fileName, it) } + + fun deleteAvatar(): Completable = completableBuilder { + room.deleteAvatar(it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 8c08743972..e4baa58c30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -58,6 +58,11 @@ interface StateService { */ fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + /** + * Delete the avatar of the room + */ + fun deleteAvatar(callback: MatrixCallback): Cancelable + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index d21805f4f3..65d375e176 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } } + + override fun deleteAvatar(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = emptyMap(), + callback = callback, + stateKey = null + ) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt index 9849f22f87..b279af2922 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt @@ -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() { @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 - 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, 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) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index ece221d884..85bc8773a5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 0ad831057a..5453545874 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -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) @@ -113,11 +98,10 @@ class RoomProfileFragment @Inject constructor( matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) roomProfileViewModel.observeViewEvents { when (it) { - 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) + is RoomProfileViewEvents.Loading -> showLoading(it.message) + is RoomProfileViewEvents.Failure -> showFailure(it.throwable) + is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) + is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it) }.exhaustive } roomListQuickActionsSharedActionViewModel @@ -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) @@ -269,45 +245,9 @@ 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) + val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.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()) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt index 380efd6fcd..237df0bed5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 922dd995e9..e927ec9876 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -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() - } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 10d35db36e..50723655bc 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -26,8 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomProfileViewState( val roomId: String, val roomSummary: Async = Uninitialized, - val bannedMembership: Async> = Uninitialized, - val canChangeAvatar: Boolean = false + val bannedMembership: Async> = Uninitialized ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt index 5d35586cce..62852562b0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index f680e28aa8..08a9393f4f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -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() { 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,24 @@ 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) + matrixItem(roomSummary.toMatrixItem()) + } + RoomSettingsViewState.AvatarAction.DeleteAvatar -> + imageUri(null) + is RoomSettingsViewState.AvatarAction.UpdateAvatar -> + imageUri(avatarAction.newAvatarUri) + } + clickListener { callback?.onAvatarChange() } + deleteListener { callback?.onAvatarDelete() } + } + buildProfileSection( stringProvider.getString(R.string.settings) ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 68c631b391..ad8da40be8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -16,30 +16,40 @@ 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.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 +57,14 @@ class RoomSettingsFragment @Inject constructor( private val controller: RoomSettingsController, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val avatarRenderer: AvatarRenderer -) : VectorBaseFragment(), RoomSettingsController.Callback { +) : + VectorBaseFragment(), + RoomSettingsController.Callback, + 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 +175,56 @@ 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()) + ) + ) + } + } + } + } + + // TODO BMA Handle Back with unsaved data } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 0fdb6139c2..51056b25a7 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -60,11 +60,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: 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 +74,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 +104,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, @@ -117,6 +121,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: 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 +137,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 ?: "")) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index fe04c8b508..a722ce9875 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -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,7 @@ data class RoomSettingsViewState( val historyVisibilityEvent: Event? = null, val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, + val avatarAction: AvatarAction = AvatarAction.None, val newName: String? = null, val newTopic: String? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, @@ -40,10 +42,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() + } } diff --git a/vector/src/main/res/menu/vector_big_avatar_viewer.xml b/vector/src/main/res/menu/vector_big_avatar_viewer.xml deleted file mode 100644 index aff0ac0e03..0000000000 --- a/vector/src/main/res/menu/vector_big_avatar_viewer.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file