diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 014a244bf8..acdad5407c 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -87,6 +87,7 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment +import im.vector.app.features.settings.VectorSettingsGeneralFragment import im.vector.app.features.settings.VectorSettingsHelpAboutFragment import im.vector.app.features.settings.VectorSettingsLabsFragment import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment @@ -292,6 +293,11 @@ interface FragmentModule { @FragmentKey(VectorSettingsPinFragment::class) fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VectorSettingsGeneralFragment::class) + fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment + @Binds @IntoMap @FragmentKey(PushRulesFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt index 1f357b4cfc..7198cdb4a2 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -19,25 +19,39 @@ package im.vector.app.core.dialogs import android.app.Activity import android.net.Uri import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri import androidx.fragment.app.Fragment +import com.yalantis.ucrop.UCrop import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.lib.multipicker.MultiPicker import im.vector.lib.multipicker.entity.MultiPickerImageType +import java.io.File +/** + * Use to let the user choose between Camera (with permission handling) and Gallery (with single image selection), + * then edit the image + * [Listener.onImageReady] will be called with an uri of a square image store in the cache of the application. + * It's up to the caller to delete the file. + */ class GalleryOrCameraDialogHelper( - private val fragment: Fragment + // must implement GalleryOrCameraDialogHelper.Listener + private val fragment: Fragment, + private val colorProvider: ColorProvider ) { interface Listener { - fun onImageReady(image: MultiPickerImageType) + fun onImageReady(uri: Uri?) } - private val activity by lazy { fragment.requireActivity() } + private val activity + get() = fragment.requireActivity() - private val listener: Listener = fragment as? Listener ?: error("Fragment must implements GalleryOrCameraDialogHelper.Listener") + private val listener = fragment as? Listener ?: error("Fragment must implement GalleryOrCameraDialogHelper.Listener") private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted -> if (allGranted) { @@ -49,8 +63,8 @@ class GalleryOrCameraDialogHelper( if (activityResult.resultCode == Activity.RESULT_OK) { avatarCameraUri?.let { uri -> MultiPicker.get(MultiPicker.CAMERA) - .getTakenPhoto(fragment.requireContext(), uri) - ?.let { listener.onImageReady(it) } + .getTakenPhoto(activity, uri) + ?.let { startUCrop(it) } } } } @@ -59,37 +73,53 @@ class GalleryOrCameraDialogHelper( if (activityResult.resultCode == Activity.RESULT_OK) { MultiPicker .get(MultiPicker.IMAGE) - .getSelectedFiles(fragment.requireContext(), activityResult.data) + .getSelectedFiles(activity, activityResult.data) .firstOrNull() - ?.let { listener.onImageReady(it) } + ?.let { startUCrop(it) } } } + private val uCropActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + activityResult.data?.let { listener.onImageReady(UCrop.getOutput(it)) } + } + } + + private fun startUCrop(image: MultiPickerImageType) { + val destinationFile = File(activity.cacheDir, "${image.displayName}_e_${System.currentTimeMillis()}") + val uri = image.contentUri + createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title)) + .withAspectRatio(1f, 1f) + .getIntent(activity) + .let { uCropActivityResultLauncher.launch(it) } + } + private enum class Type { - Gallery, - Camera + Camera, + Gallery } fun show() { - AlertDialog.Builder(fragment.requireContext()) + AlertDialog.Builder(activity) + .setTitle(R.string.attachment_type_dialog_title) .setItems(arrayOf( fragment.getString(R.string.attachment_type_camera), fragment.getString(R.string.attachment_type_gallery) - )) { dialog, which -> - dialog.cancel() + )) { _, which -> onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery) } + .setPositiveButton(R.string.cancel, null) .show() } private fun onAvatarTypeSelected(type: Type) { when (type) { - Type.Gallery -> - MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) Type.Camera -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) { - avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment.requireContext(), takePhotoActivityResultLauncher) + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher) } + Type.Gallery -> + MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index b040101c84..9f3ba39bbe 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -19,7 +19,6 @@ package im.vector.app.features.attachments.preview import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK -import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -39,6 +38,7 @@ import com.airbnb.mvrx.withState import com.yalantis.ucrop.UCrop import im.vector.app.R import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.OnSnapPositionChangeListener @@ -49,7 +49,6 @@ import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import timber.log.Timber import java.io.File import javax.inject.Inject @@ -80,20 +79,15 @@ class AttachmentsPreviewFragment @Inject constructor( } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - // TODO handle this one (Ucrop lib) - @Suppress("DEPRECATION") - super.onActivityResult(requestCode, resultCode, data) - - if (resultCode == RESULT_OK) { - if (requestCode == UCrop.REQUEST_CROP && data != null) { - Timber.v("Crop success") - handleCropResult(data) + private val uCropActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == RESULT_OK) { + val resultUri = activityResult.data?.let { UCrop.getOutput(it) } + if (resultUri != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) + } else { + Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() } } - if (resultCode == UCrop.RESULT_ERROR) { - Timber.v("Crop error") - } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -170,15 +164,6 @@ class AttachmentsPreviewFragment @Inject constructor( } } - private fun handleCropResult(result: Intent) { - val resultUri = UCrop.getOutput(result) - if (resultUri != null) { - viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) - } else { - Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() - } - } - private fun handleRemoveAction() { viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment) } @@ -187,8 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor( val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") val uri = currentAttachment.queryUri - createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), currentAttachment.name) - .start(requireContext(), this) + createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), currentAttachment.name) + .getIntent(requireContext()) + .let { intent -> uCropActivityResultLauncher.launch(intent) } } private fun setupRecyclerViews() { diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt index 72a7c2dc85..195421ff58 100644 --- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt @@ -24,7 +24,6 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.resources.ColorProvider import kotlinx.android.synthetic.main.activity_big_image_viewer.* import javax.inject.Inject @@ -33,7 +32,6 @@ import javax.inject.Inject */ class BigImageViewerActivity : VectorBaseActivity() { @Inject lateinit var sessionHolder: ActiveSessionHolder - @Inject lateinit var colorProvider: ColorProvider override fun injectWith(injector: ScreenComponent) { injector.inject(this) diff --git a/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt b/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt index 8c8c8f22f1..191571959b 100644 --- a/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt +++ b/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt @@ -16,16 +16,17 @@ package im.vector.app.features.media -import android.content.Context import android.graphics.Color import android.net.Uri -import androidx.core.content.ContextCompat import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.UCropActivity import im.vector.app.R -import im.vector.app.features.themes.ThemeUtils +import im.vector.app.core.resources.ColorProvider -fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: Uri, toolbarTitle: String?): UCrop { +fun createUCropWithDefaultSettings(colorProvider: ColorProvider, + source: Uri, + destination: Uri, + toolbarTitle: String?): UCrop { return UCrop.of(source, destination) .withOptions( UCrop.Options() @@ -39,15 +40,15 @@ fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: U // Disable freestyle crop, usability was not easy // setFreeStyleCropEnabled(true) // Color used for toolbar icon and text - setToolbarColor(ThemeUtils.getColor(context, R.attr.riotx_background)) - setToolbarWidgetColor(ThemeUtils.getColor(context, R.attr.vctr_toolbar_primary_text_color)) + setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) + setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) // Background - setRootViewBackgroundColor(ThemeUtils.getColor(context, R.attr.riotx_background)) + setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) // Status bar color (pb in dark mode, icon of the status bar are dark) - setStatusBarColor(ThemeUtils.getColor(context, R.attr.riotx_header_panel_background)) + setStatusBarColor(colorProvider.getColorFromAttribute(R.attr.riotx_header_panel_background)) // Known issue: there is still orange color used by the lib // https://github.com/Yalantis/uCrop/issues/602 - setActiveControlsWidgetColor(ContextCompat.getColor(context, R.color.riotx_accent)) + setActiveControlsWidgetColor(colorProvider.getColor(R.color.riotx_accent)) // Hide the logo (does not work) setLogoColor(Color.TRANSPARENT) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index e12939f222..88b8a65a1c 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -16,32 +16,30 @@ package im.vector.app.features.roomdirectory.createroom -import android.app.Activity -import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.View -import androidx.core.net.toUri +import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.Success import com.airbnb.mvrx.activityViewModel 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.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.features.media.createUCropWithDefaultSettings +import im.vector.app.core.resources.ColorProvider import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel -import im.vector.lib.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.fragment_create_room.* import timber.log.Timber -import java.io.File import javax.inject.Inject class CreateRoomFragment @Inject constructor( - private val createRoomController: CreateRoomController + private val createRoomController: CreateRoomController, + colorProvider: ColorProvider ) : VectorBaseFragment(), CreateRoomController.Listener, GalleryOrCameraDialogHelper.Listener, @@ -50,7 +48,7 @@ class CreateRoomFragment @Inject constructor( private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private val viewModel: CreateRoomViewModel by activityViewModel() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) override fun getLayoutResId() = R.layout.fragment_create_room @@ -62,6 +60,11 @@ class CreateRoomFragment @Inject constructor( createRoomClose.debouncedClicks { sharedActionViewModel.post(RoomDirectorySharedAction.Back) } + viewModel.observeViewEvents { + when (it) { + CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed() + }.exhaustive + } } override fun onDestroyView() { @@ -83,25 +86,8 @@ class CreateRoomFragment @Inject constructor( 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) - } - - 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 -> - viewModel.handle(CreateRoomAction.SetAvatar(data?.let { UCrop.getOutput(it) })) - } - } + override fun onImageReady(uri: Uri?) { + viewModel.handle(CreateRoomAction.SetAvatar(uri)) } override fun onNameChange(newName: String) { @@ -134,8 +120,21 @@ class CreateRoomFragment @Inject constructor( } override fun onBackPressed(toolbarButton: Boolean): Boolean { - viewModel.handle(CreateRoomAction.Reset) - return false + return withState(viewModel) { + return@withState if (!it.isEmpty()) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_warning) + .setMessage(R.string.warning_room_not_created_yet) + .setPositiveButton(R.string.yes) { _, _ -> + viewModel.handle(CreateRoomAction.Reset) + } + .setNegativeButton(R.string.no, null) + .show() + true + } else { + false + } + } } override fun invalidate() = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt new file mode 100644 index 0000000000..4ff4ee4bdf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory.createroom + +import im.vector.app.core.platform.VectorViewEvents + +/** + * Transient events for room creation screen + */ +sealed class CreateRoomViewEvents : VectorViewEvents { + object Quit : CreateRoomViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index a0e0672d25..57af95b107 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.roomdirectory.createroom +import androidx.core.net.toFile import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext @@ -27,7 +28,6 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault @@ -45,7 +45,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState, private val session: Session, private val rawService: RawService -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -104,11 +104,16 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr private fun doReset() { setState { + // Delete temporary file with the avatar + avatarUri?.let { tryOrNull { it.toFile().delete() } } + CreateRoomViewState( isEncrypted = adminE2EByDefault, hsAdminHasDisabledE2E = !adminE2EByDefault ) } + + _viewEvents.post(CreateRoomViewEvents.Quit) } private fun setAvatar(action: CreateRoomAction.SetAvatar) = setState { copy(avatarUri = action.imageUri) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index 6fa8a104c3..433cc02cc9 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -30,4 +30,10 @@ data class CreateRoomViewState( val isEncrypted: Boolean = false, val hsAdminHasDisabledE2E: Boolean = false, val asyncCreateRoomRequest: Async = Uninitialized -) : MvRxState +) : MvRxState { + + /** + * Return true if there is not important input from user + */ + fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty() +} 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 62852562b0..80bb8813cf 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 @@ -27,4 +27,5 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() + object Cancel : RoomSettingsAction() } 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 4133d297ee..57521f7d80 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,19 +16,16 @@ package im.vector.app.features.roomprofile.settings -import android.app.Activity -import android.content.Intent +import android.net.Uri 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 @@ -37,19 +34,17 @@ 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.resources.ColorProvider 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 @@ -57,6 +52,7 @@ class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, + colorProvider: ColorProvider, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), @@ -66,7 +62,7 @@ class RoomSettingsFragment @Inject constructor( private val viewModel: RoomSettingsViewModel by fragmentViewModel() private val roomProfileArgs: RoomProfileArgs by args() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) override fun getLayoutResId() = R.layout.fragment_room_setting_generic @@ -83,7 +79,11 @@ class RoomSettingsFragment @Inject constructor( viewModel.observeViewEvents { when (it) { is RoomSettingsViewEvents.Failure -> showFailure(it.throwable) - is RoomSettingsViewEvents.Success -> showSuccess() + RoomSettingsViewEvents.Success -> showSuccess() + RoomSettingsViewEvents.GoBack -> { + ignoreChanges = true + vectorBaseActivity.onBackPressed() + } }.exhaustive } } @@ -178,12 +178,15 @@ class RoomSettingsFragment @Inject constructor( 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 onImageReady(uri: Uri?) { + uri ?: return + viewModel.handle( + RoomSettingsAction.SetAvatarAction( + RoomSettingsViewState.AvatarAction.UpdateAvatar( + newAvatarUri = uri, + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + ) + ) } override fun onAvatarDelete() { @@ -208,26 +211,6 @@ class RoomSettingsFragment @Inject constructor( 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 { @@ -239,8 +222,7 @@ class RoomSettingsFragment @Inject constructor( .setTitle(R.string.dialog_title_warning) .setMessage(R.string.warning_unsaved_change) .setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ -> - ignoreChanges = true - vectorBaseActivity.onBackPressed() + viewModel.handle(RoomSettingsAction.Cancel) } .setNegativeButton(R.string.cancel, null) .show() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt index 952ca791c9..83a768fb34 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt @@ -25,4 +25,5 @@ import im.vector.app.core.platform.VectorViewEvents sealed class RoomSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : RoomSettingsViewEvents() object Success : RoomSettingsViewEvents() + object GoBack : RoomSettingsViewEvents() } 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 6090209b1a..32d8f043c3 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 @@ -16,6 +16,7 @@ package im.vector.app.features.roomprofile.settings +import androidx.core.net.toFile import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -27,6 +28,7 @@ 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.extensions.tryOrNull 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 @@ -140,15 +142,35 @@ 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.SetAvatarAction -> handleSetAvatarAction(action) is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) } is RoomSettingsAction.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) } is RoomSettingsAction.Save -> saveSettings() + is RoomSettingsAction.Cancel -> cancel() }.exhaustive } + private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) { + deletePendingAvatar() + setState { copy(avatarAction = action.avatarAction) } + } + + private fun deletePendingAvatar() { + // Maybe delete the pending avatar + withState { + (it.avatarAction as? RoomSettingsViewState.AvatarAction.UpdateAvatar) + ?.let { tryOrNull { it.newAvatarUri.toFile().delete() } } + } + } + + private fun cancel() { + deletePendingAvatar() + + _viewEvents.post(RoomSettingsViewEvents.GoBack) + } + private fun saveSettings() = withState { state -> postLoading(true) @@ -188,6 +210,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: { postLoading(false) setState { copy(newHistoryVisibility = null) } + deletePendingAvatar() _viewEvents.post(RoomSettingsViewEvents.Success) }, { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index 3ac2cb9147..d8171bd30d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -18,8 +18,6 @@ package im.vector.app.features.settings -import android.app.Activity -import android.content.Intent import android.net.Uri import android.os.Bundle import android.text.Editable @@ -28,7 +26,6 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.preference.EditTextPreference import androidx.preference.Preference @@ -38,7 +35,6 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.cache.DiskCache import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout -import com.yalantis.ucrop.UCrop import im.vector.app.R import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.hideKeyboard @@ -48,14 +44,13 @@ import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.getSizeOfFiles import im.vector.app.core.utils.toast import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs -import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.app.features.workers.signout.SignOutUiWorker -import im.vector.lib.multipicker.entity.MultiPickerImageType import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -70,15 +65,18 @@ import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import java.io.File import java.util.UUID +import javax.inject.Inject -class VectorSettingsGeneralFragment : +class VectorSettingsGeneralFragment @Inject constructor( + colorProvider: ColorProvider +): VectorSettingsBaseFragment(), GalleryOrCameraDialogHelper.Listener { override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) private val mUserSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! @@ -277,18 +275,6 @@ class VectorSettingsGeneralFragment : session.integrationManagerService().removeListener(integrationServiceListener) } - 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 refreshIntegrationManagerSettings() { val integrationAllowed = session.integrationManagerService().isIntegrationEnabled() (findPreference(VectorPreferences.SETTINGS_ALLOW_INTEGRATIONS_KEY))!!.let { @@ -308,15 +294,7 @@ class VectorSettingsGeneralFragment : } } - 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 fun onAvatarCropped(uri: Uri?) { + override fun onImageReady(uri: Uri?) { if (uri != null) { uploadAvatar(uri) } else { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a78e295a5a..dd461123cc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1840,12 +1840,14 @@ "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." "An error occurred while retrieving the attachment." + "Add image from" "File" "Contact" "Camera" "Audio" "Gallery" "Sticker" + Rotate and crop Couldn\'t handle share data MEDIA @@ -2630,6 +2632,7 @@ The link was malformed + The room is not yet created. Cancel the room creation? There are unsaved changes. Discard the changes? Discard changes