Add Avatar: SDK

Also add remove avatar action, and add Crop UX
This commit is contained in:
Benoit Marty 2020-10-20 14:58:20 +02:00 committed by Benoit Marty
parent 1f9712d8a2
commit 4b8c31d806
12 changed files with 100 additions and 23 deletions

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.api.session.room.model.create package org.matrix.android.sdk.api.session.room.model.create
import android.net.Uri
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@ -51,6 +52,11 @@ class CreateRoomParams {
*/ */
var topic: String? = null var topic: String? = null
/**
* If this is not null, the image uri will be sent to the media server and will be set as a room avatar.
*/
var avatarUri: Uri? = null
/** /**
* A list of user IDs to invite to the room. * A list of user IDs to invite to the room.
* This will tell the server to invite everyone in the list to the newly created room. * This will tell the server to invite everyone in the list to the newly created room.

View File

@ -16,10 +16,10 @@
package org.matrix.android.sdk.internal.session.room.create package org.matrix.android.sdk.internal.session.room.create
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import java.security.InvalidParameterException import java.security.InvalidParameterException
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
internal class CreateRoomBodyBuilder @Inject constructor( internal class CreateRoomBodyBuilder @Inject constructor(
@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private val crossSigningService: CrossSigningService, private val crossSigningService: CrossSigningService,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val identityStore: IdentityStore, private val identityStore: IdentityStore,
private val fileUploader: FileUploader,
@AuthenticatedIdentity @AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider private val accessTokenProvider: AccessTokenProvider
) { ) {
@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
val initialStates = listOfNotNull( val initialStates = listOfNotNull(
buildEncryptionWithAlgorithmEvent(params), buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params) buildHistoryVisibilityEvent(params),
buildAvatarEvent(params)
) )
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor(
) )
} }
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
return params.avatarUri?.let { avatarUri ->
// First upload the image, ignoring any error
tryOrNull {
fileUploader.uploadFromUri(
uri = avatarUri,
filename = UUID.randomUUID().toString(),
mimeType = "image/jpeg")
}
?.let { response ->
Event(
type = EventType.STATE_ROOM_AVATAR,
stateKey = "",
content = mapOf("url" to response.contentUri)
)
}
}
}
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? { private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
return params.historyVisibility return params.historyVisibility
?.let { ?.let {
val contentMap = mapOf("history_visibility" to it)
Event( Event(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY, type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "", stateKey = "",
content = contentMap.toContent()) content = mapOf("history_visibility" to it)
)
} }
} }
@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
if (it != MXCRYPTO_ALGORITHM_MEGOLM) { if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
throw InvalidParameterException("Unsupported algorithm: $it") throw InvalidParameterException("Unsupported algorithm: $it")
} }
val contentMap = mapOf("algorithm" to it)
Event( Event(
type = EventType.STATE_ROOM_ENCRYPTION, type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "", stateKey = "",
content = contentMap.toContent() content = mapOf("algorithm" to it)
) )
} }
} }

View File

@ -16,7 +16,9 @@
package im.vector.app.features.form package im.vector.app.features.form
import android.net.Uri import android.net.Uri
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
@ -43,6 +45,9 @@ abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarI
@EpoxyAttribute @EpoxyAttribute
var clickListener: ClickListener? = null var clickListener: ClickListener? = null
@EpoxyAttribute
var deleteListener: ClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.image.onClick(clickListener?.takeIf { enabled }) holder.image.onClick(clickListener?.takeIf { enabled })
@ -51,9 +56,12 @@ abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarI
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.placeholder(R.drawable.bg_accent) .placeholder(R.drawable.bg_accent)
.into(holder.image) .into(holder.image)
holder.delete.isVisible = imageUri != null
holder.delete.onClick(deleteListener?.takeIf { enabled })
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val image by bind<ImageView>(R.id.itemEditableAvatarImage) val image by bind<ImageView>(R.id.itemEditableAvatarImage)
val delete by bind<View>(R.id.itemEditableAvatarDelete)
} }
} }

View File

@ -122,7 +122,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri val uri = image.contentUri
createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName) createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName)
.apply { withAspectRatio(1f, 1f) } .withAspectRatio(1f, 1f)
.start(this) .start(this)
} }

View File

@ -16,11 +16,11 @@
package im.vector.app.features.roomdirectory.createroom package im.vector.app.features.roomdirectory.createroom
import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.lib.multipicker.entity.MultiPickerImageType
sealed class CreateRoomAction : VectorViewModelAction { sealed class CreateRoomAction : VectorViewModelAction {
data class SetAvatar(val image: MultiPickerImageType) : CreateRoomAction() data class SetAvatar(val imageUri: Uri?) : CreateRoomAction()
data class SetName(val name: String) : CreateRoomAction() data class SetName(val name: String) : CreateRoomAction()
data class SetTopic(val topic: String) : CreateRoomAction() data class SetTopic(val topic: String) : CreateRoomAction()
data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction() data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction()

View File

@ -73,8 +73,9 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
formEditableAvatarItem { formEditableAvatarItem {
id("avatar") id("avatar")
enabled(enableFormElement) enabled(enableFormElement)
imageUri(viewState.avatar?.contentUri) imageUri(viewState.avatarUri)
clickListener { listener?.onAvatarChange() } clickListener { listener?.onAvatarChange() }
deleteListener { listener?.onAvatarDelete() }
} }
settingsSectionTitleItem { settingsSectionTitleItem {
id("nameSection") id("nameSection")
@ -156,6 +157,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
} }
interface Listener { interface Listener {
fun onAvatarDelete()
fun onAvatarChange() fun onAvatarChange()
fun onNameChange(newName: String) fun onNameChange(newName: String)
fun onTopicChange(newTopic: String) fun onTopicChange(newTopic: String)

View File

@ -16,22 +16,27 @@
package im.vector.app.features.roomdirectory.createroom package im.vector.app.features.roomdirectory.createroom
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.net.toUri
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.lib.multipicker.entity.MultiPickerImageType import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.fragment_create_room.* import kotlinx.android.synthetic.main.fragment_create_room.*
import timber.log.Timber import timber.log.Timber
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class CreateRoomFragment @Inject constructor( class CreateRoomFragment @Inject constructor(
@ -68,12 +73,33 @@ class CreateRoomFragment @Inject constructor(
createRoomController.listener = this createRoomController.listener = this
} }
override fun onAvatarDelete() {
viewModel.handle(CreateRoomAction.SetAvatar(null))
}
override fun onAvatarChange() { override fun onAvatarChange() {
galleryOrCameraDialogHelper.show() galleryOrCameraDialogHelper.show()
} }
override fun onImageReady(image: MultiPickerImageType) { override fun onImageReady(image: MultiPickerImageType) {
viewModel.handle(CreateRoomAction.SetAvatar(image)) val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
.withAspectRatio(1f, 1f)
.start(requireContext(), this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// TODO handle this one (Ucrop lib)
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
UCrop.REQUEST_CROP ->
viewModel.handle(CreateRoomAction.SetAvatar(data?.let { UCrop.getOutput(it) }))
}
}
} }
override fun onNameChange(newName: String) { override fun onNameChange(newName: String) {

View File

@ -101,7 +101,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
}.exhaustive }.exhaustive
} }
private fun setAvatar(action: CreateRoomAction.SetAvatar) = setState { copy(avatar = action.image) } private fun setAvatar(action: CreateRoomAction.SetAvatar) = setState { copy(avatarUri = action.imageUri) }
private fun setName(action: CreateRoomAction.SetName) = setState { copy(roomName = action.name) } private fun setName(action: CreateRoomAction.SetName) = setState { copy(roomName = action.name) }
@ -131,6 +131,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
.apply { .apply {
name = state.roomName.takeIf { it.isNotBlank() } name = state.roomName.takeIf { it.isNotBlank() }
topic = state.roomTopic.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() }
avatarUri = state.avatarUri
// Directory visibility // Directory visibility
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE
// Public room // Public room

View File

@ -16,13 +16,13 @@
package im.vector.app.features.roomdirectory.createroom package im.vector.app.features.roomdirectory.createroom
import android.net.Uri
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.lib.multipicker.entity.MultiPickerImageType
data class CreateRoomViewState( data class CreateRoomViewState(
val avatar: MultiPickerImageType? = null, val avatarUri: Uri? = null,
val roomName: String = "", val roomName: String = "",
val roomTopic: String = "", val roomTopic: String = "",
val isPublic: Boolean = false, val isPublic: Boolean = false,

View File

@ -282,7 +282,7 @@ class RoomProfileFragment @Inject constructor(
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri val uri = image.contentUri
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName) createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
.apply { withAspectRatio(1f, 1f) } .withAspectRatio(1f, 1f)
.start(requireContext(), this) .start(requireContext(), this)
} }

View File

@ -312,7 +312,7 @@ class VectorSettingsGeneralFragment :
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
val uri = image.contentUri val uri = image.contentUri
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName) createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
.apply { withAspectRatio(1f, 1f) } .withAspectRatio(1f, 1f)
.start(requireContext(), this) .start(requireContext(), this)
} }

View File

@ -10,18 +10,32 @@
<ImageView <ImageView
android:id="@+id/itemEditableAvatarImage" android:id="@+id/itemEditableAvatarImage"
android:layout_width="112dp" android:layout_width="128dp"
android:layout_height="112dp" android:layout_height="128dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/itemEditableAvatarDelete"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/circle"
android:scaleType="center"
android:src="@drawable/ic_delete"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/itemEditableAvatarImage"
app:layout_constraintTop_toTopOf="@+id/itemEditableAvatarImage"
app:tint="@color/riotx_destructive_accent"
tools:ignore="MissingPrefix"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/itemEditableAvatarPicto" android:id="@+id/itemEditableAvatarPicto"
android:layout_width="24dp" android:layout_width="32dp"
android:layout_height="24dp" android:layout_height="32dp"
android:background="@drawable/circle" android:background="@drawable/circle"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_add_image" android:src="@drawable/ic_add_image"