Updating room avatar is implemented.

This commit is contained in:
onurays 2020-06-12 16:39:22 +03:00 committed by Benoit Marty
parent a6e4a328b3
commit 52eec06110
12 changed files with 293 additions and 22 deletions

View File

@ -16,6 +16,7 @@
package im.vector.matrix.rx package im.vector.matrix.rx
import android.net.Uri
import im.vector.matrix.android.api.query.QueryStringValue 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.Event
import im.vector.matrix.android.api.session.room.Room 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<Unit> { fun updateName(name: String): Completable = completableBuilder<Unit> {
room.updateName(name, it) room.updateName(name, it)
} }
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it)
}
} }
fun Room.rx(): RxRoom { fun Room.rx(): RxRoom {

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.api.session.room.state package im.vector.matrix.android.api.session.room.state
import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
@ -36,6 +37,11 @@ interface StateService {
*/ */
fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Update the avatar of the room
*/
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -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.cache.CacheModule
import im.vector.matrix.android.internal.session.call.CallModule 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.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.content.UploadContentWorker
import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
@ -117,6 +118,8 @@ internal interface SessionComponent {
fun inject(worker: UploadContentWorker) fun inject(worker: UploadContentWorker)
fun inject(worker: UploadAvatarWorker)
fun inject(worker: SyncWorker) fun inject(worker: SyncWorker)
fun inject(worker: AddHttpPusherWorker) fun inject(worker: AddHttpPusherWorker)

View File

@ -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<Params>(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))
}
}

View File

@ -16,7 +16,10 @@
package im.vector.matrix.android.internal.session.room.state package im.vector.matrix.android.internal.session.room.state
import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback 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.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService 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.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional 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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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, internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource, private val stateEventDataSource: StateEventDataSource,
private val taskExecutor: TaskExecutor, 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 { ) : StateService {
@AssistedInject.Factory @AssistedInject.Factory
@ -93,4 +110,40 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
stateKey = null stateKey = null
) )
} }
override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable {
val cancelableBag = CancelableBag()
val workerParams = UploadAvatarWorker.Params(sessionId, avatarUri, fileName)
val workerData = WorkerParamsFactory.toData(workerParams)
val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadAvatarWorker>()
.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<UploadAvatarWorker.OutputParams>(info.outputData)
cancelableBag.add(
sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = mapOf("url" to result?.imageUrl!!),
callback = callback,
stateKey = null
)
)
}
}
}
return cancelableBag
}
} }

View File

