diff --git a/CHANGES.md b/CHANGES.md index bd8c6e1bf5..d7bd5da965 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Rework sending Event management (#154) + - New room creation screen: set topic and avatar in the room creation form (#2078) Bugfix 🐛: - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index 0860b25d69..892a865751 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -16,6 +16,7 @@ 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.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility @@ -51,6 +52,11 @@ class CreateRoomParams { */ 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. * This will tell the server to invite everyone in the list to the newly created room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 7e28200ccd..632fcab70b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -16,10 +16,10 @@ 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.events.model.Event 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.toMedium 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.di.AuthenticatedIdentity 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.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import java.security.InvalidParameterException +import java.util.UUID import javax.inject.Inject internal class CreateRoomBodyBuilder @Inject constructor( @@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( private val crossSigningService: CrossSigningService, private val deviceListManager: DeviceListManager, private val identityStore: IdentityStore, + private val fileUploader: FileUploader, @AuthenticatedIdentity private val accessTokenProvider: AccessTokenProvider ) { @@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( val initialStates = listOfNotNull( buildEncryptionWithAlgorithmEvent(params), - buildHistoryVisibilityEvent(params) + buildHistoryVisibilityEvent(params), + buildAvatarEvent(params) ) .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? { return params.historyVisibility ?.let { - val contentMap = mapOf("history_visibility" to it) - Event( type = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "", - content = contentMap.toContent()) + content = mapOf("history_visibility" to it) + ) } } @@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor( if (it != MXCRYPTO_ALGORITHM_MEGOLM) { throw InvalidParameterException("Unsupported algorithm: $it") } - val contentMap = mapOf("algorithm" to it) - Event( type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "", - content = contentMap.toContent() + content = mapOf("algorithm" to it) ) } } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 6e879df7ab..63a3fad109 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===82 +enum class===83 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 4439c2fb8c..067a1a4dfe 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -45,6 +45,10 @@ parser.add_argument('-e', '--expecting', type=int, help='the expected number of artifacts. If omitted, no check will be done.') +parser.add_argument('-i', + '--ignoreErrors', + help='Ignore errors that can be ignored. Build state and number of artifacts.', + action="store_true") parser.add_argument('-d', '--directory', default="", @@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit')) print(" git commit message : \"%s\"" % data0.get('message')) print(" build state : %s" % data0.get('state')) +error = False + if data0.get('state') != 'passed': print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state')) - exit(1) + if args.ignoreErrors: + error = True + else: + exit(1) ### Fetch artifacts list @@ -110,8 +119,11 @@ data = json.loads(r.content.decode()) print(" %d artifact(s) found." % len(data)) if args.expecting is not None and args.expecting != len(data): - print("Error, expecting %d artifacts and found %d." % (args.expecting, len(data))) - exit(1) + print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data))) + if args.ignoreErrors: + error = True + else: + exit(1) if args.verbose: print("Json data:") @@ -128,8 +140,6 @@ else: if not args.simulate: os.mkdir(targetDir) -error = False - for elt in data: if args.verbose: print() @@ -157,7 +167,7 @@ for elt in data: print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash)) if error: - print("❌ Error(s) occurred, check the log") + print("❌ Error(s) occurred, please check the log") exit(1) else: print("Done!") diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt new file mode 100644 index 0000000000..1f357b4cfc --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.dialogs + +import android.app.Activity +import android.net.Uri +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.lib.multipicker.MultiPicker +import im.vector.lib.multipicker.entity.MultiPickerImageType + +class GalleryOrCameraDialogHelper( + private val fragment: Fragment +) { + interface Listener { + fun onImageReady(image: MultiPickerImageType) + } + + private val activity by lazy { fragment.requireActivity() } + + private val listener: Listener = fragment as? Listener ?: error("Fragment must implements GalleryOrCameraDialogHelper.Listener") + + private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted -> + if (allGranted) { + doOpenCamera() + } + } + + private val takePhotoActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + avatarCameraUri?.let { uri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(fragment.requireContext(), uri) + ?.let { listener.onImageReady(it) } + } + } + } + + private val pickImageActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(fragment.requireContext(), activityResult.data) + .firstOrNull() + ?.let { listener.onImageReady(it) } + } + } + + private enum class Type { + Gallery, + Camera + } + + fun show() { + AlertDialog.Builder(fragment.requireContext()) + .setItems(arrayOf( + fragment.getString(R.string.attachment_type_camera), + fragment.getString(R.string.attachment_type_gallery) + )) { dialog, which -> + dialog.cancel() + onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery) + } + .show() + } + + private fun onAvatarTypeSelected(type: Type) { + when (type) { + Type.Gallery -> + MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) + Type.Camera -> + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment.requireContext(), takePhotoActivityResultLauncher) + } + } + } + + private var avatarCameraUri: Uri? = null + private fun doOpenCamera() { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher) + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt new file mode 100644 index 0000000000..9849f22f87 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.form + +import android.net.Uri +import android.view.View +import android.widget.ImageView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.bumptech.glide.request.RequestOptions +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_editable_avatar) +abstract class FormEditableAvatarItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var imageUri: Uri? = null + + @EpoxyAttribute + var clickListener: ClickListener? = null + + @EpoxyAttribute + var deleteListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.imageContainer.onClick(clickListener?.takeIf { enabled }) + GlideApp.with(holder.image) + .load(imageUri) + .apply(RequestOptions.circleCropTransform()) + .into(holder.image) + holder.delete.isVisible = imageUri != null + holder.delete.onClick(deleteListener?.takeIf { enabled }) + } + + class Holder : VectorEpoxyHolder() { + val imageContainer by bind(R.id.itemEditableAvatarImageContainer) + val image by bind(R.id.itemEditableAvatarImage) + val delete by bind(R.id.itemEditableAvatarDelete) + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt new file mode 100644 index 0000000000..2d2a5e7aec --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.form + +import android.widget.Button +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_form_submit_button) +abstract class FormSubmitButtonItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonClickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (buttonTitleId != null) { + holder.button.setText(buttonTitleId!!) + } else { + holder.button.setTextOrHide(buttonTitle) + } + + holder.button.isEnabled = enabled + holder.button.onClick(buttonClickListener) + } + + class Holder : VectorEpoxyHolder() { + val button by bind