From 05e848244e472ec3b82bcd2998273781cdc9d660 Mon Sep 17 00:00:00 2001 From: onurays Date: Tue, 9 Jun 2020 11:11:17 +0300 Subject: [PATCH 01/29] Room name and topic fields added to form. --- CHANGES.md | 1 + .../settings/RoomSettingsController.kt | 25 +++++++++++++++++++ .../settings/RoomSettingsFragment.kt | 8 ++++++ .../settings/RoomSettingsViewModel.kt | 2 ++ .../settings/RoomSettingsViewState.kt | 4 ++- vector/src/main/res/values/strings.xml | 6 ++++- 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3f2657a65c..337e61afb4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: - "Add Matrix app" menu is now always visible (#1495) - Handle `/op`, `/deop`, and `/nick` commands (#12) - Prioritising Recovery key over Recovery passphrase (#1463) + - Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455) Bugfix 🐛: - Fix dark theme issue on login screen (#1097) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index fdd127e68b..3bf1daba77 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -22,6 +22,7 @@ import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.form.formEditTextItem import javax.inject.Inject // TODO Add other feature here (waiting for design) @@ -32,6 +33,8 @@ class RoomSettingsController @Inject constructor( interface Callback { fun onEnableEncryptionClicked() + fun onNameChanged(name: String) + fun onTopicChanged(topic: String) } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -68,5 +71,27 @@ class RoomSettingsController @Inject constructor( action = { callback?.onEnableEncryptionClicked() } ) } + + formEditTextItem { + id("name") + /*enabled(enableFormElement)*/ + value(roomSummary.displayName) + hint(stringProvider.getString(R.string.room_settings_name_hint)) + + onTextChange { text -> + callback?.onNameChanged(text) + } + } + + formEditTextItem { + id("topic") + /*enabled(enableFormElement)*/ + value(roomSummary.topic) + hint(stringProvider.getString(R.string.room_settings_topic_hint)) + + onTextChange { text -> + callback?.onTopicChanged(text) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index d2e06f30e7..072ada6179 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -82,6 +82,14 @@ class RoomSettingsFragment @Inject constructor( .show() } + override fun onNameChanged(name: String) { + viewModel.handle(RoomSettingsAction.SetRoomName(name)) + } + + override fun onTopicChanged(topic: String) { + viewModel.handle(RoomSettingsAction.SetRoomTopic(topic)) + } + private fun renderRoomSummary(state: RoomSettingsViewState) { waiting_view.isVisible = state.isLoading diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index f1dee87005..09f0157acc 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -62,6 +62,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomSettingsAction) { when (action) { is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() + is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } + is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index d68ed6853e..5c2e984192 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -25,7 +25,9 @@ import im.vector.riotx.features.roomprofile.RoomProfileArgs data class RoomSettingsViewState( val roomId: String, val roomSummary: Async = Uninitialized, - val isLoading: Boolean = false + val isLoading: Boolean = false, + val newName: String? = null, + val newTopic: String? = null ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5ac50b2484..98d8c3323e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2496,4 +2496,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Save your Security Key Store your Security Key somewhere safe, like a password manager or a safe. - \ No newline at end of file + + Room Name + Topic + + From bfebaa5c6c31a3d37e44e355856ae95396099f29 Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 10 Jun 2020 10:41:24 +0300 Subject: [PATCH 02/29] Show/hide save action button dynamically. --- .../settings/RoomSettingsAction.kt | 1 + .../settings/RoomSettingsController.kt | 44 +++++++++---------- .../settings/RoomSettingsFragment.kt | 20 +++++++++ .../settings/RoomSettingsViewModel.kt | 31 +++++++++++-- .../settings/RoomSettingsViewState.kt | 3 +- .../main/res/menu/vector_room_settings.xml | 9 ++++ 6 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 vector/src/main/res/menu/vector_room_settings.xml diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index 3c1b10cf8e..1108c6813b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -23,4 +23,5 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomAvatar(val newAvatarUrl: String) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() + object Save : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 3bf1daba77..d0ee589da5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -52,6 +52,28 @@ class RoomSettingsController @Inject constructor( stringProvider.getString(R.string.settings) ) + formEditTextItem { + id("name") + /*enabled(enableFormElement)*/ + value(data.newName ?: roomSummary.displayName) + hint(stringProvider.getString(R.string.room_settings_name_hint)) + + onTextChange { text -> + callback?.onNameChanged(text) + } + } + + formEditTextItem { + id("topic") + /*enabled(enableFormElement)*/ + value(data.newTopic ?: roomSummary.topic) + hint(stringProvider.getString(R.string.room_settings_topic_hint)) + + onTextChange { text -> + callback?.onTopicChanged(text) + } + } + if (roomSummary.isEncrypted) { buildProfileAction( id = "encryption", @@ -71,27 +93,5 @@ class RoomSettingsController @Inject constructor( action = { callback?.onEnableEncryptionClicked() } ) } - - formEditTextItem { - id("name") - /*enabled(enableFormElement)*/ - value(roomSummary.displayName) - hint(stringProvider.getString(R.string.room_settings_name_hint)) - - onTextChange { text -> - callback?.onNameChanged(text) - } - } - - formEditTextItem { - id("topic") - /*enabled(enableFormElement)*/ - value(roomSummary.topic) - hint(stringProvider.getString(R.string.room_settings_topic_hint)) - - onTextChange { text -> - callback?.onTopicChanged(text) - } - } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 072ada6179..12af2be689 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -17,6 +17,8 @@ package im.vector.riotx.features.roomprofile.settings import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -46,6 +48,8 @@ class RoomSettingsFragment @Inject constructor( override fun getLayoutResId() = R.layout.fragment_room_setting_generic + override fun getMenuRes() = R.menu.vector_room_settings + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) controller.callback = this @@ -66,6 +70,20 @@ class RoomSettingsFragment @Inject constructor( super.onDestroyView() } + override fun onPrepareOptionsMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction + } + super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.roomSettingsSaveAction) { + viewModel.handle(RoomSettingsAction.Save) + } + return super.onOptionsItemSelected(item) + } + override fun invalidate() = withState(viewModel) { viewState -> controller.setData(viewState) renderRoomSummary(viewState) @@ -97,5 +115,7 @@ class RoomSettingsFragment @Inject constructor( roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) } + + invalidateOptionsMenu() } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 09f0157acc..d5283a240b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -55,18 +55,43 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: room.rx().liveRoomSummary() .unwrap() .execute { async -> - copy(roomSummary = async) + val roomSummary = async.invoke() + copy( + roomSummary = async, + newName = roomSummary?.displayName, + newTopic = roomSummary?.topic + ) } } override fun handle(action: RoomSettingsAction) { when (action) { is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() - is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } - is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } + is RoomSettingsAction.SetRoomName -> setState { + copy( + newName = action.newName, + showSaveAction = shouldShowSaveAction(this) + ) + } + is RoomSettingsAction.SetRoomTopic -> setState { + copy( + newTopic = action.newTopic, + showSaveAction = shouldShowSaveAction(this) + ) + } + is RoomSettingsAction.Save -> saveSettings() } } + private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean { + val summary = state.roomSummary.invoke() + return summary?.displayName != state.newName || + summary?.topic != state.newTopic + } + + private fun saveSettings() { + } + private fun handleEnableEncryption() { setState { copy(isLoading = true) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 5c2e984192..0b288fd829 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -27,7 +27,8 @@ data class RoomSettingsViewState( val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, val newName: String? = null, - val newTopic: String? = null + val newTopic: String? = null, + val showSaveAction: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/menu/vector_room_settings.xml b/vector/src/main/res/menu/vector_room_settings.xml new file mode 100644 index 0000000000..f8a569f062 --- /dev/null +++ b/vector/src/main/res/menu/vector_room_settings.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file From a6e4a328b35fe85637d05fb3046d6527f9657c7e Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 10 Jun 2020 15:54:10 +0300 Subject: [PATCH 03/29] Chain all operations to save settings. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 8 +++ .../api/session/room/state/StateService.kt | 5 ++ .../session/room/state/DefaultStateService.kt | 9 ++++ .../settings/RoomSettingsFragment.kt | 6 +++ .../settings/RoomSettingsViewEvents.kt | 1 + .../settings/RoomSettingsViewModel.kt | 49 ++++++++++++++----- vector/src/main/res/values/strings.xml | 1 + 7 files changed, 68 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 5cc9d1fc00..20f794860e 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -101,6 +101,14 @@ class RxRoom(private val room: Room) { fun invite(userId: String, reason: String? = null): Completable = completableBuilder { room.invite(userId, reason, it) } + + fun updateTopic(topic: String): Completable = completableBuilder { + room.updateTopic(topic, it) + } + + fun updateName(name: String): Completable = completableBuilder { + room.updateName(name, it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 827ce50e13..91a193fba7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -31,6 +31,11 @@ interface StateService { */ fun updateTopic(topic: String, callback: MatrixCallback): Cancelable + /** + * Update the name of the room + */ + fun updateName(name: String, 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/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 6646f08c2d..ed077d76b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -84,4 +84,13 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private stateKey = null ) } + + override fun updateName(name: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_NAME, + body = mapOf("name" to name), + callback = callback, + stateKey = null + ) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 12af2be689..bb97409e74 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -31,6 +31,7 @@ import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs import kotlinx.android.synthetic.main.fragment_room_setting_generic.* @@ -61,10 +62,15 @@ class RoomSettingsFragment @Inject constructor( viewModel.observeViewEvents { when (it) { is RoomSettingsViewEvents.Failure -> showFailure(it.throwable) + is RoomSettingsViewEvents.Success -> showSuccess() }.exhaustive } } + private fun showSuccess() { + activity?.toast(R.string.room_settings_save_success) + } + override fun onDestroyView() { recyclerView.cleanup() super.onDestroyView() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt index 4856a935a8..c30a5ff9c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt @@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorViewEvents */ sealed class RoomSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : RoomSettingsViewEvents() + object Success : RoomSettingsViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index d5283a240b..deca6858e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -26,6 +26,8 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Completable +import io.reactivex.Observable class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, private val session: Session) @@ -89,28 +91,53 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: summary?.topic != state.newTopic } - private fun saveSettings() { + private fun saveSettings() = withState { state -> + postLoading(true) + + val operationList = mutableListOf() + + val summary = state.roomSummary.invoke() + + if (summary?.displayName != state.newName) { + operationList.add(room.rx().updateName(state.newName ?: "")) + } + if (summary?.topic != state.newTopic) { + operationList.add(room.rx().updateTopic(state.newTopic ?: "")) + } + + Observable + .fromIterable(operationList) + .flatMapCompletable { it } + .subscribe( + { + postLoading(false) + _viewEvents.post(RoomSettingsViewEvents.Success) + }, + { + postLoading(false) + _viewEvents.post(RoomSettingsViewEvents.Failure(it)) + } + ) } private fun handleEnableEncryption() { - setState { - copy(isLoading = true) - } + postLoading(true) room.enableEncryption(callback = object : MatrixCallback { override fun onFailure(failure: Throwable) { - setState { - copy(isLoading = false) - } - + postLoading(false) _viewEvents.post(RoomSettingsViewEvents.Failure(failure)) } override fun onSuccess(data: Unit) { - setState { - copy(isLoading = false) - } + postLoading(false) } }) } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 98d8c3323e..3b3af55d1f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2499,5 +2499,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Room Name Topic + You changed room settings successfully From 52eec061102e937286123a72b7fd28b74a1af7aa Mon Sep 17 00:00:00 2001 From: onurays Date: Fri, 12 Jun 2020 16:39:22 +0300 Subject: [PATCH 04/29] Updating room avatar is implemented. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 5 + .../api/session/room/state/StateService.kt | 6 + .../internal/session/SessionComponent.kt | 3 + .../session/content/UploadAvatarWorker.kt | 128 ++++++++++++++++++ .../session/room/state/DefaultStateService.kt | 55 +++++++- .../core/epoxy/profiles/ProfileActionItem.kt | 15 ++ .../epoxy/profiles/ProfileItemExtensions.kt | 8 +- .../settings/RoomSettingsAction.kt | 3 +- .../settings/RoomSettingsController.kt | 16 +++ .../settings/RoomSettingsFragment.kt | 44 ++++-- .../settings/RoomSettingsViewModel.kt | 30 ++-- .../settings/RoomSettingsViewState.kt | 2 + 12 files changed, 293 insertions(+), 22 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 20f794860e..a6c135c13f 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -16,6 +16,7 @@ package im.vector.matrix.rx +import android.net.Uri import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.Room @@ -109,6 +110,10 @@ class RxRoom(private val room: Room) { fun updateName(name: String): Completable = completableBuilder { room.updateName(name, it) } + + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { + room.updateAvatar(avatarUri, fileName, it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 91a193fba7..64066640b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.state +import android.net.Uri import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.query.QueryStringValue @@ -36,6 +37,11 @@ interface StateService { */ fun updateName(name: String, callback: MatrixCallback): Cancelable + /** + * Update the avatar of the room + */ + fun updateAvatar(avatarUri: Uri, fileName: String, 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/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index b95595ed23..4dc9fc0305 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.session.account.AccountModule import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.call.CallModule import im.vector.matrix.android.internal.session.content.ContentModule +import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker @@ -117,6 +118,8 @@ internal interface SessionComponent { fun inject(worker: UploadContentWorker) + fun inject(worker: UploadAvatarWorker) + fun inject(worker: SyncWorker) fun inject(worker: AddHttpPusherWorker) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt new file mode 100644 index 0000000000..8becdfa05c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt @@ -0,0 +1,128 @@ +/* + * 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.matrix.android.internal.session.content + +import android.content.Context +import android.net.Uri +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.worker.SessionWorkerParams +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class UploadAvatarWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val queryUri: Uri, + val fileName: String, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @JsonClass(generateAdapter = true) + internal data class OutputParams( + override val sessionId: String, + val imageUrl: String? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var fileUploader: FileUploader + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + Timber.v("Starting upload media work with params $params") + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + // Just defensive code to ensure that we never have an uncaught exception that could break the queue + return try { + internalDoWork(params) + } catch (failure: Throwable) { + Timber.e(failure) + handleFailure(params, failure) + } + } + + private suspend fun internalDoWork(params: Params): Result { + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + try { + val inputStream = context.contentResolver.openInputStream(params.queryUri) + ?: return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = "Cannot openInputStream for file: " + params.queryUri.toString() + ) + ) + ) + + inputStream.use { + return try { + Timber.v("## UploadAvatarWorker - Uploading avatar...") + val response = fileUploader.uploadByteArray(inputStream.readBytes(), params.fileName, "image/jpeg") + Timber.v("## UploadAvatarWorker - Uploadeded avatar: ${response.contentUri}") + handleSuccess(params, response.contentUri) + } catch (t: Throwable) { + Timber.e(t, "## UploadAvatarWorker - Uploading avatar failed...") + handleFailure(params, t) + } + } + } catch (e: Exception) { + Timber.e(e) + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = e.localizedMessage + ) + ) + ) + } + } + + private fun handleFailure(params: Params, failure: Throwable): Result { + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = failure.localizedMessage + ) + ) + ) + } + + private fun handleSuccess(params: Params, imageUrl: String): Result { + Timber.v("handleSuccess $imageUrl, work is stopped $isStopped") + + val sendParams = OutputParams(params.sessionId, imageUrl, params.lastFailureMessage) + return Result.success(WorkerParamsFactory.toData(sendParams)) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index ed077d76b9..ebfc3398b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -16,7 +16,10 @@ package im.vector.matrix.android.internal.session.room.state +import android.net.Uri import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -25,15 +28,29 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, private val taskExecutor: TaskExecutor, - private val sendStateTask: SendStateTask + private val sendStateTask: SendStateTask, + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : StateService { @AssistedInject.Factory @@ -93,4 +110,40 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private stateKey = null ) } + + override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { + val cancelableBag = CancelableBag() + val workerParams = UploadAvatarWorker.Params(sessionId, avatarUri, fileName) + val workerData = WorkerParamsFactory.toData(workerParams) + + val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + + workManagerProvider.workManager + .beginUniqueWork("${roomId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork) + .enqueue() + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id)) + + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id) + .observeForever { info -> + if (info != null && info.state.isFinished) { + val result = WorkerParamsFactory.fromData(info.outputData) + cancelableBag.add( + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to result?.imageUrl!!), + callback = callback, + stateKey = null + ) + ) + } + } + } + return cancelableBag + } } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt index a015358d8b..80b78d0d70 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -25,10 +25,12 @@ import androidx.core.view.isVisible import androidx.core.widget.ImageViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.themes.ThemeUtils @EpoxyModelClass(layout = R.layout.item_profile_action) @@ -51,6 +53,12 @@ abstract class ProfileActionItem : VectorEpoxyModel() @EpoxyAttribute var accessoryRes: Int = 0 + @EpoxyAttribute + var accessoryMatrixItem: MatrixItem? = null + + @EpoxyAttribute + var avatarRenderer: AvatarRenderer? = null + @EpoxyAttribute var editable: Boolean = true @@ -93,6 +101,13 @@ abstract class ProfileActionItem : VectorEpoxyModel() holder.secondaryAccessory.isVisible = false } + if (accessoryMatrixItem != null) { + avatarRenderer?.render(accessoryMatrixItem!!, holder.secondaryAccessory) + holder.secondaryAccessory.isVisible = true + } else { + holder.secondaryAccessory.isVisible = false + } + if (editableRes != 0 && editable) { val tintColorSecondary = if (destructive) { tintColor diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt index 693efc5418..1f3aff0de6 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt @@ -19,8 +19,10 @@ package im.vector.riotx.core.epoxy.profiles import androidx.annotation.DrawableRes import com.airbnb.epoxy.EpoxyController +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.core.epoxy.ClickListener import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.features.home.AvatarRenderer fun EpoxyController.buildProfileSection(title: String) { profileSectionItem { @@ -41,7 +43,9 @@ fun EpoxyController.buildProfileAction( destructive: Boolean = false, divider: Boolean = true, action: ClickListener? = null, - @DrawableRes accessory: Int = 0 + @DrawableRes accessory: Int = 0, + accessoryMatrixItem: MatrixItem? = null, + avatarRenderer: AvatarRenderer? = null ) { profileActionItem { iconRes(icon) @@ -53,6 +57,8 @@ fun EpoxyController.buildProfileAction( destructive(destructive) title(title) accessoryRes(accessory) + accessoryMatrixItem(accessoryMatrixItem) + avatarRenderer(avatarRenderer) listener { _ -> action?.invoke() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index 1108c6813b..f740c22d6d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -17,11 +17,12 @@ package im.vector.riotx.features.roomprofile.settings import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.multipicker.entity.MultiPickerImageType sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() - data class SetRoomAvatar(val newAvatarUrl: String) : RoomSettingsAction() + data class SetRoomAvatar(val image: MultiPickerImageType) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index d0ee589da5..bb87c256e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -17,16 +17,19 @@ package im.vector.riotx.features.roomprofile.settings import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.form.formEditTextItem +import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject // TODO Add other feature here (waiting for design) class RoomSettingsController @Inject constructor( + private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, colorProvider: ColorProvider ) : TypedEpoxyController() { @@ -35,6 +38,7 @@ class RoomSettingsController @Inject constructor( fun onEnableEncryptionClicked() fun onNameChanged(name: String) fun onTopicChanged(topic: String) + fun onPhotoClicked() } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -74,6 +78,18 @@ class RoomSettingsController @Inject constructor( } } + buildProfileAction( + id = "photo", + title = stringProvider.getString(R.string.room_settings_room_photo), + subtitle = "", + dividerColor = dividerColor, + divider = true, + editable = true, + accessoryMatrixItem = roomSummary.toMatrixItem(), + avatarRenderer = avatarRenderer, + action = { callback?.onPhotoClicked() } + ) + if (roomSummary.isEncrypted) { buildProfileAction( id = "encryption", diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index bb97409e74..3733b311ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -16,6 +16,8 @@ package im.vector.riotx.features.roomprofile.settings +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -34,6 +36,8 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs +import im.vector.riotx.multipicker.MultiPicker +import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import javax.inject.Inject @@ -62,7 +66,7 @@ class RoomSettingsFragment @Inject constructor( viewModel.observeViewEvents { when (it) { is RoomSettingsViewEvents.Failure -> showFailure(it.throwable) - is RoomSettingsViewEvents.Success -> showSuccess() + is RoomSettingsViewEvents.Success -> showSuccess() }.exhaustive } } @@ -95,6 +99,17 @@ class RoomSettingsFragment @Inject constructor( renderRoomSummary(viewState) } + private fun renderRoomSummary(state: RoomSettingsViewState) { + waiting_view.isVisible = state.isLoading + + state.roomSummary()?.let { + roomSettingsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) + } + + invalidateOptionsMenu() + } + override fun onEnableEncryptionClicked() { AlertDialog.Builder(requireActivity()) .setTitle(R.string.room_settings_enable_encryption_dialog_title) @@ -114,14 +129,27 @@ class RoomSettingsFragment @Inject constructor( viewModel.handle(RoomSettingsAction.SetRoomTopic(topic)) } - private fun renderRoomSummary(state: RoomSettingsViewState) { - waiting_view.isVisible = state.isLoading + override fun onPhotoClicked() { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } - state.roomSummary()?.let { - roomSettingsToolbarTitleView.text = it.displayName - avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) + private fun onRoomPhotoSelected(selectedImage: MultiPickerImageType) { + viewModel.handle(RoomSettingsAction.SetRoomAvatar(selectedImage)) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + MultiPicker.REQUEST_CODE_PICK_IMAGE -> { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(requireContext(), requestCode, resultCode, data) + .firstOrNull()?.let { + onRoomPhotoSelected(it) + } + } + } } - - invalidateOptionsMenu() + super.onActivityResult(requestCode, resultCode, data) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index deca6858e2..1b0d2da749 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -28,6 +28,7 @@ import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel import io.reactivex.Completable import io.reactivex.Observable +import java.util.UUID class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, private val session: Session) @@ -69,17 +70,17 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomSettingsAction) { when (action) { is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() - is RoomSettingsAction.SetRoomName -> setState { - copy( - newName = action.newName, - showSaveAction = shouldShowSaveAction(this) - ) + is RoomSettingsAction.SetRoomName -> { + setState { copy(newName = action.newName) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } } - is RoomSettingsAction.SetRoomTopic -> setState { - copy( - newTopic = action.newTopic, - showSaveAction = shouldShowSaveAction(this) - ) + is RoomSettingsAction.SetRoomTopic -> { + setState { copy(newTopic = action.newTopic) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } + } + is RoomSettingsAction.SetRoomAvatar -> { + setState { copy(newAvatar = action.image) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } } is RoomSettingsAction.Save -> saveSettings() } @@ -88,7 +89,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean { val summary = state.roomSummary.invoke() return summary?.displayName != state.newName || - summary?.topic != state.newTopic + summary?.topic != state.newTopic || + state.newAvatar != null } private fun saveSettings() = withState { state -> @@ -105,12 +107,18 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } + if (state.newAvatar != null) { + operationList.add(room.rx().updateAvatar(state.newAvatar.contentUri, state.newAvatar.displayName ?: UUID.randomUUID().toString())) + } + Observable .fromIterable(operationList) .flatMapCompletable { it } .subscribe( { postLoading(false) + setState { copy(newAvatar = null) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } _viewEvents.post(RoomSettingsViewEvents.Success) }, { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 0b288fd829..4ac870028c 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.features.roomprofile.RoomProfileArgs +import im.vector.riotx.multipicker.entity.MultiPickerImageType data class RoomSettingsViewState( val roomId: String, @@ -28,6 +29,7 @@ data class RoomSettingsViewState( val isLoading: Boolean = false, val newName: String? = null, val newTopic: String? = null, + val newAvatar: MultiPickerImageType? = null, val showSaveAction: Boolean = false ) : MvRxState { From e1a12f4c77661a392b33efd988af6a947fd8d256 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 15 Jun 2020 00:21:49 +0300 Subject: [PATCH 05/29] Show current history readability. --- .../settings/RoomSettingsController.kt | 29 +++++++++++++++++++ .../settings/RoomSettingsFragment.kt | 4 +++ .../settings/RoomSettingsViewModel.kt | 2 ++ .../settings/RoomSettingsViewState.kt | 2 ++ 4 files changed, 37 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index bb87c256e8..41fff7e988 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -17,6 +17,10 @@ package im.vector.riotx.features.roomprofile.settings import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction @@ -39,6 +43,7 @@ class RoomSettingsController @Inject constructor( fun onNameChanged(name: String) fun onTopicChanged(topic: String) fun onPhotoClicked() + fun onHistoryVisibilityClicked() } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -52,6 +57,8 @@ class RoomSettingsController @Inject constructor( override fun buildModels(data: RoomSettingsViewState?) { val roomSummary = data?.roomSummary?.invoke() ?: return + val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" + buildProfileSection( stringProvider.getString(R.string.settings) ) @@ -78,6 +85,16 @@ class RoomSettingsController @Inject constructor( } } + buildProfileAction( + id = "historyReadability", + title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), + subtitle = historyVisibility.toString(), + dividerColor = dividerColor, + divider = false, + editable = true, + action = { callback?.onHistoryVisibilityClicked() } + ) + buildProfileAction( id = "photo", title = stringProvider.getString(R.string.room_settings_room_photo), @@ -110,4 +127,16 @@ class RoomSettingsController @Inject constructor( ) } } + + private fun formatRoomHistoryVisibilityEvent(event: Event): String? { + val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null + + val formattedVisibility = when (historyVisibility) { + RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) + RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) + RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) + RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) + } + return formattedVisibility + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 3733b311ac..c75a4bda71 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -152,4 +152,8 @@ class RoomSettingsFragment @Inject constructor( } super.onActivityResult(requestCode, resultCode, data) } + + override fun onHistoryVisibilityClicked() { + + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 1b0d2da749..db46b6f9d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -23,6 +23,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel @@ -60,6 +61,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: .execute { async -> val roomSummary = async.invoke() copy( + historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY), roomSummary = async, newName = roomSummary?.displayName, newTopic = roomSummary?.topic diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 4ac870028c..3a953f2d18 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -19,12 +19,14 @@ package im.vector.riotx.features.roomprofile.settings import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.features.roomprofile.RoomProfileArgs import im.vector.riotx.multipicker.entity.MultiPickerImageType data class RoomSettingsViewState( val roomId: String, + val historyVisibilityEvent: Event? = null, val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, val newName: String? = null, From f5790e5dc277a8bb873ba23fedd3e1227c1f2a1c Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 22 Jun 2020 10:29:34 +0300 Subject: [PATCH 06/29] Implementation of room history readability. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 4 ++ .../api/session/room/state/StateService.kt | 5 +++ .../session/room/state/DefaultStateService.kt | 9 +++++ .../settings/RoomSettingsAction.kt | 2 + .../settings/RoomSettingsController.kt | 7 +++- .../settings/RoomSettingsFragment.kt | 40 +++++++++++++++++-- .../settings/RoomSettingsViewModel.kt | 22 +++++++--- .../settings/RoomSettingsViewState.kt | 2 + 8 files changed, 81 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index a6c135c13f..876f01de02 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -111,6 +111,10 @@ class RxRoom(private val room: Room) { room.updateName(name, it) } + fun updateHistoryReadability(readability: String) = completableBuilder { + room.updateHistoryReadability(readability, it) + } + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { room.updateAvatar(avatarUri, fileName, it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 64066640b4..9bfaff00ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -37,6 +37,11 @@ interface StateService { */ fun updateName(name: String, callback: MatrixCallback): Cancelable + /** + * Update the history readability of the room + */ + fun updateHistoryReadability(readability: String, callback: MatrixCallback): Cancelable + /** * Update the avatar of the room */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index ebfc3398b2..77fcd9a9bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -111,6 +111,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } + override fun updateHistoryReadability(readability: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + body = mapOf("history_visibility" to readability), + callback = callback, + stateKey = null + ) + } + override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { val cancelableBag = CancelableBag() val workerParams = UploadAvatarWorker.Params(sessionId, avatarUri, fileName) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index f740c22d6d..aadb6584a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.roomprofile.settings +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.multipicker.entity.MultiPickerImageType @@ -23,6 +24,7 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomAvatar(val image: MultiPickerImageType) : RoomSettingsAction() + data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 41fff7e988..f29c6cc65e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -58,6 +58,7 @@ class RoomSettingsController @Inject constructor( val roomSummary = data?.roomSummary?.invoke() ?: return val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" + val newHistoryVisibility = data.newHistoryVisibility?.let { formatRoomHistoryVisibility(it) } buildProfileSection( stringProvider.getString(R.string.settings) @@ -88,7 +89,7 @@ class RoomSettingsController @Inject constructor( buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), - subtitle = historyVisibility.toString(), + subtitle = newHistoryVisibility ?: historyVisibility, dividerColor = dividerColor, divider = false, editable = true, @@ -131,6 +132,10 @@ class RoomSettingsController @Inject constructor( private fun formatRoomHistoryVisibilityEvent(event: Event): String? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null + return formatRoomHistoryVisibility(historyVisibility) + } + + private fun formatRoomHistoryVisibility(historyVisibility: RoomHistoryVisibility): String { val formattedVisibility = when (historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index c75a4bda71..8e25646637 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -27,12 +27,16 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs @@ -45,7 +49,8 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider ) : VectorBaseFragment(), RoomSettingsController.Callback { private val viewModel: RoomSettingsViewModel by fragmentViewModel() @@ -153,7 +158,36 @@ class RoomSettingsFragment @Inject constructor( super.onActivityResult(requestCode, resultCode, data) } - override fun onHistoryVisibilityClicked() { - + override fun onHistoryVisibilityClicked() = withState(viewModel) { state -> + val historyVisibilities = arrayOf( + RoomHistoryVisibility.SHARED, + RoomHistoryVisibility.INVITED, + RoomHistoryVisibility.JOINED, + RoomHistoryVisibility.WORLD_READABLE + ) + val currentHistoryVisibility = + state.newHistoryVisibility ?: state.historyVisibilityEvent?.getClearContent().toModel()?.historyVisibility + val currentHistoryVisibilityIndex = historyVisibilities.indexOf(currentHistoryVisibility) + + AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.room_settings_room_read_history_rules_pref_title) + setSingleChoiceItems(historyVisibilities.map { formatHistoryVisibility(it) }.toTypedArray(), currentHistoryVisibilityIndex) { dialog, which -> + if (which != currentHistoryVisibilityIndex) { + viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(historyVisibilities[which])) + } + dialog.cancel() + } + show() + } + return@withState + } + + private fun formatHistoryVisibility(historyVisibility: RoomHistoryVisibility): String { + return when (historyVisibility) { + RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) + RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) + RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) + RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index db46b6f9d7..26da0bb478 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -29,6 +29,7 @@ import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel import io.reactivex.Completable import io.reactivex.Observable +import java.util.Locale import java.util.UUID class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, @@ -71,20 +72,24 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomSettingsAction) { when (action) { - is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() - is RoomSettingsAction.SetRoomName -> { + is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() + is RoomSettingsAction.SetRoomName -> { setState { copy(newName = action.newName) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } } - is RoomSettingsAction.SetRoomTopic -> { + is RoomSettingsAction.SetRoomTopic -> { setState { copy(newTopic = action.newTopic) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } } - is RoomSettingsAction.SetRoomAvatar -> { + is RoomSettingsAction.SetRoomAvatar -> { setState { copy(newAvatar = action.image) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } } - is RoomSettingsAction.Save -> saveSettings() + is RoomSettingsAction.SetRoomHistoryVisibility -> { + setState { copy(newHistoryVisibility = action.visibility) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } + } + is RoomSettingsAction.Save -> saveSettings() } } @@ -92,6 +97,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = state.roomSummary.invoke() return summary?.displayName != state.newName || summary?.topic != state.newTopic || + state.newHistoryVisibility != null || state.newAvatar != null } @@ -109,6 +115,10 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } + if (state.newHistoryVisibility != null) { + operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility.name.toLowerCase(Locale.ROOT))) + } + if (state.newAvatar != null) { operationList.add(room.rx().updateAvatar(state.newAvatar.contentUri, state.newAvatar.displayName ?: UUID.randomUUID().toString())) } @@ -119,7 +129,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: .subscribe( { postLoading(false) - setState { copy(newAvatar = null) } + setState { copy(newAvatar = null, newHistoryVisibility = null) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } _viewEvents.post(RoomSettingsViewEvents.Success) }, diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 3a953f2d18..e456f753c7 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.features.roomprofile.RoomProfileArgs import im.vector.riotx.multipicker.entity.MultiPickerImageType @@ -32,6 +33,7 @@ data class RoomSettingsViewState( val newName: String? = null, val newTopic: String? = null, val newAvatar: MultiPickerImageType? = null, + val newHistoryVisibility: RoomHistoryVisibility? = null, val showSaveAction: Boolean = false ) : MvRxState { From 762dd1d0a51b6d3c2428bee2283ca542385111d0 Mon Sep 17 00:00:00 2001 From: onurays Date: Tue, 23 Jun 2020 10:01:04 +0300 Subject: [PATCH 07/29] Implementation of canonical alias. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 4 + .../api/session/room/state/StateService.kt | 5 ++ .../session/room/state/DefaultStateService.kt | 9 ++ .../form/FormEditTextWithButtonItem.kt | 89 +++++++++++++++++++ .../settings/RoomSettingsAction.kt | 1 + .../settings/RoomSettingsController.kt | 12 +++ .../settings/RoomSettingsFragment.kt | 4 + .../settings/RoomSettingsViewModel.kt | 9 ++ .../settings/RoomSettingsViewState.kt | 1 + .../item_form_text_input_with_button.xml | 50 +++++++++++ 10 files changed, 184 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt create mode 100644 vector/src/main/res/layout/item_form_text_input_with_button.xml diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 876f01de02..e1a4e3ac7b 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -111,6 +111,10 @@ class RxRoom(private val room: Room) { room.updateName(name, it) } + fun updateCanonicalAlias(alias: String): Completable = completableBuilder { + room.updateCanonicalAlias(alias, it) + } + fun updateHistoryReadability(readability: String) = completableBuilder { room.updateHistoryReadability(readability, it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 9bfaff00ca..f22e3d59f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -37,6 +37,11 @@ interface StateService { */ fun updateName(name: String, callback: MatrixCallback): Cancelable + /** + * Update the canonical alias of the room + */ + fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable + /** * Update the history readability of the room */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 77fcd9a9bb..7cc061d0ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -111,6 +111,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } + override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, + body = mapOf("alias" to alias), + callback = callback, + stateKey = null + ) + } + override fun updateHistoryReadability(readability: String, callback: MatrixCallback): Cancelable { return sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, diff --git a/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt new file mode 100644 index 0000000000..0650c0f55c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt @@ -0,0 +1,89 @@ +/* + * 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.riotx.features.form + +import android.text.Editable +import android.view.View +import androidx.appcompat.widget.AppCompatButton +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.platform.SimpleTextWatcher + +@EpoxyModelClass(layout = R.layout.item_form_text_input_with_button) +abstract class FormEditTextWithButtonItem : VectorEpoxyModel() { + + @EpoxyAttribute + var hint: String? = null + + @EpoxyAttribute + var value: String? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var buttonText: String? = null + + @EpoxyAttribute + var onTextChange: ((String) -> Unit)? = null + + @EpoxyAttribute + var onButtonClicked: ((View) -> Unit)? = null + + private val onTextChangeListener = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + onTextChange?.invoke(s.toString()) + } + } + + override fun bind(holder: Holder) { + holder.textInputLayout.isEnabled = enabled + holder.textInputLayout.hint = hint + + // Update only if text is different + if (holder.textInputEditText.text.toString() != value) { + holder.textInputEditText.setText(value) + } + holder.textInputEditText.isEnabled = enabled + + holder.textInputEditText.addTextChangedListener(onTextChangeListener) + + holder.textInputButton.text = buttonText + + holder.textInputButton.setOnClickListener(onButtonClicked) + } + + override fun shouldSaveViewState(): Boolean { + return false + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.textInputEditText.removeTextChangedListener(onTextChangeListener) + } + + class Holder : VectorEpoxyHolder() { + val textInputLayout by bind(R.id.formTextInputTextInputLayout) + val textInputEditText by bind(R.id.formTextInputTextInputEditText) + val textInputButton by bind(R.id.formTextInputButton) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index aadb6584a5..615a7bd7cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -25,6 +25,7 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomAvatar(val image: MultiPickerImageType) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() + data class SetRoomAlias(val alias: String) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index f29c6cc65e..ca472b9de4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -44,6 +44,7 @@ class RoomSettingsController @Inject constructor( fun onTopicChanged(topic: String) fun onPhotoClicked() fun onHistoryVisibilityClicked() + fun onAliasChanged(alias: String) } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -86,6 +87,17 @@ class RoomSettingsController @Inject constructor( } } + formEditTextItem { + id("alias") + /*enabled(enableFormElement)*/ + value(data.newAlias ?: roomSummary.canonicalAlias) + hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address)) + + onTextChange { text -> + callback?.onAliasChanged(text) + } + } + buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 8e25646637..7733f4323f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -190,4 +190,8 @@ class RoomSettingsFragment @Inject constructor( RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) } } + + override fun onAliasChanged(alias: String) { + viewModel.handle(RoomSettingsAction.SetRoomAlias(alias)) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 26da0bb478..d07c2befaf 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -89,6 +89,10 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: setState { copy(newHistoryVisibility = action.visibility) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } } + is RoomSettingsAction.SetRoomAlias -> { + setState { copy(newAlias = action.alias) } + setState { copy(showSaveAction = shouldShowSaveAction(this)) } + } is RoomSettingsAction.Save -> saveSettings() } } @@ -97,6 +101,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = state.roomSummary.invoke() return summary?.displayName != state.newName || summary?.topic != state.newTopic || + summary?.canonicalAlias != state.newAlias || state.newHistoryVisibility != null || state.newAvatar != null } @@ -115,6 +120,10 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } + if (summary?.canonicalAlias != state.newAlias) { + operationList.add(room.rx().updateCanonicalAlias(state.newAlias ?: "")) + } + if (state.newHistoryVisibility != null) { operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility.name.toLowerCase(Locale.ROOT))) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index e456f753c7..0502c7ebc5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -34,6 +34,7 @@ data class RoomSettingsViewState( val newTopic: String? = null, val newAvatar: MultiPickerImageType? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, + val newAlias: String? = null, val showSaveAction: Boolean = false ) : MvRxState { diff --git a/vector/src/main/res/layout/item_form_text_input_with_button.xml b/vector/src/main/res/layout/item_form_text_input_with_button.xml new file mode 100644 index 0000000000..ccab8af3c1 --- /dev/null +++ b/vector/src/main/res/layout/item_form_text_input_with_button.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + From 1f30cf468a8897c0b0efe1ffb576f04454c9e593 Mon Sep 17 00:00:00 2001 From: onurays Date: Tue, 23 Jun 2020 10:59:08 +0300 Subject: [PATCH 08/29] Check if user have enough power level to change settings. --- .../room/powerlevels/PowerLevelsHelper.kt | 56 +++++++++++++++++++ .../core/epoxy/profiles/ProfileActionItem.kt | 2 +- .../settings/RoomSettingsController.kt | 10 ++-- .../settings/RoomSettingsViewModel.kt | 16 ++++++ .../settings/RoomSettingsViewState.kt | 11 +++- 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt index f434859f6e..6361a46bac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.powerlevels +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.PowerLevelsContent /** @@ -123,4 +124,59 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { else -> Role.Moderator.value } } + + /** + * Check if user have the necessary power level to change room name + * @param userId the id of the user to check for. + * @return true if able to change room name + */ + fun isUserAbleToChangeRoomName(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault + return powerLevel >= minPowerLevel + } + + /** + * Check if user have the necessary power level to change room topic + * @param userId the id of the user to check for. + * @return true if able to change room topic + */ + fun isUserAbleToChangeRoomTopic(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault + return powerLevel >= minPowerLevel + } + + /** + * Check if user have the necessary power level to change room canonical alias + * @param userId the id of the user to check for. + * @return true if able to change room canonical alias + */ + fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault + return powerLevel >= minPowerLevel + } + + /** + * Check if user have the necessary power level to change room history readability + * @param userId the id of the user to check for. + * @return true if able to change room history readability + */ + fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault + return powerLevel >= minPowerLevel + } + + /** + * Check if user have the necessary power level to change room avatar + * @param userId the id of the user to check for. + * @return true if able to change room avatar + */ + fun isUserAbleToChangeRoomAvatar(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault + return powerLevel >= minPowerLevel + } } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt index 80b78d0d70..1be5386c2f 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -71,7 +71,7 @@ abstract class ProfileActionItem : VectorEpoxyModel() override fun bind(holder: Holder) { super.bind(holder) holder.view.setOnClickListener(listener) - if (listener == null) { + if (listener == null || !editable) { holder.view.isClickable = false } holder.title.text = title diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index ca472b9de4..5764282d47 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -67,7 +67,7 @@ class RoomSettingsController @Inject constructor( formEditTextItem { id("name") - /*enabled(enableFormElement)*/ + enabled(data.actionPermissions.canChangeName) value(data.newName ?: roomSummary.displayName) hint(stringProvider.getString(R.string.room_settings_name_hint)) @@ -78,7 +78,7 @@ class RoomSettingsController @Inject constructor( formEditTextItem { id("topic") - /*enabled(enableFormElement)*/ + enabled(data.actionPermissions.canChangeTopic) value(data.newTopic ?: roomSummary.topic) hint(stringProvider.getString(R.string.room_settings_topic_hint)) @@ -89,7 +89,7 @@ class RoomSettingsController @Inject constructor( formEditTextItem { id("alias") - /*enabled(enableFormElement)*/ + enabled(data.actionPermissions.canChangeCanonicalAlias) value(data.newAlias ?: roomSummary.canonicalAlias) hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address)) @@ -104,7 +104,7 @@ class RoomSettingsController @Inject constructor( subtitle = newHistoryVisibility ?: historyVisibility, dividerColor = dividerColor, divider = false, - editable = true, + editable = data.actionPermissions.canChangeHistoryReadability, action = { callback?.onHistoryVisibilityClicked() } ) @@ -114,7 +114,7 @@ class RoomSettingsController @Inject constructor( subtitle = "", dividerColor = dividerColor, divider = true, - editable = true, + editable = data.actionPermissions.canChangeAvatar, accessoryMatrixItem = roomSummary.toMatrixItem(), avatarRenderer = avatarRenderer, action = { callback?.onPhotoClicked() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index d07c2befaf..fd30a5f4db 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -24,9 +24,11 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable import java.util.Locale @@ -68,6 +70,20 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: newTopic = roomSummary?.topic ) } + + val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + + powerLevelsContentLive.subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomSettingsViewState.ActionPermissions( + canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId), + canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId), + canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId), + canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId), + canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId) + ) + setState { copy(actionPermissions = permissions) } + }.disposeOnClear() } override fun handle(action: RoomSettingsAction) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 0502c7ebc5..05a2646999 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -35,8 +35,17 @@ data class RoomSettingsViewState( val newAvatar: MultiPickerImageType? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, val newAlias: String? = null, - val showSaveAction: Boolean = false + val showSaveAction: Boolean = false, + val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + data class ActionPermissions( + val canChangeName: Boolean = false, + val canChangeTopic: Boolean = false, + val canChangeCanonicalAlias: Boolean = false, + val canChangeAvatar: Boolean = false, + val canChangeHistoryReadability: Boolean = false + ) } From 7f2ce91c821bd7e3ca6d0421599bccea51d7d252 Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 24 Jun 2020 09:49:58 +0300 Subject: [PATCH 09/29] Add camera and gallery options to set room avatar. --- .../roomprofile/AvatarSelectorView.kt | 216 ++++++++++++++++++ .../features/roomprofile/RoomProfileAction.kt | 2 + .../roomprofile/RoomProfileFragment.kt | 114 ++++++++- .../roomprofile/RoomProfileViewEvents.kt | 1 + .../roomprofile/RoomProfileViewModel.kt | 13 ++ .../main/res/layout/view_avatar_selector.xml | 68 ++++++ 6 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt create mode 100644 vector/src/main/res/layout/view_avatar_selector.xml diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt new file mode 100644 index 0000000000..6636d533f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt @@ -0,0 +1,216 @@ +/* + * 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.riotx.features.roomprofile + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.TargetApi +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.util.Pair +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewAnimationUtils +import android.view.animation.Animation +import android.view.animation.AnimationSet +import android.view.animation.OvershootInterpolator +import android.view.animation.ScaleAnimation +import android.view.animation.TranslateAnimation +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.PopupWindow +import androidx.core.view.doOnNextLayout +import com.amulyakhare.textdrawable.TextDrawable +import com.amulyakhare.textdrawable.util.ColorGenerator +import im.vector.riotx.R +import im.vector.riotx.core.extensions.getMeasurements +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES +import im.vector.riotx.features.roomprofile.AvatarSelectorView.Callback +import kotlin.math.max + +private const val ANIMATION_DURATION = 250 + +/** + * This class is the view presenting choices for picking avatar. + * It will return result through [Callback]. + */ +class AvatarSelectorView(context: Context, + inflater: LayoutInflater, + var callback: Callback?) + : PopupWindow(context) { + + interface Callback { + fun onTypeSelected(type: Type) + } + + private val iconColorGenerator = ColorGenerator.MATERIAL + + private var galleryButton: ImageButton + private var cameraButton: ImageButton + + private var anchor: View? = null + + init { + val root = FrameLayout(context) + val layout = inflater.inflate(R.layout.view_avatar_selector, root, true) + galleryButton = layout.findViewById(R.id.avatarGalleryButton).configure(Type.GALLERY) + cameraButton = layout.findViewById(R.id.avatarCameraButton).configure(Type.CAMERA) + contentView = root + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.WRAP_CONTENT + animationStyle = 0 + @Suppress("DEPRECATION") + setBackgroundDrawable(BitmapDrawable()) + inputMethodMode = INPUT_METHOD_NOT_NEEDED + isFocusable = true + isTouchable = true + } + + fun show(anchor: View, isKeyboardOpen: Boolean) { + this.anchor = anchor + val anchorCoordinates = IntArray(2) + anchor.getLocationOnScreen(anchorCoordinates) + if (isKeyboardOpen) { + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height) + } else { + val contentViewHeight = if (contentView.height == 0) { + contentView.getMeasurements().second + } else { + contentView.height + } + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight) + } + contentView.doOnNextLayout { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + animateWindowInCircular(anchor, contentView) + } else { + animateWindowInTranslate(contentView) + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + animateButtonIn(galleryButton, ANIMATION_DURATION / 2) + animateButtonIn(cameraButton, ANIMATION_DURATION / 2) + } + } + + override fun dismiss() { + val capturedAnchor = anchor + if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + animateWindowOutCircular(capturedAnchor, contentView) + } else { + animateWindowOutTranslate(contentView) + } + } + + private fun animateButtonIn(button: View, delay: Int) { + val animation = AnimationSet(true) + val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f) + animation.addAnimation(scale) + animation.interpolator = OvershootInterpolator(1f) + animation.duration = ANIMATION_DURATION.toLong() + animation.startOffset = delay.toLong() + button.startAnimation(animation) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun animateWindowInCircular(anchor: View, contentView: View) { + val coordinates = getClickCoordinates(anchor, contentView) + val animator = ViewAnimationUtils.createCircularReveal(contentView, + coordinates.first, + coordinates.second, + 0f, + max(contentView.width, contentView.height).toFloat()) + animator.duration = ANIMATION_DURATION.toLong() + animator.start() + } + + private fun animateWindowInTranslate(contentView: View) { + val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f) + animation.duration = ANIMATION_DURATION.toLong() + getContentView().startAnimation(animation) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun animateWindowOutCircular(anchor: View, contentView: View) { + val coordinates = getClickCoordinates(anchor, contentView) + val animator = ViewAnimationUtils.createCircularReveal(getContentView(), + coordinates.first, + coordinates.second, + max(getContentView().width, getContentView().height).toFloat(), + 0f) + + animator.duration = ANIMATION_DURATION.toLong() + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super@AvatarSelectorView.dismiss() + } + }) + animator.start() + } + + private fun animateWindowOutTranslate(contentView: View) { + val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat()) + animation.duration = ANIMATION_DURATION.toLong() + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) {} + + override fun onAnimationEnd(animation: Animation) { + super@AvatarSelectorView.dismiss() + } + + override fun onAnimationRepeat(animation: Animation) {} + }) + + getContentView().startAnimation(animation) + } + + private fun getClickCoordinates(anchor: View, contentView: View): Pair { + val anchorCoordinates = IntArray(2) + anchor.getLocationOnScreen(anchorCoordinates) + val contentCoordinates = IntArray(2) + contentView.getLocationOnScreen(contentCoordinates) + val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2 + val y = anchorCoordinates[1] - contentCoordinates[1] + return Pair(x, y) + } + + private fun ImageButton.configure(type: Type): ImageButton { + this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal)) + this.setOnClickListener(TypeClickListener(type)) + return this + } + + private inner class TypeClickListener(private val type: Type) : View.OnClickListener { + + override fun onClick(v: View) { + dismiss() + callback?.onTypeSelected(type) + } + } + + /** + * The all possible types to pick with their required permissions. + */ + enum class Type(val permissionsBit: Int) { + CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), + GALLERY(PERMISSIONS_FOR_WRITING_FILES), + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt index 545d67c314..20498dbf84 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.roomprofile +import android.net.Uri import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.riotx.core.platform.VectorViewModelAction 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() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 52f2d95c93..19840007e6 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -17,15 +17,23 @@ package im.vector.riotx.features.roomprofile +import android.app.Activity +import android.content.Intent +import android.graphics.Color +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.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 com.yalantis.ucrop.UCropActivity import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem @@ -36,7 +44,9 @@ import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.features.crypto.util.toImageRes @@ -45,10 +55,13 @@ import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.riotx.multipicker.MultiPicker +import im.vector.riotx.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.* import timber.log.Timber +import java.io.File import javax.inject.Inject @Parcelize @@ -59,8 +72,9 @@ data class RoomProfileArgs( class RoomProfileFragment @Inject constructor( private val roomProfileController: RoomProfileController, private val avatarRenderer: AvatarRenderer, - val roomProfileViewModelFactory: RoomProfileViewModel.Factory -) : VectorBaseFragment(), RoomProfileController.Callback { + val roomProfileViewModelFactory: RoomProfileViewModel.Factory, + val colorProvider: ColorProvider +) : VectorBaseFragment(), RoomProfileController.Callback, AvatarSelectorView.Callback { private val roomProfileArgs: RoomProfileArgs by args() private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel @@ -69,6 +83,8 @@ class RoomProfileFragment @Inject constructor( private var appBarStateChangeListener: AppBarStateChangeListener? = null + private lateinit var avatarSelector: AvatarSelectorView + override fun getLayoutResId() = R.layout.fragment_matrix_profile override fun getMenuRes() = R.menu.vector_room_profile @@ -96,6 +112,7 @@ class RoomProfileFragment @Inject constructor( is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom() is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) + RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog() }.exhaustive } roomListQuickActionsSharedActionViewModel @@ -222,6 +239,97 @@ class RoomProfileFragment @Inject constructor( } private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) { - navigator.openBigImageViewer(requireActivity(), view, matrixItem) + if (matrixItem.avatarUrl.isNullOrEmpty()) { + showAvatarSelector() + } else { + navigator.openBigImageViewer(requireActivity(), view, matrixItem) + } + } + + private fun showAvatarSelector() { + if (!::avatarSelector.isInitialized) { + avatarSelector = AvatarSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomProfileFragment) + } + avatarSelector.show(vector_coordinator_layout, false) + } + + private var avatarCameraUri: Uri? = null + override fun onTypeSelected(type: AvatarSelectorView.Type) { + when (type) { + AvatarSelectorView.Type.CAMERA -> { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) + } + AvatarSelectorView.Type.GALLERY -> { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } + } + } + + private fun onRoomAvatarSelected(image: MultiPickerImageType) { + val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") + val uri = image.contentUri + UCrop.of(uri, destinationFile.toUri()) + .withOptions( + UCrop.Options() + .apply { + setAllowedGestures( + /* tabScale = */ UCropActivity.SCALE, + /* tabRotate = */ UCropActivity.ALL, + /* tabAspectRatio = */ UCropActivity.SCALE + ) + setToolbarTitle(image.displayName) + // Disable freestyle crop, usability was not easy + // setFreeStyleCropEnabled(true) + // Color used for toolbar icon and text + setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) + setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) + // Background + setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) + // Status bar color (pb in dark mode, icon of the status bar are dark) + 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(colorProvider.getColor(R.color.riotx_accent)) + // Hide the logo (does not work) + setLogoColor(Color.TRANSPARENT) + } + ) + .start(requireContext(), this) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + MultiPicker.REQUEST_CODE_TAKE_PHOTO -> { + avatarCameraUri?.let { uri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(requireContext(), requestCode, resultCode, uri) + ?.let { + onRoomAvatarSelected(it) + } + } + } + MultiPicker.REQUEST_CODE_PICK_IMAGE -> { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(requireContext(), requestCode, resultCode, data) + .firstOrNull()?.let { + // TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is. + // onRoomAvatarSelected(it) + onAvatarCropped(it.contentUri) + } + } + UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) } + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + 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() + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt index 7a08a08126..78df127f72 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt @@ -26,5 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : RoomProfileViewEvents() object OnLeaveRoomSuccess : RoomProfileViewEvents() + object OnChangeAvatarSuccess : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index d49727d12d..5e4dde169b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -30,6 +30,7 @@ import im.vector.matrix.rx.unwrap import im.vector.riotx.R import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider +import java.util.UUID class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState, private val stringProvider: StringProvider, @@ -68,6 +69,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini RoomProfileAction.LeaveRoom -> handleLeaveRoom() is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() + is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action) } private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) { @@ -96,4 +98,15 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini _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/res/layout/view_avatar_selector.xml b/vector/src/main/res/layout/view_avatar_selector.xml new file mode 100644 index 0000000000..a184bd1544 --- /dev/null +++ b/vector/src/main/res/layout/view_avatar_selector.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From ef1ae4105b492490b3f20197bb2f26abe7b6ce0c Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 24 Jun 2020 10:49:51 +0300 Subject: [PATCH 10/29] Change room avatar in BigImageViewer. --- .../features/media/BigImageViewerActivity.kt | 125 +++++++++++++++++- .../roomprofile/RoomProfileFragment.kt | 9 +- .../res/menu/vector_big_avatar_viewer.xml | 10 ++ 3 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/res/menu/vector_big_avatar_viewer.xml diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 24b595d182..8cd7bf1497 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -16,19 +16,38 @@ package im.vector.riotx.features.media +import android.app.Activity import android.content.Context import android.content.Intent +import android.graphics.Color +import android.net.Uri import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast import androidx.core.net.toUri +import com.yalantis.ucrop.UCrop +import com.yalantis.ucrop.UCropActivity import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.features.roomprofile.AvatarSelectorView +import im.vector.riotx.multipicker.MultiPicker +import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.activity_big_image_viewer.* +import java.io.File import javax.inject.Inject -class BigImageViewerActivity : VectorBaseActivity() { +class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback { @Inject lateinit var sessionHolder: ActiveSessionHolder + @Inject lateinit var colorProvider: ColorProvider + + private var uri: Uri? = null + private lateinit var avatarSelector: AvatarSelectorView + + override fun getMenuRes() = R.menu.vector_big_avatar_viewer override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -45,7 +64,7 @@ class BigImageViewerActivity : VectorBaseActivity() { setDisplayHomeAsUpEnabled(true) } - val uri = sessionHolder.getSafeActiveSession() + uri = sessionHolder.getSafeActiveSession() ?.contentUrlResolver() ?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL)) ?.toUri() @@ -57,9 +76,111 @@ class BigImageViewerActivity : VectorBaseActivity() { } } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + menu.findItem(R.id.bigAvatarEditAction).isVisible = uri != null + 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 showAvatarSelector() { + if (!::avatarSelector.isInitialized) { + avatarSelector = AvatarSelectorView(this, layoutInflater, this) + } + avatarSelector.show(bigImageViewerToolbar, false) + } + + private var avatarCameraUri: Uri? = null + override fun onTypeSelected(type: AvatarSelectorView.Type) { + when (type) { + AvatarSelectorView.Type.CAMERA -> { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) + } + AvatarSelectorView.Type.GALLERY -> { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } + } + } + + private fun onRoomAvatarSelected(image: MultiPickerImageType) { + val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") + val uri = image.contentUri + UCrop.of(uri, destinationFile.toUri()) + .withOptions( + UCrop.Options() + .apply { + setAllowedGestures( + /* tabScale = */ UCropActivity.SCALE, + /* tabRotate = */ UCropActivity.ALL, + /* tabAspectRatio = */ UCropActivity.SCALE + ) + setToolbarTitle(image.displayName) + // Disable freestyle crop, usability was not easy + // setFreeStyleCropEnabled(true) + // Color used for toolbar icon and text + setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) + setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) + // Background + setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) + // Status bar color (pb in dark mode, icon of the status bar are dark) + 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(colorProvider.getColor(R.color.riotx_accent)) + // Hide the logo (does not work) + setLogoColor(Color.TRANSPARENT) + } + ) + .start(this) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + MultiPicker.REQUEST_CODE_TAKE_PHOTO -> { + avatarCameraUri?.let { uri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(this, requestCode, resultCode, uri) + ?.let { + onRoomAvatarSelected(it) + } + } + } + MultiPicker.REQUEST_CODE_PICK_IMAGE -> { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(this, requestCode, resultCode, data) + .firstOrNull()?.let { + // TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is. + // onRoomAvatarSelected(it) + onAvatarCropped(it.contentUri) + } + } + UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) } + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + 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" + const val REQUEST_CODE = 1000 fun newIntent(context: Context, title: String?, imageUrl: String): Intent { return Intent(context, BigImageViewerActivity::class.java).apply { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 19840007e6..bffd294c17 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -27,7 +27,9 @@ 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.net.toUri +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -55,6 +57,7 @@ import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.parcel.Parcelize @@ -242,7 +245,10 @@ class RoomProfileFragment @Inject constructor( if (matrixItem.avatarUrl.isNullOrEmpty()) { showAvatarSelector() } else { - navigator.openBigImageViewer(requireActivity(), view, matrixItem) + val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, ViewCompat.getTransitionName(view) ?: "") + + startActivityForResult(intent, BigImageViewerActivity.REQUEST_CODE, options.toBundle()) } } @@ -320,6 +326,7 @@ class RoomProfileFragment @Inject constructor( } } UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) } + BigImageViewerActivity.REQUEST_CODE -> data?.let { onAvatarCropped(it.data) } } } super.onActivityResult(requestCode, resultCode, data) diff --git a/vector/src/main/res/menu/vector_big_avatar_viewer.xml b/vector/src/main/res/menu/vector_big_avatar_viewer.xml new file mode 100644 index 0000000000..aff0ac0e03 --- /dev/null +++ b/vector/src/main/res/menu/vector_big_avatar_viewer.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file From 8787f5d920a608d4ae57ba9592d51ce5f736e324 Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 24 Jun 2020 11:07:10 +0300 Subject: [PATCH 11/29] Remove room avatar item from room settings. --- .../settings/RoomSettingsAction.kt | 2 -- .../settings/RoomSettingsController.kt | 16 ----------- .../settings/RoomSettingsFragment.kt | 28 ------------------- .../settings/RoomSettingsViewModel.kt | 15 ++-------- .../settings/RoomSettingsViewState.kt | 3 -- 5 files changed, 2 insertions(+), 62 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index 615a7bd7cd..4c0cc97a0d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -18,12 +18,10 @@ package im.vector.riotx.features.roomprofile.settings import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.riotx.core.platform.VectorViewModelAction -import im.vector.riotx.multipicker.entity.MultiPickerImageType sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() - data class SetRoomAvatar(val image: MultiPickerImageType) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() data class SetRoomAlias(val alias: String) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 5764282d47..3a989b201a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -21,19 +21,16 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent -import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.form.formEditTextItem -import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject // TODO Add other feature here (waiting for design) class RoomSettingsController @Inject constructor( - private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, colorProvider: ColorProvider ) : TypedEpoxyController() { @@ -42,7 +39,6 @@ class RoomSettingsController @Inject constructor( fun onEnableEncryptionClicked() fun onNameChanged(name: String) fun onTopicChanged(topic: String) - fun onPhotoClicked() fun onHistoryVisibilityClicked() fun onAliasChanged(alias: String) } @@ -108,18 +104,6 @@ class RoomSettingsController @Inject constructor( action = { callback?.onHistoryVisibilityClicked() } ) - buildProfileAction( - id = "photo", - title = stringProvider.getString(R.string.room_settings_room_photo), - subtitle = "", - dividerColor = dividerColor, - divider = true, - editable = data.actionPermissions.canChangeAvatar, - accessoryMatrixItem = roomSummary.toMatrixItem(), - avatarRenderer = avatarRenderer, - action = { callback?.onPhotoClicked() } - ) - if (roomSummary.isEncrypted) { buildProfileAction( id = "encryption", diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 7733f4323f..e6b0ad92af 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -16,8 +16,6 @@ package im.vector.riotx.features.roomprofile.settings -import android.app.Activity -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -40,8 +38,6 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs -import im.vector.riotx.multipicker.MultiPicker -import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import javax.inject.Inject @@ -134,30 +130,6 @@ class RoomSettingsFragment @Inject constructor( viewModel.handle(RoomSettingsAction.SetRoomTopic(topic)) } - override fun onPhotoClicked() { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) - } - - private fun onRoomPhotoSelected(selectedImage: MultiPickerImageType) { - viewModel.handle(RoomSettingsAction.SetRoomAvatar(selectedImage)) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - MultiPicker.REQUEST_CODE_PICK_IMAGE -> { - MultiPicker - .get(MultiPicker.IMAGE) - .getSelectedFiles(requireContext(), requestCode, resultCode, data) - .firstOrNull()?.let { - onRoomPhotoSelected(it) - } - } - } - } - super.onActivityResult(requestCode, resultCode, data) - } - override fun onHistoryVisibilityClicked() = withState(viewModel) { state -> val historyVisibilities = arrayOf( RoomHistoryVisibility.SHARED, diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index fd30a5f4db..c6808b4de8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -32,7 +32,6 @@ import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable import java.util.Locale -import java.util.UUID class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, private val session: Session) @@ -79,7 +78,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId), canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId), canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId), - canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId), canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId) ) setState { copy(actionPermissions = permissions) } @@ -97,10 +95,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: setState { copy(newTopic = action.newTopic) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } } - is RoomSettingsAction.SetRoomAvatar -> { - setState { copy(newAvatar = action.image) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } - } is RoomSettingsAction.SetRoomHistoryVisibility -> { setState { copy(newHistoryVisibility = action.visibility) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } @@ -118,8 +112,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: return summary?.displayName != state.newName || summary?.topic != state.newTopic || summary?.canonicalAlias != state.newAlias || - state.newHistoryVisibility != null || - state.newAvatar != null + state.newHistoryVisibility != null } private fun saveSettings() = withState { state -> @@ -144,17 +137,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility.name.toLowerCase(Locale.ROOT))) } - if (state.newAvatar != null) { - operationList.add(room.rx().updateAvatar(state.newAvatar.contentUri, state.newAvatar.displayName ?: UUID.randomUUID().toString())) - } - Observable .fromIterable(operationList) .flatMapCompletable { it } .subscribe( { postLoading(false) - setState { copy(newAvatar = null, newHistoryVisibility = null) } + setState { copy(newHistoryVisibility = null) } setState { copy(showSaveAction = shouldShowSaveAction(this)) } _viewEvents.post(RoomSettingsViewEvents.Success) }, diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 05a2646999..6eea5d0d64 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.features.roomprofile.RoomProfileArgs -import im.vector.riotx.multipicker.entity.MultiPickerImageType data class RoomSettingsViewState( val roomId: String, @@ -32,7 +31,6 @@ data class RoomSettingsViewState( val isLoading: Boolean = false, val newName: String? = null, val newTopic: String? = null, - val newAvatar: MultiPickerImageType? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, val newAlias: String? = null, val showSaveAction: Boolean = false, @@ -45,7 +43,6 @@ data class RoomSettingsViewState( val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, val canChangeCanonicalAlias: Boolean = false, - val canChangeAvatar: Boolean = false, val canChangeHistoryReadability: Boolean = false ) } From a03f69fb98dc707badc4c883af61f967cbd505cc Mon Sep 17 00:00:00 2001 From: onurays Date: Wed, 24 Jun 2020 11:07:51 +0300 Subject: [PATCH 12/29] Check if user has enough power level to change room avatar. --- .../riotx/features/media/BigImageViewerActivity.kt | 10 ++++++++-- .../riotx/features/roomprofile/RoomProfileFragment.kt | 11 +++++------ .../features/roomprofile/RoomProfileViewModel.kt | 9 +++++++++ .../features/roomprofile/RoomProfileViewState.kt | 3 ++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 8cd7bf1497..10e752c8a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -77,7 +77,7 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback } override fun onPrepareOptionsMenu(menu: Menu): Boolean { - menu.findItem(R.id.bigAvatarEditAction).isVisible = uri != null + menu.findItem(R.id.bigAvatarEditAction).isVisible = shouldShowEditAction() return super.onPrepareOptionsMenu(menu) } @@ -89,6 +89,10 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback return super.onOptionsItemSelected(item) } + private fun shouldShowEditAction(): Boolean { + return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false) + } + private fun showAvatarSelector() { if (!::avatarSelector.isInitialized) { avatarSelector = AvatarSelectorView(this, layoutInflater, this) @@ -180,12 +184,14 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback 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" const val REQUEST_CODE = 1000 - fun newIntent(context: Context, title: String?, imageUrl: String): Intent { + fun newIntent(context: Context, title: String?, imageUrl: String, canEditImage: Boolean = false): 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/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index bffd294c17..a1e9c2613b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -241,14 +241,13 @@ class RoomProfileFragment @Inject constructor( startSharePlainTextIntent(fragment = this, chooserTitle = null, text = permalink) } - private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) { - if (matrixItem.avatarUrl.isNullOrEmpty()) { - showAvatarSelector() - } else { - val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!) + 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 options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, ViewCompat.getTransitionName(view) ?: "") - startActivityForResult(intent, BigImageViewerActivity.REQUEST_CODE, options.toBundle()) + } else if (it.canChangeAvatar) { + showAvatarSelector() } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index 5e4dde169b..88e14432bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -25,11 +25,13 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import java.util.UUID class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState, @@ -63,6 +65,13 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini .execute { copy(roomSummary = it) } + + val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + + powerLevelsContentLive.subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) } + }.disposeOnClear() } override fun handle(action: RoomProfileAction) = when (action) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt index aed1488b07..76cc3f1f07 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt @@ -24,7 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary data class RoomProfileViewState( val roomId: String, - val roomSummary: Async = Uninitialized + val roomSummary: Async = Uninitialized, + val canChangeAvatar: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) From 16bd642ae8453a45c8f82469bf8484abb5be5de6 Mon Sep 17 00:00:00 2001 From: onurays Date: Thu, 25 Jun 2020 14:38:22 +0300 Subject: [PATCH 13/29] Implementation of updating user avatar. Fixes #1054 --- CHANGES.md | 1 + .../api/session/profile/ProfileService.kt | 9 + .../session/profile/DefaultProfileService.kt | 55 ++++- .../internal/session/profile/ProfileAPI.kt | 6 + .../internal/session/profile/ProfileModule.kt | 3 + .../session/profile/SetAvatarUrlBody.kt | 28 +++ .../session/profile/SetAvatarUrlTask.kt | 43 ++++ .../settings/VectorSettingsGeneralFragment.kt | 217 +++++++++++++----- 8 files changed, 301 insertions(+), 61 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt diff --git a/CHANGES.md b/CHANGES.md index 337e61afb4..06c1424404 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: - Handle `/op`, `/deop`, and `/nick` commands (#12) - Prioritising Recovery key over Recovery passphrase (#1463) - Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455) + - Update user avatar (#1054) Bugfix 🐛: - Fix dark theme issue on login screen (#1097) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt index 3d084336e3..d7569bbc18 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.profile +import android.net.Uri import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.identity.ThreePid @@ -48,6 +49,14 @@ interface ProfileService { */ fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable + /** + * Update the avatar for this user + * @param userId the userId to update the avatar of + * @param newAvatarUri the new avatar uri of the user + * @param fileName the fileName of selected image + */ + fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable + /** * Return the current avatarUrl for this user. * @param userId the userId param to look for diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index 459d53607b..aae2df5379 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -17,26 +17,44 @@ package im.vector.matrix.android.internal.session.profile +import android.net.Uri import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.kotlin.where +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit import javax.inject.Inject +private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" + internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, @SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, + private val coroutineDispatchers: MatrixCoroutineDispatchers, private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val getProfileInfoTask: GetProfileInfoTask, - private val setDisplayNameTask: SetDisplayNameTask) : ProfileService { + private val setDisplayNameTask: SetDisplayNameTask, + private val setAvatarUrlTask: SetAvatarUrlTask) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { val params = GetProfileInfoTask.Params(userId) @@ -64,6 +82,41 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto .executeBy(taskExecutor) } + override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable { + val cancelableBag = CancelableBag() + val workerParams = UploadAvatarWorker.Params(sessionId, newAvatarUri, fileName) + val workerData = WorkerParamsFactory.toData(workerParams) + + val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + + workManagerProvider.workManager + .beginUniqueWork("${userId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork) + .enqueue() + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id)) + + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id) + .observeForever { info -> + if (info != null && info.state.isFinished) { + val result = WorkerParamsFactory.fromData(info.outputData) + cancelableBag.add( + setAvatarUrlTask + .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = result?.imageUrl!!)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + ) + } + } + } + return cancelableBag + } + override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable { val params = GetProfileInfoTask.Params(userId) return getProfileInfoTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt index b3b726a315..7dc4763403 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt @@ -49,6 +49,12 @@ internal interface ProfileAPI { @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call + /** + * Change user avatar url. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") + fun setAvatarUrl(@Path("userId") userId: String, @Body body: SetAvatarUrlBody): Call + /** * Bind a threePid * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt index d83c305c10..b86d0ee07a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt @@ -54,4 +54,7 @@ internal abstract class ProfileModule { @Binds abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask + + @Binds + abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt new file mode 100644 index 0000000000..0288853e28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SetAvatarUrlBody( + /** + * The new avatar url for this user. + */ + @Json(name = "avatar_url") + val avatarUrl: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt new file mode 100644 index 0000000000..263922ca78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt @@ -0,0 +1,43 @@ +/* + * 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.matrix.android.internal.session.profile + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class SetAvatarUrlTask : Task { + data class Params( + val userId: String, + val newAvatarUrl: String + ) +} + +internal class DefaultSetAvatarUrlTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : SetAvatarUrlTask() { + + override suspend fun execute(params: Params) { + return executeRequest(eventBus) { + val body = SetAvatarUrlBody( + avatarUrl = params.newAvatarUrl + ) + apiCall = profileAPI.setAvatarUrl(params.userId, body) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 5ff521400f..791b0e20f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -20,13 +20,17 @@ package im.vector.riotx.features.settings import android.app.Activity import android.content.Intent +import android.graphics.Color +import android.net.Uri import android.text.Editable import android.util.Patterns import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.preference.EditTextPreference import androidx.preference.Preference @@ -36,6 +40,8 @@ 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 com.yalantis.ucrop.UCropActivity import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.failure.isInvalidPassword @@ -44,27 +50,34 @@ import im.vector.matrix.android.api.session.integrationmanager.IntegrationManage import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.preference.VectorSwitchPreference +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.getSizeOfFiles import im.vector.riotx.core.utils.toast import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs +import im.vector.riotx.features.roomprofile.AvatarSelectorView import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.workers.signout.SignOutUiWorker +import im.vector.riotx.multipicker.MultiPicker +import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import java.util.UUID -class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { +class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelectorView.Callback { override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general @@ -72,6 +85,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { private var mDisplayedEmails = ArrayList() private var mDisplayedPhoneNumber = ArrayList() + private lateinit var avatarSelector: AvatarSelectorView + private var avatarCameraUri: Uri? = null + private val mUserSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! } @@ -281,7 +297,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { - changeAvatar() + onTypeSelected(AvatarSelectorView.Type.CAMERA) } } } @@ -291,8 +307,27 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { if (resultCode == Activity.RESULT_OK) { when (requestCode) { - REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList() - REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data) + REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList() + REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data) + MultiPicker.REQUEST_CODE_TAKE_PHOTO -> { + avatarCameraUri?.let { uri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(requireContext(), requestCode, resultCode, uri) + ?.let { + onAvatarSelected(it) + } + } + } + MultiPicker.REQUEST_CODE_PICK_IMAGE -> { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(requireContext(), requestCode, resultCode, data) + .firstOrNull()?.let { + // TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is. + onAvatarCropped(it.contentUri) + } + } + UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) } /* TODO VectorUtils.TAKE_IMAGE -> { val thumbnailUri = VectorUtils.getThumbnailUriFromIntent(activity, data, session.mediaCache) @@ -370,26 +405,88 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { * Update the avatar. */ private fun onUpdateAvatarClick() { - notImplemented() - - /* TODO - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { - changeAvatar() + if (!::avatarSelector.isInitialized) { + avatarSelector = AvatarSelectorView(activity!!, activity!!.layoutInflater, this) + } + mUserAvatarPreference.mAvatarView?.let { + avatarSelector.show(it, false) } - */ } - private fun changeAvatar() { - /* TODO - val intent = Intent(activity, VectorMediaPickerActivity::class.java) - intent.putExtra(VectorMediaPickerActivity.EXTRA_AVATAR_MODE, true) - startActivityForResult(intent, VectorUtils.TAKE_IMAGE) - */ + override fun onTypeSelected(type: AvatarSelectorView.Type) { + when (type) { + AvatarSelectorView.Type.CAMERA -> { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) + } + } + AvatarSelectorView.Type.GALLERY -> { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } + } } - // ============================================================================================================== - // contacts management - // ============================================================================================================== + private fun onAvatarSelected(image: MultiPickerImageType) { + val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") + val uri = image.contentUri + UCrop.of(uri, destinationFile.toUri()) + .withOptions( + UCrop.Options() + .apply { + setAllowedGestures( + /* tabScale = */ UCropActivity.SCALE, + /* tabRotate = */ UCropActivity.ALL, + /* tabAspectRatio = */ UCropActivity.SCALE + ) + setToolbarTitle(image.displayName) + // Disable freestyle crop, usability was not easy + // setFreeStyleCropEnabled(true) + // Color used for toolbar icon and text + setToolbarColor(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) + setToolbarWidgetColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_primary_text_color)) + // Background + setRootViewBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) + // Status bar color (pb in dark mode, icon of the status bar are dark) + setStatusBarColor(ThemeUtils.getColor(requireContext(), 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(requireContext(), R.color.riotx_accent)) + // Hide the logo (does not work) + setLogoColor(Color.TRANSPARENT) + } + ) + .start(requireContext(), this) + } + + private fun onAvatarCropped(uri: Uri?) { + if (uri != null) { + uploadAvatar(uri) + } else { + Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() + } + } + + private fun uploadAvatar(uri: Uri) { + displayLoadingView() + + session.updateAvatar(session.myUserId, uri, getFilenameFromUri(context, uri) ?: UUID.randomUUID().toString(), object : MatrixCallback { + override fun onSuccess(data: Unit) { + if (!isAdded) return + + mUserAvatarPreference.refreshAvatar() + onCommonDone(null) + } + + override fun onFailure(failure: Throwable) { + if (!isAdded) return + onCommonDone(failure.localizedMessage) + } + }) + } + +// ============================================================================================================== +// contacts management +// ============================================================================================================== private fun setContactsPreferences() { /* TODO @@ -422,9 +519,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { */ } - // ============================================================================================================== - // Phone number management - // ============================================================================================================== +// ============================================================================================================== +// Phone number management +// ============================================================================================================== /** * Refresh phone number list @@ -505,9 +602,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { } */ } - // ============================================================================================================== - // Email management - // ============================================================================================================== +// ============================================================================================================== +// Email management +// ============================================================================================================== /** * Refresh the emails list @@ -632,47 +729,47 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { * * @param pid the used pid. */ - /* TODO - private fun showEmailValidationDialog(pid: ThreePid) { - activity?.let { - AlertDialog.Builder(it) - .setTitle(R.string.account_email_validation_title) - .setMessage(R.string.account_email_validation_message) - .setPositiveButton(R.string._continue) { _, _ -> - session.myUser.add3Pid(pid, true, object : MatrixCallback { - override fun onSuccess(info: Void?) { +/* TODO +private fun showEmailValidationDialog(pid: ThreePid) { + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.account_email_validation_title) + .setMessage(R.string.account_email_validation_message) + .setPositiveButton(R.string._continue) { _, _ -> + session.myUser.add3Pid(pid, true, object : MatrixCallback { + override fun onSuccess(info: Void?) { + it.runOnUiThread { + hideLoadingView() + refreshEmailsList() + } + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) { it.runOnUiThread { hideLoadingView() - refreshEmailsList() + it.toast(R.string.account_email_validation_error) } - } - - override fun onNetworkError(e: Exception) { + } else { onCommonDone(e.localizedMessage) } + } - override fun onMatrixError(e: MatrixError) { - if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) { - it.runOnUiThread { - hideLoadingView() - it.toast(R.string.account_email_validation_error) - } - } else { - onCommonDone(e.localizedMessage) - } - } - - override fun onUnexpectedError(e: Exception) { - onCommonDone(e.localizedMessage) - } - }) - } - .setNegativeButton(R.string.cancel) { _, _ -> - hideLoadingView() - } - .show() - } - } */ + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + .setNegativeButton(R.string.cancel) { _, _ -> + hideLoadingView() + } + .show() + } +} */ /** * Display a dialog which asks confirmation for the deletion of a 3pid From e0e4cf3df124929bdb33b567ba3ffcd6a65ceb61 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 06:38:58 +0300 Subject: [PATCH 14/29] Code review fixes. --- .../session/profile/DefaultProfileService.kt | 2 +- .../settings/VectorSettingsGeneralFragment.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index aae2df5379..7d510d2510 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -106,7 +106,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto val result = WorkerParamsFactory.fromData(info.outputData) cancelableBag.add( setAvatarUrlTask - .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = result?.imageUrl!!)) { + .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = result!!.imageUrl!!)) { callback = matrixCallback } .executeBy(taskExecutor) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 791b0e20f5..c344d7bf2f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -484,9 +484,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect }) } -// ============================================================================================================== -// contacts management -// ============================================================================================================== + // ============================================================================================================== + // contacts management + // ============================================================================================================== private fun setContactsPreferences() { /* TODO @@ -519,9 +519,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect */ } -// ============================================================================================================== -// Phone number management -// ============================================================================================================== + // ============================================================================================================== + // Phone number management + // ============================================================================================================== /** * Refresh phone number list From a93cbf3548dba7cf8371bc3e311a737a51610b64 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 06:43:36 +0300 Subject: [PATCH 15/29] Lint fix. --- .../roomprofile/settings/RoomSettingsViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index c6808b4de8..8fc699e4c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -109,10 +109,10 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean { val summary = state.roomSummary.invoke() - return summary?.displayName != state.newName || - summary?.topic != state.newTopic || - summary?.canonicalAlias != state.newAlias || - state.newHistoryVisibility != null + return summary?.displayName != state.newName + || summary?.topic != state.newTopic + || summary?.canonicalAlias != state.newAlias + || state.newHistoryVisibility != null } private fun saveSettings() = withState { state -> From 56f8e52352b6e25ccceedfc593794d7559ac2a37 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 08:30:18 +0300 Subject: [PATCH 16/29] Simplify uploading room and user avatar. --- .../internal/session/SessionComponent.kt | 3 - .../internal/session/content/FileUploader.kt | 19 +++ .../session/content/UploadAvatarWorker.kt | 128 ------------------ .../session/profile/DefaultProfileService.kt | 49 ++----- .../session/room/state/DefaultStateService.kt | 53 ++------ 5 files changed, 41 insertions(+), 211 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 4dc9fc0305..b95595ed23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -32,7 +32,6 @@ import im.vector.matrix.android.internal.session.account.AccountModule import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.call.CallModule import im.vector.matrix.android.internal.session.content.ContentModule -import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker @@ -118,8 +117,6 @@ internal interface SessionComponent { fun inject(worker: UploadContentWorker) - fun inject(worker: UploadAvatarWorker) - fun inject(worker: SyncWorker) fun inject(worker: AddHttpPusherWorker) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 1153b39b0a..6ee508b02a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -16,12 +16,16 @@ package im.vector.matrix.android.internal.session.content +import android.content.Context +import android.net.Uri import com.squareup.moshi.Moshi import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.network.awaitResponse import im.vector.matrix.android.internal.network.toFailure +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -31,12 +35,14 @@ import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.greenrobot.eventbus.EventBus import java.io.File +import java.io.FileNotFoundException import java.io.IOException import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated private val okHttpClient: OkHttpClient, private val eventBus: EventBus, + private val context: Context, contentUrlResolver: ContentUrlResolver, moshi: Moshi) { @@ -59,6 +65,19 @@ internal class FileUploader @Inject constructor(@Authenticated return upload(uploadBody, filename, progressListener) } + suspend fun uploadFromUri(uri: Uri, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val inputStream = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } ?: throw FileNotFoundException() + + inputStream.use { + return uploadByteArray(it.readBytes(), filename, mimeType, progressListener) + } + } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt deleted file mode 100644 index 8becdfa05c..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadAvatarWorker.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.matrix.android.internal.session.content - -import android.content.Context -import android.net.Uri -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.worker.SessionWorkerParams -import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import im.vector.matrix.android.internal.worker.getSessionComponent -import timber.log.Timber -import javax.inject.Inject - -/** - * Possible previous worker: None - * Possible next worker : None - */ -internal class UploadAvatarWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val queryUri: Uri, - val fileName: String, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @JsonClass(generateAdapter = true) - internal data class OutputParams( - override val sessionId: String, - val imageUrl: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var fileUploader: FileUploader - - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } - - Timber.v("Starting upload media work with params $params") - - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } - } - - // Just defensive code to ensure that we never have an uncaught exception that could break the queue - return try { - internalDoWork(params) - } catch (failure: Throwable) { - Timber.e(failure) - handleFailure(params, failure) - } - } - - private suspend fun internalDoWork(params: Params): Result { - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) - - try { - val inputStream = context.contentResolver.openInputStream(params.queryUri) - ?: return Result.success( - WorkerParamsFactory.toData( - params.copy( - lastFailureMessage = "Cannot openInputStream for file: " + params.queryUri.toString() - ) - ) - ) - - inputStream.use { - return try { - Timber.v("## UploadAvatarWorker - Uploading avatar...") - val response = fileUploader.uploadByteArray(inputStream.readBytes(), params.fileName, "image/jpeg") - Timber.v("## UploadAvatarWorker - Uploadeded avatar: ${response.contentUri}") - handleSuccess(params, response.contentUri) - } catch (t: Throwable) { - Timber.e(t, "## UploadAvatarWorker - Uploading avatar failed...") - handleFailure(params, t) - } - } - } catch (e: Exception) { - Timber.e(e) - return Result.success( - WorkerParamsFactory.toData( - params.copy( - lastFailureMessage = e.localizedMessage - ) - ) - ) - } - } - - private fun handleFailure(params: Params, failure: Throwable): Result { - return Result.success( - WorkerParamsFactory.toData( - params.copy( - lastFailureMessage = failure.localizedMessage - ) - ) - ) - } - - private fun handleSuccess(params: Params, imageUrl: String): Result { - Timber.v("handleSuccess $imageUrl, work is stopped $isStopped") - - val sendParams = OutputParams(params.sessionId, imageUrl, params.lastFailureMessage) - return Result.success(WorkerParamsFactory.toData(sendParams)) - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index 7d510d2510..c76426b2cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -19,29 +19,23 @@ package im.vector.matrix.android.internal.session.profile import android.net.Uri import androidx.lifecycle.LiveData -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider -import im.vector.matrix.android.internal.session.content.UploadAvatarWorker +import im.vector.matrix.android.internal.session.content.FileUploader import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.kotlin.where -import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit import javax.inject.Inject private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" @@ -54,7 +48,8 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val getProfileInfoTask: GetProfileInfoTask, private val setDisplayNameTask: SetDisplayNameTask, - private val setAvatarUrlTask: SetAvatarUrlTask) : ProfileService { + private val setAvatarUrlTask: SetAvatarUrlTask, + private val fileUploader: FileUploader) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { val params = GetProfileInfoTask.Params(userId) @@ -83,38 +78,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable { - val cancelableBag = CancelableBag() - val workerParams = UploadAvatarWorker.Params(sessionId, newAvatarUri, fileName) - val workerData = WorkerParamsFactory.toData(workerParams) - - val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerData) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("${userId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork) - .enqueue() - - cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id)) - - taskExecutor.executorScope.launch(coroutineDispatchers.main) { - workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id) - .observeForever { info -> - if (info != null && info.state.isFinished) { - val result = WorkerParamsFactory.fromData(info.outputData) - cancelableBag.add( - setAvatarUrlTask - .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = result!!.imageUrl!!)) { - callback = matrixCallback - } - .executeBy(taskExecutor) - ) - } + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { + val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") + setAvatarUrlTask + .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) { + callback = matrixCallback } + .executeBy(taskExecutor) } - return cancelableBag } override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 7cc061d0ae..14465cf631 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -18,8 +18,6 @@ package im.vector.matrix.android.internal.session.room.state import android.net.Uri import androidx.lifecycle.LiveData -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -28,19 +26,15 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider -import im.vector.matrix.android.internal.session.content.UploadAvatarWorker +import im.vector.matrix.android.internal.session.content.FileUploader import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" @@ -50,7 +44,8 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private private val sendStateTask: SendStateTask, @SessionId private val sessionId: String, private val workManagerProvider: WorkManagerProvider, - private val coroutineDispatchers: MatrixCoroutineDispatchers + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val fileUploader: FileUploader ) : StateService { @AssistedInject.Factory @@ -130,38 +125,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private } override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { - val cancelableBag = CancelableBag() - val workerParams = UploadAvatarWorker.Params(sessionId, avatarUri, fileName) - val workerData = WorkerParamsFactory.toData(workerParams) - - val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerData) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("${roomId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork) - .enqueue() - - cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id)) - - taskExecutor.executorScope.launch(coroutineDispatchers.main) { - workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id) - .observeForever { info -> - if (info != null && info.state.isFinished) { - val result = WorkerParamsFactory.fromData(info.outputData) - cancelableBag.add( - sendStateEvent( - eventType = EventType.STATE_ROOM_AVATAR, - body = mapOf("url" to result?.imageUrl!!), - callback = callback, - stateKey = null - ) - ) - } - } + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + callback = callback, + stateKey = null + ) } - return cancelableBag } } From 512e4f0ce335b18369695b2a42f8c6cd97d542eb Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 09:18:14 +0300 Subject: [PATCH 17/29] Create UCropHelper with default settings. --- .../preview/AttachmentsPreviewFragment.kt | 30 +--------- .../features/media/BigImageViewerActivity.kt | 30 +--------- .../riotx/features/media/UCropHelper.kt | 55 +++++++++++++++++++ .../roomprofile/RoomProfileFragment.kt | 31 +---------- .../settings/VectorSettingsGeneralFragment.kt | 31 +---------- 5 files changed, 65 insertions(+), 112 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index 3b1972ffbc..59629743b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -20,7 +20,6 @@ package im.vector.riotx.features.attachments.preview import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.content.Intent -import android.graphics.Color import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -38,7 +37,6 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.yalantis.ucrop.UCrop -import com.yalantis.ucrop.UCropActivity import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R @@ -52,6 +50,7 @@ import im.vector.riotx.core.utils.SnapOnScrollListener import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.attachSnapHelperWithListener import im.vector.riotx.core.utils.checkPermissions +import im.vector.riotx.features.media.createUCropWithDefaultSettings import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* import timber.log.Timber @@ -203,32 +202,7 @@ 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 - UCrop.of(uri, destinationFile.toUri()) - .withOptions( - UCrop.Options() - .apply { - setAllowedGestures( - /* tabScale = */ UCropActivity.SCALE, - /* tabRotate = */ UCropActivity.ALL, - /* tabAspectRatio = */ UCropActivity.SCALE - ) - setToolbarTitle(currentAttachment.name) - // Disable freestyle crop, usability was not easy - // setFreeStyleCropEnabled(true) - // Color used for toolbar icon and text - setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) - // Background - setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - // Status bar color (pb in dark mode, icon of the status bar are dark) - 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(colorProvider.getColor(R.color.riotx_accent)) - // Hide the logo (does not work) - setLogoColor(Color.TRANSPARENT) - } - ) + createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), currentAttachment.name) .start(requireContext(), this) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 10e752c8a7..8513d64da9 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.media import android.app.Activity import android.content.Context import android.content.Intent -import android.graphics.Color import android.net.Uri import android.os.Bundle import android.view.Menu @@ -27,7 +26,6 @@ import android.view.MenuItem import android.widget.Toast import androidx.core.net.toUri import com.yalantis.ucrop.UCrop -import com.yalantis.ucrop.UCropActivity import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent @@ -115,32 +113,8 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback private fun onRoomAvatarSelected(image: MultiPickerImageType) { val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val uri = image.contentUri - UCrop.of(uri, destinationFile.toUri()) - .withOptions( - UCrop.Options() - .apply { - setAllowedGestures( - /* tabScale = */ UCropActivity.SCALE, - /* tabRotate = */ UCropActivity.ALL, - /* tabAspectRatio = */ UCropActivity.SCALE - ) - setToolbarTitle(image.displayName) - // Disable freestyle crop, usability was not easy - // setFreeStyleCropEnabled(true) - // Color used for toolbar icon and text - setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) - // Background - setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - // Status bar color (pb in dark mode, icon of the status bar are dark) - 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(colorProvider.getColor(R.color.riotx_accent)) - // Hide the logo (does not work) - setLogoColor(Color.TRANSPARENT) - } - ) + createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName) + .apply { withAspectRatio(1f, 1f) } .start(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt b/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt new file mode 100644 index 0000000000..5e3ca5f483 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt @@ -0,0 +1,55 @@ +/* + * 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.riotx.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.riotx.R +import im.vector.riotx.features.themes.ThemeUtils + +fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: Uri, toolbarTitle: String?): UCrop { + return UCrop.of(source, destination) + .withOptions( + UCrop.Options() + .apply { + setAllowedGestures( + /* tabScale = */ UCropActivity.SCALE, + /* tabRotate = */ UCropActivity.ALL, + /* tabAspectRatio = */ UCropActivity.SCALE + ) + setToolbarTitle(toolbarTitle) + // 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)) + // Background + setRootViewBackgroundColor(ThemeUtils.getColor(context, 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)) + // 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)) + // Hide the logo (does not work) + setLogoColor(Color.TRANSPARENT) + } + ) +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index a1e9c2613b..2558c2bd32 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.roomprofile import android.app.Activity import android.content.Intent -import android.graphics.Color import android.net.Uri import android.os.Bundle import android.os.Parcelable @@ -35,7 +34,6 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.yalantis.ucrop.UCrop -import com.yalantis.ucrop.UCropActivity import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem @@ -58,6 +56,7 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBotto import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.media.BigImageViewerActivity +import im.vector.riotx.features.media.createUCropWithDefaultSettings import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.parcel.Parcelize @@ -273,32 +272,8 @@ class RoomProfileFragment @Inject constructor( private fun onRoomAvatarSelected(image: MultiPickerImageType) { val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val uri = image.contentUri - UCrop.of(uri, destinationFile.toUri()) - .withOptions( - UCrop.Options() - .apply { - setAllowedGestures( - /* tabScale = */ UCropActivity.SCALE, - /* tabRotate = */ UCropActivity.ALL, - /* tabAspectRatio = */ UCropActivity.SCALE - ) - setToolbarTitle(image.displayName) - // Disable freestyle crop, usability was not easy - // setFreeStyleCropEnabled(true) - // Color used for toolbar icon and text - setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color)) - // Background - setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background)) - // Status bar color (pb in dark mode, icon of the status bar are dark) - 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(colorProvider.getColor(R.color.riotx_accent)) - // Hide the logo (does not work) - setLogoColor(Color.TRANSPARENT) - } - ) + createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName) + .apply { withAspectRatio(1f, 1f) } .start(requireContext(), this) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index c344d7bf2f..69ca6f56ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -20,7 +20,6 @@ package im.vector.riotx.features.settings import android.app.Activity import android.content.Intent -import android.graphics.Color import android.net.Uri import android.text.Editable import android.util.Patterns @@ -41,7 +40,6 @@ 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 com.yalantis.ucrop.UCropActivity import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.failure.isInvalidPassword @@ -65,6 +63,7 @@ import im.vector.riotx.core.utils.getSizeOfFiles import im.vector.riotx.core.utils.toast import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs +import im.vector.riotx.features.media.createUCropWithDefaultSettings import im.vector.riotx.features.roomprofile.AvatarSelectorView import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.workers.signout.SignOutUiWorker @@ -429,32 +428,8 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect private fun onAvatarSelected(image: MultiPickerImageType) { val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val uri = image.contentUri - UCrop.of(uri, destinationFile.toUri()) - .withOptions( - UCrop.Options() - .apply { - setAllowedGestures( - /* tabScale = */ UCropActivity.SCALE, - /* tabRotate = */ UCropActivity.ALL, - /* tabAspectRatio = */ UCropActivity.SCALE - ) - setToolbarTitle(image.displayName) - // Disable freestyle crop, usability was not easy - // setFreeStyleCropEnabled(true) - // Color used for toolbar icon and text - setToolbarColor(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - setToolbarWidgetColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_primary_text_color)) - // Background - setRootViewBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - // Status bar color (pb in dark mode, icon of the status bar are dark) - setStatusBarColor(ThemeUtils.getColor(requireContext(), 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(requireContext(), R.color.riotx_accent)) - // Hide the logo (does not work) - setLogoColor(Color.TRANSPARENT) - } - ) + createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName) + .apply { withAspectRatio(1f, 1f) } .start(requireContext(), this) } From ad084e1fece86d32ebf6d7c3faeafc967757f536 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 10:20:59 +0300 Subject: [PATCH 18/29] Use simple dialog for avatar selection. --- .../features/media/BigImageViewerActivity.kt | 40 ++-- .../roomprofile/AvatarSelectorView.kt | 216 ------------------ .../roomprofile/RoomProfileFragment.kt | 31 +-- .../settings/VectorSettingsGeneralFragment.kt | 36 ++- 4 files changed, 56 insertions(+), 267 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 8513d64da9..9262fd3051 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.media import android.app.Activity +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.net.Uri @@ -31,19 +32,19 @@ import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.features.roomprofile.AvatarSelectorView +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.activity_big_image_viewer.* import java.io.File import javax.inject.Inject -class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback { +class BigImageViewerActivity : VectorBaseActivity() { @Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var colorProvider: ColorProvider + @Inject lateinit var stringProvider: StringProvider private var uri: Uri? = null - private lateinit var avatarSelector: AvatarSelectorView override fun getMenuRes() = R.menu.vector_big_avatar_viewer @@ -91,23 +92,24 @@ class BigImageViewerActivity : VectorBaseActivity(), AvatarSelectorView.Callback return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false) } - private fun showAvatarSelector() { - if (!::avatarSelector.isInitialized) { - avatarSelector = AvatarSelectorView(this, layoutInflater, this) - } - avatarSelector.show(bigImageViewerToolbar, false) - } - private var avatarCameraUri: Uri? = null - override fun onTypeSelected(type: AvatarSelectorView.Type) { - when (type) { - AvatarSelectorView.Type.CAMERA -> { - avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) - } - AvatarSelectorView.Type.GALLERY -> { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) - } - } + private fun showAvatarSelector() { + AlertDialog + .Builder(this) + .setItems(arrayOf( + stringProvider.getString(R.string.attachment_type_camera), + stringProvider.getString(R.string.attachment_type_gallery) + )) { dialog, which -> + dialog.cancel() + when (which) { + 0 -> { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) + } + 1 -> { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } + } + }.show() } private fun onRoomAvatarSelected(image: MultiPickerImageType) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt deleted file mode 100644 index 6636d533f1..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/AvatarSelectorView.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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.riotx.features.roomprofile - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.TargetApi -import android.content.Context -import android.graphics.drawable.BitmapDrawable -import android.os.Build -import android.util.Pair -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewAnimationUtils -import android.view.animation.Animation -import android.view.animation.AnimationSet -import android.view.animation.OvershootInterpolator -import android.view.animation.ScaleAnimation -import android.view.animation.TranslateAnimation -import android.widget.FrameLayout -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.PopupWindow -import androidx.core.view.doOnNextLayout -import com.amulyakhare.textdrawable.TextDrawable -import com.amulyakhare.textdrawable.util.ColorGenerator -import im.vector.riotx.R -import im.vector.riotx.core.extensions.getMeasurements -import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES -import im.vector.riotx.features.roomprofile.AvatarSelectorView.Callback -import kotlin.math.max - -private const val ANIMATION_DURATION = 250 - -/** - * This class is the view presenting choices for picking avatar. - * It will return result through [Callback]. - */ -class AvatarSelectorView(context: Context, - inflater: LayoutInflater, - var callback: Callback?) - : PopupWindow(context) { - - interface Callback { - fun onTypeSelected(type: Type) - } - - private val iconColorGenerator = ColorGenerator.MATERIAL - - private var galleryButton: ImageButton - private var cameraButton: ImageButton - - private var anchor: View? = null - - init { - val root = FrameLayout(context) - val layout = inflater.inflate(R.layout.view_avatar_selector, root, true) - galleryButton = layout.findViewById(R.id.avatarGalleryButton).configure(Type.GALLERY) - cameraButton = layout.findViewById(R.id.avatarCameraButton).configure(Type.CAMERA) - contentView = root - width = LinearLayout.LayoutParams.MATCH_PARENT - height = LinearLayout.LayoutParams.WRAP_CONTENT - animationStyle = 0 - @Suppress("DEPRECATION") - setBackgroundDrawable(BitmapDrawable()) - inputMethodMode = INPUT_METHOD_NOT_NEEDED - isFocusable = true - isTouchable = true - } - - fun show(anchor: View, isKeyboardOpen: Boolean) { - this.anchor = anchor - val anchorCoordinates = IntArray(2) - anchor.getLocationOnScreen(anchorCoordinates) - if (isKeyboardOpen) { - showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height) - } else { - val contentViewHeight = if (contentView.height == 0) { - contentView.getMeasurements().second - } else { - contentView.height - } - showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight) - } - contentView.doOnNextLayout { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateWindowInCircular(anchor, contentView) - } else { - animateWindowInTranslate(contentView) - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateButtonIn(galleryButton, ANIMATION_DURATION / 2) - animateButtonIn(cameraButton, ANIMATION_DURATION / 2) - } - } - - override fun dismiss() { - val capturedAnchor = anchor - if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateWindowOutCircular(capturedAnchor, contentView) - } else { - animateWindowOutTranslate(contentView) - } - } - - private fun animateButtonIn(button: View, delay: Int) { - val animation = AnimationSet(true) - val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f) - animation.addAnimation(scale) - animation.interpolator = OvershootInterpolator(1f) - animation.duration = ANIMATION_DURATION.toLong() - animation.startOffset = delay.toLong() - button.startAnimation(animation) - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private fun animateWindowInCircular(anchor: View, contentView: View) { - val coordinates = getClickCoordinates(anchor, contentView) - val animator = ViewAnimationUtils.createCircularReveal(contentView, - coordinates.first, - coordinates.second, - 0f, - max(contentView.width, contentView.height).toFloat()) - animator.duration = ANIMATION_DURATION.toLong() - animator.start() - } - - private fun animateWindowInTranslate(contentView: View) { - val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f) - animation.duration = ANIMATION_DURATION.toLong() - getContentView().startAnimation(animation) - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private fun animateWindowOutCircular(anchor: View, contentView: View) { - val coordinates = getClickCoordinates(anchor, contentView) - val animator = ViewAnimationUtils.createCircularReveal(getContentView(), - coordinates.first, - coordinates.second, - max(getContentView().width, getContentView().height).toFloat(), - 0f) - - animator.duration = ANIMATION_DURATION.toLong() - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super@AvatarSelectorView.dismiss() - } - }) - animator.start() - } - - private fun animateWindowOutTranslate(contentView: View) { - val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat()) - animation.duration = ANIMATION_DURATION.toLong() - animation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) {} - - override fun onAnimationEnd(animation: Animation) { - super@AvatarSelectorView.dismiss() - } - - override fun onAnimationRepeat(animation: Animation) {} - }) - - getContentView().startAnimation(animation) - } - - private fun getClickCoordinates(anchor: View, contentView: View): Pair { - val anchorCoordinates = IntArray(2) - anchor.getLocationOnScreen(anchorCoordinates) - val contentCoordinates = IntArray(2) - contentView.getLocationOnScreen(contentCoordinates) - val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2 - val y = anchorCoordinates[1] - contentCoordinates[1] - return Pair(x, y) - } - - private fun ImageButton.configure(type: Type): ImageButton { - this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal)) - this.setOnClickListener(TypeClickListener(type)) - return this - } - - private inner class TypeClickListener(private val type: Type) : View.OnClickListener { - - override fun onClick(v: View) { - dismiss() - callback?.onTypeSelected(type) - } - } - - /** - * The all possible types to pick with their required permissions. - */ - enum class Type(val permissionsBit: Int) { - CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), - GALLERY(PERMISSIONS_FOR_WRITING_FILES), - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 2558c2bd32..0b5025e086 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -47,6 +47,9 @@ import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.features.crypto.util.toImageRes @@ -76,7 +79,7 @@ class RoomProfileFragment @Inject constructor( private val avatarRenderer: AvatarRenderer, val roomProfileViewModelFactory: RoomProfileViewModel.Factory, val colorProvider: ColorProvider -) : VectorBaseFragment(), RoomProfileController.Callback, AvatarSelectorView.Callback { +) : VectorBaseFragment(), RoomProfileController.Callback { private val roomProfileArgs: RoomProfileArgs by args() private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel @@ -85,8 +88,6 @@ class RoomProfileFragment @Inject constructor( private var appBarStateChangeListener: AppBarStateChangeListener? = null - private lateinit var avatarSelector: AvatarSelectorView - override fun getLayoutResId() = R.layout.fragment_matrix_profile override fun getMenuRes() = R.menu.vector_room_profile @@ -251,21 +252,25 @@ class RoomProfileFragment @Inject constructor( } private fun showAvatarSelector() { - if (!::avatarSelector.isInitialized) { - avatarSelector = AvatarSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomProfileFragment) - } - avatarSelector.show(vector_coordinator_layout, false) + AlertDialog + .Builder(requireContext()) + .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 - override fun onTypeSelected(type: AvatarSelectorView.Type) { - when (type) { - AvatarSelectorView.Type.CAMERA -> { + 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) } - AvatarSelectorView.Type.GALLERY -> { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) - } + } else { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 69ca6f56ca..17739c2503 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -64,7 +64,6 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.media.createUCropWithDefaultSettings -import im.vector.riotx.features.roomprofile.AvatarSelectorView import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.workers.signout.SignOutUiWorker import im.vector.riotx.multipicker.MultiPicker @@ -76,7 +75,7 @@ import kotlinx.coroutines.withContext import java.io.File import java.util.UUID -class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelectorView.Callback { +class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general @@ -84,7 +83,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect private var mDisplayedEmails = ArrayList() private var mDisplayedPhoneNumber = ArrayList() - private lateinit var avatarSelector: AvatarSelectorView private var avatarCameraUri: Uri? = null private val mUserSettingsCategory by lazy { @@ -296,7 +294,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { - onTypeSelected(AvatarSelectorView.Type.CAMERA) + onAvatarTypeSelected(true) } } } @@ -404,24 +402,24 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment(), AvatarSelect * Update the avatar. */ private fun onUpdateAvatarClick() { - if (!::avatarSelector.isInitialized) { - avatarSelector = AvatarSelectorView(activity!!, activity!!.layoutInflater, this) - } - mUserAvatarPreference.mAvatarView?.let { - avatarSelector.show(it, false) - } + AlertDialog + .Builder(requireContext()) + .setItems(arrayOf( + getString(R.string.attachment_type_camera), + getString(R.string.attachment_type_gallery) + )) { dialog, which -> + dialog.cancel() + onAvatarTypeSelected(isCamera = (which == 0)) + }.show() } - override fun onTypeSelected(type: AvatarSelectorView.Type) { - when (type) { - AvatarSelectorView.Type.CAMERA -> { - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { - avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) - } - } - AvatarSelectorView.Type.GALLERY -> { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + 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) } + } else { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) } } From 32721caf5a2b3fa9d4208895adc4c36903bd3212 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 10:28:39 +0300 Subject: [PATCH 19/29] Code review fix. --- .../im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt index 1be5386c2f..80b78d0d70 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -71,7 +71,7 @@ abstract class ProfileActionItem : VectorEpoxyModel() override fun bind(holder: Holder) { super.bind(holder) holder.view.setOnClickListener(listener) - if (listener == null || !editable) { + if (listener == null) { holder.view.isClickable = false } holder.title.text = title From 4d6ba5a4913fb621e908f315067807f456cf5962 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 10:29:16 +0300 Subject: [PATCH 20/29] Check permission before triggering history readability click action. --- .../features/roomprofile/settings/RoomSettingsController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 3a989b201a..57e1a80ca3 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -101,7 +101,7 @@ class RoomSettingsController @Inject constructor( dividerColor = dividerColor, divider = false, editable = data.actionPermissions.canChangeHistoryReadability, - action = { callback?.onHistoryVisibilityClicked() } + action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } ) if (roomSummary.isEncrypted) { From 2650453d4b7a4f19240fca6b47d66bbc3eeaaee0 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 12:48:11 +0300 Subject: [PATCH 21/29] Add room alias first before setting the canonical alias. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 4 ++ .../api/session/room/state/StateService.kt | 5 ++ .../android/internal/session/room/RoomAPI.kt | 9 ++++ .../internal/session/room/RoomModule.kt | 5 ++ .../session/room/alias/AddRoomAliasBody.kt | 28 +++++++++++ .../session/room/alias/AddRoomAliasTask.kt | 47 +++++++++++++++++++ .../session/room/state/DefaultStateService.kt | 16 +++++-- .../settings/RoomSettingsViewModel.kt | 7 +-- 8 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index e1a4e3ac7b..50f4948d4b 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -111,6 +111,10 @@ class RxRoom(private val room: Room) { room.updateName(name, it) } + fun addRoomAlias(alias: String): Completable = completableBuilder { + room.addRoomAlias(alias, it) + } + fun updateCanonicalAlias(alias: String): Completable = completableBuilder { room.updateCanonicalAlias(alias, it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index f22e3d59f0..7378100dbe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -37,6 +37,11 @@ interface StateService { */ fun updateName(name: String, callback: MatrixCallback): Cancelable + /** + * Add new alias to the room. + */ + fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable + /** * Update the canonical alias of the room */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index e01e58856f..59fc0efbc0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason @@ -311,6 +312,14 @@ internal interface RoomAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call + /** + * Add alias to the room. + * @param roomAlias the room alias. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun addRoomAlias(@Path("roomAlias") roomAlias: String, + @Body body: AddRoomAliasBody): Call + /** * Inform that the user is starting to type or has stopped typing */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 0572a37506..5e84920fbd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasTask +import im.vector.matrix.android.internal.session.room.alias.DefaultAddRoomAliasTask import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask @@ -190,6 +192,9 @@ internal abstract class RoomModule { @Binds abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask + @Binds + abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask + @Binds abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt new file mode 100644 index 0000000000..0150c277ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.android.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddRoomAliasBody( + /** + * Required. The room id which the alias will be added to. + */ + @Json(name = "room_id") val roomId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt new file mode 100644 index 0000000000..baad3a3cde --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.alias + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddRoomAliasTask : Task { + data class Params( + val roomId: String, + val roomAlias: String + ) +} + +internal class DefaultAddRoomAliasTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : AddRoomAliasTask { + + override suspend fun execute(params: AddRoomAliasTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.addRoomAlias( + roomAlias = params.roomAlias, + body = AddRoomAliasBody( + roomId = params.roomId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 14465cf631..4cf1725060 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -28,9 +28,8 @@ import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.android.internal.di.SessionId -import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.content.FileUploader +import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback @@ -42,10 +41,9 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private private val stateEventDataSource: StateEventDataSource, private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask, - @SessionId private val sessionId: String, - private val workManagerProvider: WorkManagerProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val fileUploader: FileUploader + private val fileUploader: FileUploader, + private val addRoomAliasTask: AddRoomAliasTask ) : StateService { @AssistedInject.Factory @@ -106,6 +104,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } + override fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable { + return addRoomAliasTask + .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable { return sendStateEvent( eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 8fc699e4c4..7e8363bf33 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -129,8 +129,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } - if (summary?.canonicalAlias != state.newAlias) { - operationList.add(room.rx().updateCanonicalAlias(state.newAlias ?: "")) + if (state.newAlias != null && summary?.canonicalAlias != state.newAlias) { + operationList.add(room.rx().addRoomAlias(state.newAlias)) + operationList.add(room.rx().updateCanonicalAlias(state.newAlias)) } if (state.newHistoryVisibility != null) { @@ -139,7 +140,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: Observable .fromIterable(operationList) - .flatMapCompletable { it } + .concatMapCompletable { it } .subscribe( { postLoading(false) From 5f788d962e6319786af5ad402c70a69e5261d9d4 Mon Sep 17 00:00:00 2001 From: onurays Date: Mon, 29 Jun 2020 16:09:54 +0300 Subject: [PATCH 22/29] Use AlertDialog from v7 compat lib. --- .../im/vector/riotx/features/media/BigImageViewerActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 9262fd3051..9465152c1b 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.media import android.app.Activity -import android.app.AlertDialog import android.content.Context import android.content.Intent import android.net.Uri @@ -25,6 +24,7 @@ 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.riotx.R From b3d4d201954453670079d28a0075737e313f669b Mon Sep 17 00:00:00 2001 From: onurays Date: Tue, 30 Jun 2020 10:59:20 +0300 Subject: [PATCH 23/29] Check permission before trying to reach Camera. --- .../features/media/BigImageViewerActivity.kt | 33 ++++++++++++++----- .../roomprofile/RoomProfileFragment.kt | 9 +++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 9465152c1b..2448621f58 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -33,6 +33,10 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.multipicker.MultiPicker import im.vector.riotx.multipicker.entity.MultiPickerImageType import kotlinx.android.synthetic.main.activity_big_image_viewer.* @@ -92,7 +96,6 @@ class BigImageViewerActivity : VectorBaseActivity() { return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false) } - private var avatarCameraUri: Uri? = null private fun showAvatarSelector() { AlertDialog .Builder(this) @@ -101,17 +104,21 @@ class BigImageViewerActivity : VectorBaseActivity() { stringProvider.getString(R.string.attachment_type_gallery) )) { dialog, which -> dialog.cancel() - when (which) { - 0 -> { - avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this) - } - 1 -> { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) - } - } + 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) + } + } else { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(this) + } + } + private fun onRoomAvatarSelected(image: MultiPickerImageType) { val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val uri = image.contentUri @@ -148,6 +155,14 @@ class BigImageViewerActivity : VectorBaseActivity() { super.onActivityResult(requestCode, resultCode, data) } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + 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)) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 0b5025e086..a5f57b6d38 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -49,6 +49,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.startSharePlainTextIntent @@ -311,6 +312,14 @@ class RoomProfileFragment @Inject constructor( super.onActivityResult(requestCode, resultCode, data) } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + when (requestCode) { + PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true) + } + } + } + private fun onAvatarCropped(uri: Uri?) { if (uri != null) { roomProfileViewModel.handle(RoomProfileAction.ChangeRoomAvatar(uri, getFilenameFromUri(context, uri))) From e0ea0c195b120de2a753251fad421930b9dfb4d4 Mon Sep 17 00:00:00 2001 From: onurays Date: Tue, 30 Jun 2020 11:57:04 +0300 Subject: [PATCH 24/29] Hide save action after saving completed. --- .../features/roomprofile/settings/RoomSettingsViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 7e8363bf33..050bac5cf2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -66,7 +66,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY), roomSummary = async, newName = roomSummary?.displayName, - newTopic = roomSummary?.topic + newTopic = roomSummary?.topic, + newAlias = roomSummary?.canonicalAlias ) } @@ -145,7 +146,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: { postLoading(false) setState { copy(newHistoryVisibility = null) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } + setState { copy(showSaveAction = false) } _viewEvents.post(RoomSettingsViewEvents.Success) }, { From cca6d0e96767c8255376847e58b0c6db3bd9e248 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 15:52:40 +0200 Subject: [PATCH 25/29] Cleanup --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 3 +- .../api/session/room/state/StateService.kt | 3 +- .../session/profile/DefaultProfileService.kt | 6 -- .../session/room/state/DefaultStateService.kt | 5 +- .../features/media/BigImageViewerActivity.kt | 6 +- .../roomprofile/RoomProfileFragment.kt | 10 +-- .../roomprofile/RoomProfileViewModel.kt | 22 +++-- .../settings/RoomSettingsController.kt | 2 +- .../settings/RoomSettingsFragment.kt | 13 ++- .../settings/RoomSettingsViewModel.kt | 89 ++++++++++--------- .../settings/RoomSettingsViewState.kt | 2 +- .../item_form_text_input_with_button.xml | 4 +- .../main/res/layout/view_avatar_selector.xml | 10 +-- 13 files changed, 88 insertions(+), 87 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 50f4948d4b..b91949778d 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState @@ -119,7 +120,7 @@ class RxRoom(private val room: Room) { room.updateCanonicalAlias(alias, it) } - fun updateHistoryReadability(readability: String) = completableBuilder { + fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { room.updateHistoryReadability(readability, it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 7378100dbe..9c70baefd3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional @@ -50,7 +51,7 @@ interface StateService { /** * Update the history readability of the room */ - fun updateHistoryReadability(readability: String, callback: MatrixCallback): Cancelable + fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable /** * Update the avatar of the room diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index c76426b2cd..06127b6c6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -28,8 +28,6 @@ import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.SessionId -import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.content.FileUploader import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -38,12 +36,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import io.realm.kotlin.where import javax.inject.Inject -private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" - internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, @SessionDatabase private val monarchy: Monarchy, - @SessionId private val sessionId: String, - private val workManagerProvider: WorkManagerProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val getProfileInfoTask: GetProfileInfoTask, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 4cf1725060..198b601ea5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict @@ -35,8 +36,6 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" - internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, private val taskExecutor: TaskExecutor, @@ -121,7 +120,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } - override fun updateHistoryReadability(readability: String, callback: MatrixCallback): Cancelable { + override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable { return sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, body = mapOf("history_visibility" to readability), diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 2448621f58..e13bcf9dde 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -97,15 +97,15 @@ class BigImageViewerActivity : VectorBaseActivity() { } private fun showAvatarSelector() { - AlertDialog - .Builder(this) + AlertDialog.Builder(this) .setItems(arrayOf( stringProvider.getString(R.string.attachment_type_camera), stringProvider.getString(R.string.attachment_type_gallery) )) { dialog, which -> dialog.cancel() onAvatarTypeSelected(isCamera = (which == 0)) - }.show() + } + .show() } private var avatarCameraUri: Uri? = null diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index a5f57b6d38..f0cb29ea6b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -46,7 +46,6 @@ import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.riotx.core.utils.allGranted @@ -78,8 +77,7 @@ data class RoomProfileArgs( class RoomProfileFragment @Inject constructor( private val roomProfileController: RoomProfileController, private val avatarRenderer: AvatarRenderer, - val roomProfileViewModelFactory: RoomProfileViewModel.Factory, - val colorProvider: ColorProvider + val roomProfileViewModelFactory: RoomProfileViewModel.Factory ) : VectorBaseFragment(), RoomProfileController.Callback { private val roomProfileArgs: RoomProfileArgs by args() @@ -253,15 +251,15 @@ class RoomProfileFragment @Inject constructor( } private fun showAvatarSelector() { - AlertDialog - .Builder(requireContext()) + AlertDialog.Builder(requireContext()) .setItems(arrayOf( getString(R.string.attachment_type_camera), getString(R.string.attachment_type_gallery) )) { dialog, which -> dialog.cancel() onAvatarTypeSelected(isCamera = (which == 0)) - }.show() + } + .show() } private var avatarCameraUri: Uri? = null diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index 88e14432bb..4ffb2d96d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -68,10 +68,12 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() - powerLevelsContentLive.subscribe { - val powerLevelsHelper = PowerLevelsHelper(it) - setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) } - }.disposeOnClear() + powerLevelsContentLive + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) } + } + .disposeOnClear() } override fun handle(action: RoomProfileAction) = when (action) { @@ -111,11 +113,13 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini 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)) - }) + .subscribe( + { + _viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess) + }, + { + _viewEvents.post(RoomProfileViewEvents.Failure(it)) + }) .disposeOnClear() } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 57e1a80ca3..cf3945bf4a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -86,7 +86,7 @@ class RoomSettingsController @Inject constructor( formEditTextItem { id("alias") enabled(data.actionPermissions.canChangeCanonicalAlias) - value(data.newAlias ?: roomSummary.canonicalAlias) + value(data.newCanonicalAlias ?: roomSummary.canonicalAlias) hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address)) onTextChange { text -> diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index e6b0ad92af..3b808a21d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -34,7 +34,6 @@ import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs @@ -45,8 +44,7 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, - private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider + private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), RoomSettingsController.Callback { private val viewModel: RoomSettingsViewModel by fragmentViewModel() @@ -154,12 +152,13 @@ class RoomSettingsFragment @Inject constructor( return@withState } + // TODO Create a formatter for this enum, it's done 3 times in the project private fun formatHistoryVisibility(historyVisibility: RoomHistoryVisibility): String { return when (historyVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) + RoomHistoryVisibility.SHARED -> getString(R.string.notice_room_visibility_shared) + RoomHistoryVisibility.INVITED -> getString(R.string.notice_room_visibility_invited) + RoomHistoryVisibility.JOINED -> getString(R.string.notice_room_visibility_joined) + RoomHistoryVisibility.WORLD_READABLE -> getString(R.string.notice_room_visibility_world_readable) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 050bac5cf2..254673f7b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -27,11 +27,11 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable -import java.util.Locale class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, private val session: Session) @@ -55,6 +55,30 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: init { observeRoomSummary() + observeState() + } + + private fun observeState() { + selectSubscribe( + RoomSettingsViewState::newName, + RoomSettingsViewState::newCanonicalAlias, + RoomSettingsViewState::newTopic, + RoomSettingsViewState::newHistoryVisibility, + RoomSettingsViewState::roomSummary) { newName, + newAlias, + newTopic, + newHistoryVisibility, + asyncSummary -> + val summary = asyncSummary() + setState { + copy( + showSaveAction = summary?.displayName != newName + || summary?.topic != newTopic + || summary?.canonicalAlias != newAlias + || newHistoryVisibility != null + ) + } + } } private fun observeRoomSummary() { @@ -67,53 +91,35 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: roomSummary = async, newName = roomSummary?.displayName, newTopic = roomSummary?.topic, - newAlias = roomSummary?.canonicalAlias + newCanonicalAlias = roomSummary?.canonicalAlias ) } val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() - powerLevelsContentLive.subscribe { - val powerLevelsHelper = PowerLevelsHelper(it) - val permissions = RoomSettingsViewState.ActionPermissions( - canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId), - canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId), - canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId), - canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId) - ) - setState { copy(actionPermissions = permissions) } - }.disposeOnClear() + powerLevelsContentLive + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomSettingsViewState.ActionPermissions( + canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId), + canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId), + canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId), + canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId) + ) + setState { copy(actionPermissions = permissions) } + } + .disposeOnClear() } override fun handle(action: RoomSettingsAction) { when (action) { is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() - is RoomSettingsAction.SetRoomName -> { - setState { copy(newName = action.newName) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } - } - is RoomSettingsAction.SetRoomTopic -> { - setState { copy(newTopic = action.newTopic) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } - } - is RoomSettingsAction.SetRoomHistoryVisibility -> { - setState { copy(newHistoryVisibility = action.visibility) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } - } - is RoomSettingsAction.SetRoomAlias -> { - setState { copy(newAlias = action.alias) } - setState { copy(showSaveAction = shouldShowSaveAction(this)) } - } + 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.SetRoomAlias -> setState { copy(newCanonicalAlias = action.alias) } is RoomSettingsAction.Save -> saveSettings() - } - } - - private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean { - val summary = state.roomSummary.invoke() - return summary?.displayName != state.newName - || summary?.topic != state.newTopic - || summary?.canonicalAlias != state.newAlias - || state.newHistoryVisibility != null + }.exhaustive } private fun saveSettings() = withState { state -> @@ -130,13 +136,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } - if (state.newAlias != null && summary?.canonicalAlias != state.newAlias) { - operationList.add(room.rx().addRoomAlias(state.newAlias)) - operationList.add(room.rx().updateCanonicalAlias(state.newAlias)) + if (state.newCanonicalAlias != null && summary?.canonicalAlias != state.newCanonicalAlias.takeIf { it.isNotEmpty() }) { + operationList.add(room.rx().addRoomAlias(state.newCanonicalAlias)) + operationList.add(room.rx().updateCanonicalAlias(state.newCanonicalAlias)) } if (state.newHistoryVisibility != null) { - operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility.name.toLowerCase(Locale.ROOT))) + operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility)) } Observable @@ -146,7 +152,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: { postLoading(false) setState { copy(newHistoryVisibility = null) } - setState { copy(showSaveAction = false) } _viewEvents.post(RoomSettingsViewEvents.Success) }, { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index 6eea5d0d64..a86fbf8cfa 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -32,7 +32,7 @@ data class RoomSettingsViewState( val newName: String? = null, val newTopic: String? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, - val newAlias: String? = null, + val newCanonicalAlias: String? = null, val showSaveAction: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { diff --git a/vector/src/main/res/layout/item_form_text_input_with_button.xml b/vector/src/main/res/layout/item_form_text_input_with_button.xml index ccab8af3c1..8a7e60c006 100644 --- a/vector/src/main/res/layout/item_form_text_input_with_button.xml +++ b/vector/src/main/res/layout/item_form_text_input_with_button.xml @@ -32,11 +32,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/layout_horizontal_margin" + android:background="?attr/colorAccent" app:layout_constraintBottom_toBottomOf="@id/formTextInputTextInputLayout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/formTextInputTextInputLayout" - android:background="?attr/colorAccent" - tools:text="Add"/> + tools:text="Add" /> @@ -28,9 +28,9 @@ + android:src="@drawable/ic_attachment_camera_white_24dp" + tools:background="@color/riotx_accent" /> + android:src="@drawable/ic_attachment_gallery_white_24dp" + tools:background="@color/riotx_accent" /> Date: Tue, 30 Jun 2020 16:01:07 +0200 Subject: [PATCH 26/29] Create RoomHistoryVisibilityFormatter --- .../timeline/format/NoticeEventFormatter.kt | 9 ++--- .../format/RoomHistoryVisibilityFormatter.kt | 36 +++++++++++++++++++ .../settings/RoomSettingsAction.kt | 2 +- .../settings/RoomSettingsController.kt | 19 +++------- .../settings/RoomSettingsFragment.kt | 20 +++++------ .../settings/RoomSettingsViewModel.kt | 2 +- 6 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 37debace89..89e170e25e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent -import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.matrix.android.api.session.room.model.RoomJoinRules import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent @@ -47,6 +46,7 @@ import timber.log.Timber import javax.inject.Inject class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder, + private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val sp: StringProvider) { private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId @@ -223,12 +223,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - val formattedVisibility = when (historyVisibility) { - RoomHistoryVisibility.SHARED -> sp.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> sp.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> sp.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> sp.getString(R.string.notice_room_visibility_world_readable) - } + val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility) return if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_made_future_room_visibility_by_you, formattedVisibility) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt new file mode 100644 index 0000000000..c5de02f57f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt @@ -0,0 +1,36 @@ +/* + * 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.riotx.features.home.room.detail.timeline.format + +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import javax.inject.Inject + +class RoomHistoryVisibilityFormatter @Inject constructor( + private val stringProvider: StringProvider +) { + + fun format(roomHistoryVisibility: RoomHistoryVisibility): String { + return when (roomHistoryVisibility) { + RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) + RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) + RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) + RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index 4c0cc97a0d..908b72606d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -23,7 +23,7 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() - data class SetRoomAlias(val alias: String) : RoomSettingsAction() + data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index cf3945bf4a..94177159f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.roomprofile.settings import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction @@ -27,11 +26,12 @@ import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.form.formEditTextItem +import im.vector.riotx.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import javax.inject.Inject -// TODO Add other feature here (waiting for design) class RoomSettingsController @Inject constructor( private val stringProvider: StringProvider, + private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, colorProvider: ColorProvider ) : TypedEpoxyController() { @@ -55,7 +55,7 @@ class RoomSettingsController @Inject constructor( val roomSummary = data?.roomSummary?.invoke() ?: return val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" - val newHistoryVisibility = data.newHistoryVisibility?.let { formatRoomHistoryVisibility(it) } + val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) } buildProfileSection( stringProvider.getString(R.string.settings) @@ -127,17 +127,6 @@ class RoomSettingsController @Inject constructor( private fun formatRoomHistoryVisibilityEvent(event: Event): String? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - - return formatRoomHistoryVisibility(historyVisibility) - } - - private fun formatRoomHistoryVisibility(historyVisibility: RoomHistoryVisibility): String { - val formattedVisibility = when (historyVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) - } - return formattedVisibility + return roomHistoryVisibilityFormatter.format(historyVisibility) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index 3b808a21d6..840ec9f00c 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -36,6 +36,7 @@ import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.toast import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import im.vector.riotx.features.roomprofile.RoomProfileArgs import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* @@ -44,6 +45,7 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, + private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), RoomSettingsController.Callback { @@ -141,7 +143,11 @@ class RoomSettingsFragment @Inject constructor( AlertDialog.Builder(requireContext()).apply { setTitle(R.string.room_settings_room_read_history_rules_pref_title) - setSingleChoiceItems(historyVisibilities.map { formatHistoryVisibility(it) }.toTypedArray(), currentHistoryVisibilityIndex) { dialog, which -> + setSingleChoiceItems( + historyVisibilities + .map { roomHistoryVisibilityFormatter.format(it) } + .toTypedArray(), + currentHistoryVisibilityIndex) { dialog, which -> if (which != currentHistoryVisibilityIndex) { viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(historyVisibilities[which])) } @@ -152,17 +158,7 @@ class RoomSettingsFragment @Inject constructor( return@withState } - // TODO Create a formatter for this enum, it's done 3 times in the project - private fun formatHistoryVisibility(historyVisibility: RoomHistoryVisibility): String { - return when (historyVisibility) { - RoomHistoryVisibility.SHARED -> getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> getString(R.string.notice_room_visibility_world_readable) - } - } - override fun onAliasChanged(alias: String) { - viewModel.handle(RoomSettingsAction.SetRoomAlias(alias)) + viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 254673f7b1..736f571508 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -117,7 +117,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: 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.SetRoomAlias -> setState { copy(newCanonicalAlias = action.alias) } + is RoomSettingsAction.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) } is RoomSettingsAction.Save -> saveSettings() }.exhaustive } From da472ea8582e1af97a061aacab833481ed08da96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 16:10:27 +0200 Subject: [PATCH 27/29] Use name instead of computed displayName --- .../matrix/android/api/session/room/model/RoomSummary.kt | 2 ++ .../android/internal/database/mapper/RoomSummaryMapper.kt | 1 + .../android/internal/database/model/RoomSummaryEntity.kt | 1 + .../internal/session/room/summary/RoomSummaryUpdater.kt | 3 +++ .../features/roomprofile/settings/RoomSettingsViewModel.kt | 6 +++--- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 6f21b9eeae..3cb118f0ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -27,7 +27,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent */ data class RoomSummary constructor( val roomId: String, + // Computed display name val displayName: String = "", + val name: String = "", val topic: String = "", val avatarUrl: String = "", val canonicalAlias: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index d7bc0a17ae..0754ecb7d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -35,6 +35,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa return RoomSummary( roomId = roomSummaryEntity.roomId, displayName = roomSummaryEntity.displayName ?: "", + name = roomSummaryEntity.name ?: "", topic = roomSummaryEntity.topic ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "", isDirect = roomSummaryEntity.isDirect, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 521ab85a75..acfd484deb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -28,6 +28,7 @@ internal open class RoomSummaryEntity( @PrimaryKey var roomId: String = "", var displayName: String? = "", var avatarUrl: String? = "", + var name: String? = "", var topic: String? = "", var latestPreviewableEvent: TimelineEventEntity? = null, var heroes: RealmList = RealmList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt index 82b2cf3658..fad9938387 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent +import im.vector.matrix.android.api.session.room.model.RoomNameContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate @@ -107,6 +108,7 @@ internal class RoomSummaryUpdater @Inject constructor( val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) + val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root @@ -122,6 +124,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) + roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel()?.name roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 736f571508..4e406e0093 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -72,7 +72,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = asyncSummary() setState { copy( - showSaveAction = summary?.displayName != newName + showSaveAction = summary?.name != newName || summary?.topic != newTopic || summary?.canonicalAlias != newAlias || newHistoryVisibility != null @@ -89,7 +89,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: copy( historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY), roomSummary = async, - newName = roomSummary?.displayName, + newName = roomSummary?.name, newTopic = roomSummary?.topic, newCanonicalAlias = roomSummary?.canonicalAlias ) @@ -129,7 +129,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = state.roomSummary.invoke() - if (summary?.displayName != state.newName) { + if (summary?.name != state.newName) { operationList.add(room.rx().updateName(state.newName ?: "")) } if (summary?.topic != state.newTopic) { From e658ef184d104d97613c5e8ab596530aba3d3141 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 16:18:56 +0200 Subject: [PATCH 28/29] Fix issue with save action visibility --- .../features/roomprofile/settings/RoomSettingsViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index 4e406e0093..e198375cfb 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -65,7 +65,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState::newTopic, RoomSettingsViewState::newHistoryVisibility, RoomSettingsViewState::roomSummary) { newName, - newAlias, + newCanonicalAlias, newTopic, newHistoryVisibility, asyncSummary -> @@ -74,7 +74,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: copy( showSaveAction = summary?.name != newName || summary?.topic != newTopic - || summary?.canonicalAlias != newAlias + || summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() } || newHistoryVisibility != null ) } From 962e11a740faba906e6078ca2582f05e3f39f1e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 16:31:37 +0200 Subject: [PATCH 29/29] Onuray's remark :) --- .../vector/riotx/features/roomprofile/RoomProfileViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index 4ffb2d96d4..373dd6b56c 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -119,7 +119,8 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini }, { _viewEvents.post(RoomProfileViewEvents.Failure(it)) - }) + } + ) .disposeOnClear() } }