Merge pull request #2275 from vector-im/feature/bma/create_room

Feature/bma/create room
This commit is contained in:
Benoit Marty 2020-10-22 10:27:52 +02:00 committed by GitHub
commit 6a1238d2c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 527 additions and 181 deletions

View File

@ -6,6 +6,7 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- Rework sending Event management (#154) - Rework sending Event management (#154)
- New room creation screen: set topic and avatar in the room creation form (#2078)
Bugfix 🐛: Bugfix 🐛:
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)

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

@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # 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 ### 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 ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -45,6 +45,10 @@ parser.add_argument('-e',
'--expecting', '--expecting',
type=int, type=int,
help='the expected number of artifacts. If omitted, no check will be done.') 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', parser.add_argument('-d',
'--directory', '--directory',
default="", default="",
@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit'))
print(" git commit message : \"%s\"" % data0.get('message')) print(" git commit message : \"%s\"" % data0.get('message'))
print(" build state : %s" % data0.get('state')) print(" build state : %s" % data0.get('state'))
error = False
if data0.get('state') != 'passed': if data0.get('state') != 'passed':
print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state')) 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 ### Fetch artifacts list
@ -110,8 +119,11 @@ data = json.loads(r.content.decode())
print(" %d artifact(s) found." % len(data)) print(" %d artifact(s) found." % len(data))
if args.expecting is not None and args.expecting != 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))) print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
exit(1) if args.ignoreErrors:
error = True
else:
exit(1)
if args.verbose: if args.verbose:
print("Json data:") print("Json data:")
@ -128,8 +140,6 @@ else:
if not args.simulate: if not args.simulate:
os.mkdir(targetDir) os.mkdir(targetDir)
error = False
for elt in data: for elt in data:
if args.verbose: if args.verbose:
print() print()
@ -157,7 +167,7 @@ for elt in data:
print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash)) print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash))
if error: if error:
print("❌ Error(s) occurred, check the log") print("❌ Error(s) occurred, please check the log")
exit(1) exit(1)
else: else:
print("Done!") print("Done!")

View File

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

View File

@ -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<FormEditableAvatarItem.Holder>() {
@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<View>(R.id.itemEditableAvatarImageContainer)
val image by bind<ImageView>(R.id.itemEditableAvatarImage)
val delete by bind<View>(R.id.itemEditableAvatarDelete)
}
}

View File

@ -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<FormSubmitButtonItem.Holder>() {
@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<Button>(R.id.form_submit_button)
}
}

View File

@ -19,6 +19,7 @@ package im.vector.app.features.home
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.observeK import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment import im.vector.app.core.extensions.replaceChildFragment
@ -75,7 +76,7 @@ class HomeDrawerFragment @Inject constructor(
} }
// Debug menu // Debug menu
homeDrawerHeaderDebugView.isVisible = vectorPreferences.developerMode() homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
homeDrawerHeaderDebugView.debouncedClicks { homeDrawerHeaderDebugView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity()) navigator.openDebug(requireActivity())

View File