@ -25,10 +25,12 @@ import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_profile_action) @EpoxyModelClass(layout = R.layout.item_profile_action)
@ -51,6 +53,12 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
@EpoxyAttribute @EpoxyAttribute
var accessoryRes: Int = 0 var accessoryRes: Int = 0
@EpoxyAttribute
var accessoryMatrixItem: MatrixItem? = null
@EpoxyAttribute
var avatarRenderer: AvatarRenderer? = null
@EpoxyAttribute @EpoxyAttribute
var editable: Boolean = true var editable: Boolean = true
@ -93,6 +101,13 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
holder.secondaryAccessory.isVisible = false 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) { if (editableRes != 0 && editable) {
val tintColorSecondary = if (destructive) { val tintColorSecondary = if (destructive) {
tintColor tintColor

View File

@ -19,8 +19,10 @@ package im.vector.riotx.core.epoxy.profiles
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyController 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.ClickListener
import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.features.home.AvatarRenderer
fun EpoxyController.buildProfileSection(title: String) { fun EpoxyController.buildProfileSection(title: String) {
profileSectionItem { profileSectionItem {
@ -41,7 +43,9 @@ fun EpoxyController.buildProfileAction(
destructive: Boolean = false, destructive: Boolean = false,
divider: Boolean = true, divider: Boolean = true,
action: ClickListener? = null, action: ClickListener? = null,
@DrawableRes accessory: Int = 0 @DrawableRes accessory: Int = 0,
accessoryMatrixItem: MatrixItem? = null,
avatarRenderer: AvatarRenderer? = null
) { ) {
profileActionItem { profileActionItem {
iconRes(icon) iconRes(icon)
@ -53,6 +57,8 @@ fun EpoxyController.buildProfileAction(
destructive(destructive) destructive(destructive)
title(title) title(title)
accessoryRes(accessory) accessoryRes(accessory)
accessoryMatrixItem(accessoryMatrixItem)
avatarRenderer(avatarRenderer)
listener { _ -> listener { _ ->
action?.invoke() action?.invoke()
} }

View File

@ -17,11 +17,12 @@
package im.vector.riotx.features.roomprofile.settings package im.vector.riotx.features.roomprofile.settings
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.multipicker.entity.MultiPickerImageType
sealed class RoomSettingsAction : VectorViewModelAction { sealed class RoomSettingsAction : VectorViewModelAction {
data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomName(val newName: String) : RoomSettingsAction()
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomAvatar(val newAvatarUrl: String) : RoomSettingsAction() data class SetRoomAvatar(val image: MultiPickerImageType) : RoomSettingsAction()
object EnableEncryption : RoomSettingsAction() object EnableEncryption : RoomSettingsAction()
object Save : RoomSettingsAction() object Save : RoomSettingsAction()
} }

View File

@ -17,16 +17,19 @@
package im.vector.riotx.features.roomprofile.settings package im.vector.riotx.features.roomprofile.settings
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.epoxy.profiles.buildProfileSection
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.form.formEditTextItem import im.vector.riotx.features.form.formEditTextItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
// TODO Add other feature here (waiting for design) // TODO Add other feature here (waiting for design)
class RoomSettingsController @Inject constructor( class RoomSettingsController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
colorProvider: ColorProvider colorProvider: ColorProvider
) : TypedEpoxyController<RoomSettingsViewState>() { ) : TypedEpoxyController<RoomSettingsViewState>() {
@ -35,6 +38,7 @@ class RoomSettingsController @Inject constructor(
fun onEnableEncryptionClicked() fun onEnableEncryptionClicked()
fun onNameChanged(name: String) fun onNameChanged(name: String)
fun onTopicChanged(topic: String) fun onTopicChanged(topic: String)
fun onPhotoClicked()
} }
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) 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) { if (roomSummary.isEncrypted) {
buildProfileAction( buildProfileAction(
id = "encryption", id = "encryption",

View File

@ -16,6 +16,8 @@
package im.vector.riotx.features.roomprofile.settings package im.vector.riotx.features.roomprofile.settings
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -34,6 +36,8 @@ import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roomprofile.RoomProfileArgs 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.fragment_room_setting_generic.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject import javax.inject.Inject
@ -62,7 +66,7 @@ class RoomSettingsFragment @Inject constructor(
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is RoomSettingsViewEvents.Failure -> showFailure(it.throwable) is RoomSettingsViewEvents.Failure -> showFailure(it.throwable)
is RoomSettingsViewEvents.Success -> showSuccess() is RoomSettingsViewEvents.Success -> showSuccess()
}.exhaustive }.exhaustive
} }
} }
@ -95,6 +99,17 @@ class RoomSettingsFragment @Inject constructor(
renderRoomSummary(viewState) 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() { override fun onEnableEncryptionClicked() {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.room_settings_enable_encryption_dialog_title) .setTitle(R.string.room_settings_enable_encryption_dialog_title)
@ -114,14 +129,27 @@ class RoomSettingsFragment @Inject constructor(
viewModel.handle(RoomSettingsAction.SetRoomTopic(topic)) viewModel.handle(RoomSettingsAction.SetRoomTopic(topic))
} }
private fun renderRoomSummary(state: RoomSettingsViewState) { override fun onPhotoClicked() {
waiting_view.isVisible = state.isLoading MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
}
state.roomSummary()?.let { private fun onRoomPhotoSelected(selectedImage: MultiPickerImageType) {
roomSettingsToolbarTitleView.text = it.displayName viewModel.handle(RoomSettingsAction.SetRoomAvatar(selectedImage))
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) }
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)
invalidateOptionsMenu()
} }
} }

View File

@ -28,6 +28,7 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Observable import io.reactivex.Observable
import java.util.UUID
class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState,
private val session: Session) private val session: Session)
@ -69,17 +70,17 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomSettingsAction) { override fun handle(action: RoomSettingsAction) {
when (action) { when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
is RoomSettingsAction.SetRoomName -> setState { is RoomSettingsAction.SetRoomName -> {
copy( setState { copy(newName = action.newName) }
newName = action.newName, setState { copy(showSaveAction = shouldShowSaveAction(this)) }
showSaveAction = shouldShowSaveAction(this)
)
} }
is RoomSettingsAction.SetRoomTopic -> setState { is RoomSettingsAction.SetRoomTopic -> {
copy( setState { copy(newTopic = action.newTopic) }
newTopic = action.newTopic, setState { copy(showSaveAction = shouldShowSaveAction(this)) }
showSaveAction = shouldShowSaveAction(this) }
) is RoomSettingsAction.SetRoomAvatar -> {
setState { copy(newAvatar = action.image) }
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
} }
is RoomSettingsAction.Save -> saveSettings() is RoomSettingsAction.Save -> saveSettings()
} }
@ -88,7 +89,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean { private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean {
val summary = state.roomSummary.invoke() val summary = state.roomSummary.invoke()
return summary?.displayName != state.newName || return summary?.displayName != state.newName ||
summary?.topic != state.newTopic summary?.topic != state.newTopic ||
state.newAvatar != null
} }
private fun saveSettings() = withState { state -> private fun saveSettings() = withState { state ->
@ -105,12 +107,18 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
operationList.add(room.rx().updateTopic(state.newTopic ?: "")) 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 Observable
.fromIterable(operationList) .fromIterable(operationList)
.flatMapCompletable { it } .flatMapCompletable { it }
.subscribe( .subscribe(
{ {
postLoading(false) postLoading(false)
setState { copy(newAvatar = null) }
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
_viewEvents.post(RoomSettingsViewEvents.Success) _viewEvents.post(RoomSettingsViewEvents.Success)
}, },
{ {

View File

@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.features.roomprofile.RoomProfileArgs import im.vector.riotx.features.roomprofile.RoomProfileArgs
import im.vector.riotx.multipicker.entity.MultiPickerImageType
data class RoomSettingsViewState( data class RoomSettingsViewState(
val roomId: String, val roomId: String,
@ -28,6 +29,7 @@ data class RoomSettingsViewState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val newName: String? = null, val newName: String? = null,
val newTopic: String? = null, val newTopic: String? = null,
val newAvatar: MultiPickerImageType? = null,
val showSaveAction: Boolean = false val showSaveAction: Boolean = false
) : MvRxState { ) : MvRxState {