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
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<Unit> {
room.updateName(name, it)
}
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it)
}
}
fun Room.rx(): RxRoom {

View File

@ -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<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 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.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)

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
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<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 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<ProfileActionItem.Holder>()
@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<ProfileActionItem.Holder>()
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

View File

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

View File

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

View File

@ -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<RoomSettingsViewState>() {
@ -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",

View File

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

View File

@ -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)
},
{

View File

@ -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 {