@ -33,7 +33,6 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.allGranted
@ -47,7 +46,6 @@ import javax.inject.Inject
class BigImageViewerActivity : VectorBaseActivity() { class BigImageViewerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var stringProvider: StringProvider
private var uri: Uri? = null private var uri: Uri? = null
@ -100,8 +98,8 @@ class BigImageViewerActivity : VectorBaseActivity() {
private fun showAvatarSelector() { private fun showAvatarSelector() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setItems(arrayOf( .setItems(arrayOf(
stringProvider.getString(R.string.attachment_type_camera), getString(R.string.attachment_type_camera),
stringProvider.getString(R.string.attachment_type_gallery) getString(R.string.attachment_type_gallery)
)) { dialog, which -> )) { dialog, which ->
dialog.cancel() dialog.cancel()
onAvatarTypeSelected(isCamera = (which == 0)) onAvatarTypeSelected(isCamera = (which == 0))
@ -124,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

@ -20,6 +20,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
@ -58,19 +59,19 @@ class RoomDirectoryActivity : VectorBaseActivity() {
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
is RoomDirectorySharedAction.Back -> onBackPressed() is RoomDirectorySharedAction.Back -> onBackPressed()
is RoomDirectorySharedAction.CreateRoom -> is RoomDirectorySharedAction.CreateRoom -> {
addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java)
// Transmit the filter to the createRoomViewModel
withState(roomDirectoryViewModel) {
createRoomViewModel.handle(CreateRoomAction.SetName(it.currentFilter))
}
}
is RoomDirectorySharedAction.ChangeProtocol -> is RoomDirectorySharedAction.ChangeProtocol ->
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomDirectoryPickerFragment::class.java) addFragmentToBackstack(R.id.simpleFragmentContainer, RoomDirectoryPickerFragment::class.java)
is RoomDirectorySharedAction.Close -> finish() is RoomDirectorySharedAction.Close -> finish()
} }
} }
.disposeOnDestroy() .disposeOnDestroy()
roomDirectoryViewModel.selectSubscribe(this, PublicRoomsViewState::currentFilter) { currentFilter ->
// Transmit the filter to the createRoomViewModel
createRoomViewModel.handle(CreateRoomAction.SetName(currentFilter))
}
} }
override fun initUiAndData() { override fun initUiAndData() {

View File

@ -16,12 +16,17 @@
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
sealed class CreateRoomAction : VectorViewModelAction { sealed class CreateRoomAction : VectorViewModelAction {
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 SetIsPublic(val isPublic: Boolean) : CreateRoomAction() data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction()
data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction() data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction()
data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction() data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
object Create : CreateRoomAction() object Create : CreateRoomAction()
object Reset : CreateRoomAction()
} }

View File

@ -26,7 +26,10 @@ import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.settingsSectionTitleItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formEditableAvatarItem
import im.vector.app.features.form.formSubmitButtonItem
import im.vector.app.features.form.formSwitchItem import im.vector.app.features.form.formSwitchItem
import javax.inject.Inject import javax.inject.Inject
@ -67,6 +70,17 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
} }
private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) { private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) {
formEditableAvatarItem {
id("avatar")
enabled(enableFormElement)
imageUri(viewState.avatarUri)
clickListener { listener?.onAvatarChange() }
deleteListener { listener?.onAvatarDelete() }
}
settingsSectionTitleItem {
id("nameSection")
titleResId(R.string.create_room_name_section)
}
formEditTextItem { formEditTextItem {
id("name") id("name")
enabled(enableFormElement) enabled(enableFormElement)
@ -77,6 +91,24 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.onNameChange(text) listener?.onNameChange(text)
} }
} }
settingsSectionTitleItem {
id("topicSection")
titleResId(R.string.create_room_topic_section)
}
formEditTextItem {
id("topic")
enabled(enableFormElement)
value(viewState.roomTopic)
hint(stringProvider.getString(R.string.create_room_topic_hint))
onTextChange { text ->
listener?.onTopicChange(text)
}
}
settingsSectionTitleItem {
id("settingsSection")
titleResId(R.string.create_room_settings_section)
}
formSwitchItem { formSwitchItem {
id("public") id("public")
enabled(enableFormElement) enabled(enableFormElement)
@ -116,13 +148,23 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.setIsEncrypted(value) listener?.setIsEncrypted(value)
} }
} }
formSubmitButtonItem {
id("submit")
enabled(enableFormElement)
buttonTitleId(R.string.create_room_action_create)
buttonClickListener { listener?.submit() }
}
} }
interface Listener { interface Listener {
fun onAvatarDelete()
fun onAvatarChange()
fun onNameChange(newName: String) fun onNameChange(newName: String)
fun onTopicChange(newTopic: String)
fun setIsPublic(isPublic: Boolean) fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean) fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
fun setIsEncrypted(isEncrypted: Boolean) fun setIsEncrypted(isEncrypted: Boolean)
fun retry() fun retry()
fun submit()
} }
} }

View File

@ -16,30 +16,43 @@
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.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.OnBackPressed
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 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(private val createRoomController: CreateRoomController) : VectorBaseFragment(), CreateRoomController.Listener { class CreateRoomFragment @Inject constructor(
private val createRoomController: CreateRoomController
) : VectorBaseFragment(),
CreateRoomController.Listener,
GalleryOrCameraDialogHelper.Listener,
OnBackPressed {
private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel
private val viewModel: CreateRoomViewModel by activityViewModel() private val viewModel: CreateRoomViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_create_room private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun getMenuRes() = R.menu.vector_room_creation override fun getLayoutResId() = R.layout.fragment_create_room
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -57,26 +70,48 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
super.onDestroyView() super.onDestroyView()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_create_room -> {
viewModel.handle(CreateRoomAction.Create)
true
}
else ->
super.onOptionsItemSelected(item)
}
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
createRoomForm.configureWith(createRoomController) createRoomForm.configureWith(createRoomController)
createRoomController.listener = this createRoomController.listener = this
} }
override fun onAvatarDelete() {
viewModel.handle(CreateRoomAction.SetAvatar(null))
}
override fun onAvatarChange() {
galleryOrCameraDialogHelper.show()
}
override fun onImageReady(image: MultiPickerImageType) {
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) {
viewModel.handle(CreateRoomAction.SetName(newName)) viewModel.handle(CreateRoomAction.SetName(newName))
} }
override fun onTopicChange(newTopic: String) {
viewModel.handle(CreateRoomAction.SetTopic(newTopic))
}
override fun setIsPublic(isPublic: Boolean) { override fun setIsPublic(isPublic: Boolean) {
viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) viewModel.handle(CreateRoomAction.SetIsPublic(isPublic))
} }
@ -89,11 +124,20 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted)) viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
} }
override fun submit() {
viewModel.handle(CreateRoomAction.Create)
}
override fun retry() { override fun retry() {
Timber.v("Retry") Timber.v("Retry")
viewModel.handle(CreateRoomAction.Create) viewModel.handle(CreateRoomAction.Create)
} }
override fun onBackPressed(toolbarButton: Boolean): Boolean {
viewModel.handle(CreateRoomAction.Reset)
return false
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
val async = state.asyncCreateRoomRequest val async = state.asyncCreateRoomRequest
if (async is Success) { if (async is Success) {

View File

@ -26,6 +26,7 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
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.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.getElementWellknown
@ -90,16 +91,32 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
override fun handle(action: CreateRoomAction) { override fun handle(action: CreateRoomAction) {
when (action) { when (action) {
is CreateRoomAction.SetAvatar -> setAvatar(action)
is CreateRoomAction.SetName -> setName(action) is CreateRoomAction.SetName -> setName(action)
is CreateRoomAction.SetTopic -> setTopic(action)
is CreateRoomAction.SetIsPublic -> setIsPublic(action) is CreateRoomAction.SetIsPublic -> setIsPublic(action)
is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action) is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action)
is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action)
is CreateRoomAction.Create -> doCreateRoom() is CreateRoomAction.Create -> doCreateRoom()
CreateRoomAction.Reset -> doReset()
}.exhaustive
}
private fun doReset() {
setState {
CreateRoomViewState(
isEncrypted = adminE2EByDefault,
hsAdminHasDisabledE2E = !adminE2EByDefault
)
} }
} }
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) }
private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) }
private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState {
copy( copy(
isPublic = action.isPublic, isPublic = action.isPublic,
@ -123,6 +140,8 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
val createRoomParams = CreateRoomParams() val createRoomParams = CreateRoomParams()
.apply { .apply {
name = state.roomName.takeIf { it.isNotBlank() } name = state.roomName.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,12 +16,15 @@
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
data class CreateRoomViewState( data class CreateRoomViewState(
val avatarUri: Uri? = null,
val roomName: String = "", val roomName: String = "",
val roomTopic: String = "",
val isPublic: Boolean = false, val isPublic: Boolean = false,
val isInRoomDirectory: Boolean = false, val isInRoomDirectory: Boolean = false,
val isEncrypted: Boolean = false, val isEncrypted: Boolean = false,

View File

@ -38,6 +38,7 @@ import com.yalantis.ucrop.UCrop
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener import im.vector.app.core.animations.AppBarStateChangeListener
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
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.extensions.copyOnLongClick import im.vector.app.core.extensions.copyOnLongClick
@ -46,10 +47,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.crypto.util.toImageRes import im.vector.app.features.crypto.util.toImageRes
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
@ -59,7 +57,6 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.entity.MultiPickerImageType import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.* import kotlinx.android.synthetic.main.fragment_matrix_profile.*
@ -80,7 +77,9 @@ class RoomProfileFragment @Inject constructor(
private val roomProfileController: RoomProfileController, private val roomProfileController: RoomProfileController,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory val roomProfileViewModelFactory: RoomProfileViewModel.Factory
) : VectorBaseFragment(), RoomProfileController.Callback { ) : VectorBaseFragment(),
RoomProfileController.Callback,
GalleryOrCameraDialogHelper.Listener {
private val roomProfileArgs: RoomProfileArgs by args() private val roomProfileArgs: RoomProfileArgs by args()
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -93,6 +92,8 @@ class RoomProfileFragment @Inject constructor(
override fun getMenuRes() = R.menu.vector_room_profile override fun getMenuRes() = R.menu.vector_room_profile
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
@ -272,70 +273,18 @@ class RoomProfileFragment @Inject constructor(
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "") val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
bigImageStartForActivityResult.launch(intent, options) bigImageStartForActivityResult.launch(intent, options)
} else if (it.canChangeAvatar) { } else if (it.canChangeAvatar) {
showAvatarSelector() galleryOrCameraDialogHelper.show()
} }
} }
private fun showAvatarSelector() { override fun onImageReady(image: MultiPickerImageType) {
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 val takePhotoPermissionActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
onAvatarTypeSelected(true)
}
}
private var avatarCameraUri: Uri? = null
private fun onAvatarTypeSelected(isCamera: Boolean) {
if (isCamera) {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), takePhotoPermissionActivityResultLauncher)) {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(requireActivity(), takePhotoActivityResultLauncher)
}
} else {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
}
private fun onRoomAvatarSelected(image: MultiPickerImageType) {
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)
} }
private val takePhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
avatarCameraUri?.let { uri ->
MultiPicker.get(MultiPicker.CAMERA)
.getTakenPhoto(requireContext(), uri)
?.let {
onRoomAvatarSelected(it)
}
}
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireContext(), activityResult.data)
.firstOrNull()?.let {
onRoomAvatarSelected(it)
}
}
}
private val bigImageStartForActivityResult = registerStartForActivityResult { activityResult -> private val bigImageStartForActivityResult = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) { if (activityResult.resultCode == Activity.RESULT_OK) {
activityResult.data?.let { onAvatarCropped(it.data) } activityResult.data?.let { onAvatarCropped(it.data) }

View File

@ -40,25 +40,21 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.yalantis.ucrop.UCrop 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.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.showPassword import im.vector.app.core.extensions.showPassword
import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.UserAvatarPreference
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.getSizeOfFiles import im.vector.app.core.utils.getSizeOfFiles
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.app.features.workers.signout.SignOutUiWorker import im.vector.app.features.workers.signout.SignOutUiWorker
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.entity.MultiPickerImageType import im.vector.lib.multipicker.entity.MultiPickerImageType
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -75,12 +71,14 @@ import org.matrix.android.sdk.rx.unwrap
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { class VectorSettingsGeneralFragment :
VectorSettingsBaseFragment(),
GalleryOrCameraDialogHelper.Listener {
override var titleRes = R.string.settings_general_title override var titleRes = R.string.settings_general_title
override val preferenceXmlRes = R.xml.vector_settings_general override val preferenceXmlRes = R.xml.vector_settings_general
private var avatarCameraUri: Uri? = null private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
private val mUserSettingsCategory by lazy { private val mUserSettingsCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!!
@ -154,7 +152,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
// Avatar // Avatar
mUserAvatarPreference.let { mUserAvatarPreference.let {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener { it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
onUpdateAvatarClick() galleryOrCameraDialogHelper.show()
false false
} }
} }
@ -279,30 +277,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
session.integrationManagerService().removeListener(integrationServiceListener) session.integrationManagerService().removeListener(integrationServiceListener)
} }
private val attachmentPhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
avatarCameraUri?.let { uri ->
MultiPicker.get(MultiPicker.CAMERA)
.getTakenPhoto(requireContext(), uri)
?.let {
onAvatarSelected(it)
}
}
}
}
private val attachmentImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
val data = activityResult.data ?: return@registerStartForActivityResult
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireContext(), data)
.firstOrNull()?.let {
onAvatarSelected(it)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// TODO handle this one (Ucrop lib) // TODO handle this one (Ucrop lib)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -334,42 +308,11 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
} }
} }
/** override fun onImageReady(image: MultiPickerImageType) {
* Update the avatar.
*/
private fun onUpdateAvatarClick() {
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 val takePhotoActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
onAvatarTypeSelected(true)
}
}
private fun onAvatarTypeSelected(isCamera: Boolean) {
if (isCamera) {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), takePhotoActivityResultLauncher)) {
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(requireActivity(), attachmentPhotoActivityResultLauncher)
}
} else {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(attachmentImageActivityResultLauncher)
}
}
private fun onAvatarSelected(image: MultiPickerImageType) {
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

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?riotx_header_panel_background" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="10dp"
android:viewportWidth="15"
android:viewportHeight="10">
<path
android:pathData="M0.5,2C0.5,0.8954 1.3954,0 2.5,0H12.5C13.6046,0 14.5,0.8954 14.5,2V8C14.5,9.1046 13.6046,10 12.5,10H2.5C1.3954,10 0.5,9.1046 0.5,8V2ZM11.5,5C11.5,7.2091 9.7091,9 7.5,9C5.2909,9 3.5,7.2091 3.5,5C3.5,2.7909 5.2909,1 7.5,1C9.7091,1 11.5,2.7909 11.5,5ZM7.5,7C8.6046,7 9.5,6.1046 9.5,5C9.5,3.8954 8.6046,3 7.5,3C6.3954,3 5.5,3.8954 5.5,5C5.5,6.1046 6.3954,7 7.5,7Z"
android:fillColor="#8F97A3"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/memberProfileInfoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="16dp">
<!-- I cannot do what I want using layer-list, do it manually here-->
<FrameLayout
android:id="@+id/itemEditableAvatarImageContainer"
android:layout_width="128dp"
android:layout_height="128dp"
android:background="@drawable/header_panel_round_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:src="@drawable/ic_add_image" />
<ImageView
android:id="@+id/itemEditableAvatarImage"
android:layout_width="128dp"
android:layout_height="128dp"
android:scaleType="center"
tools:alpha="0.3"
tools:src="@tools:sample/avatars" />
</FrameLayout>
<ImageView
android:id="@+id/itemEditableAvatarDelete"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/header_panel_round_background"
android:scaleType="center"
android:src="@drawable/ic_delete"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/itemEditableAvatarImageContainer"
app:layout_constraintTop_toTopOf="@+id/itemEditableAvatarImageContainer"
app:tint="@color/riotx_destructive_accent"
tools:ignore="MissingPrefix"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:minHeight="64dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/form_submit_button"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="@dimen/alerter_activity_vertical_margin"
tools:text="@string/auth_submit" />
</FrameLayout>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".features.roomdirectory.RoomDirectoryActivity">
<item
android:id="@+id/action_create_room"
android:title="@string/create_room_action_create"
app:showAsAction="always" />
</menu>

View File

@ -338,7 +338,7 @@
<string name="bottom_action_people">Άτομα</string> <string name="bottom_action_people">Άτομα</string>
<string name="home_filter_placeholder_people">Αναζήτηση ατόμων</string> <string name="home_filter_placeholder_people">Αναζήτηση ατόμων</string>
<string name="tab_title_search_people">ΑΤΟΜΑ</string> <string name="tab_title_search_people">ΑΤΟΜΑ</string>
<string name="resources_script">Λτνκ</string> <string name="resources_script">Grek</string>
<string name="notification_sync_init">Αρχικοποίηση υπηρεσίας</string> <string name="notification_sync_init">Αρχικοποίηση υπηρεσίας</string>
<string name="notification_listening_for_events">Αναμονή για συμβάντα</string> <string name="notification_listening_for_events">Αναμονή για συμβάντα</string>

View File

@ -1675,7 +1675,11 @@
<!-- Create room screen --> <!-- Create room screen -->
<string name="create_room_title">"New Room"</string> <string name="create_room_title">"New Room"</string>
<string name="create_room_action_create">"CREATE"</string> <string name="create_room_action_create">"CREATE"</string>
<string name="create_room_name_hint">"Room name"</string> <string name="create_room_name_section">"Room name"</string>
<string name="create_room_name_hint">"Name"</string>
<string name="create_room_topic_section">"Room topic (optional)"</string>
<string name="create_room_topic_hint">"Topic"</string>
<string name="create_room_settings_section">"Room settings"</string>
<string name="create_room_public_title">"Public"</string> <string name="create_room_public_title">"Public"</string>
<string name="create_room_public_description">"Anyone will be able to join this room"</string> <string name="create_room_public_description">"Anyone will be able to join this room"</string>
<string name="create_room_directory_title">"Room Directory"</string> <string name="create_room_directory_title">"Room Directory"</string>