mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge pull request #2282 from vector-im/feature/bma/room_settings
Feature/bma/room settings
This commit is contained in:
commit
1e60d6f6e8
@ -11,6 +11,7 @@ Improvements 🙌:
|
||||
- Add option to send with enter (#1195)
|
||||
- Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440)
|
||||
- Edit and remove icons are now visible on image attachment preview screen (#2294)
|
||||
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
|
||||
|
||||
Bugfix 🐛:
|
||||
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
|
||||
|
@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||
room.updateAvatar(avatarUri, fileName, it)
|
||||
}
|
||||
|
||||
fun deleteAvatar(): Completable = completableBuilder<Unit> {
|
||||
room.deleteAvatar(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
@ -58,6 +58,11 @@ interface StateService {
|
||||
*/
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Delete the avatar of the room
|
||||
*/
|
||||
fun deleteAvatar(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?
|
||||
|
@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = emptyMap(),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,12 +29,16 @@ 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
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_editable_avatar)
|
||||
abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
var avatarRenderer: AvatarRenderer? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var matrixItem: MatrixItem? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var enabled: Boolean = true
|
||||
@ -51,11 +55,15 @@ abstract class FormEditableAvatarItem : EpoxyModelWithHolder<FormEditableAvatarI
|
||||
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
|
||||
if (matrixItem != null) {
|
||||
avatarRenderer?.render(matrixItem!!, holder.image)
|
||||
} else {
|
||||
GlideApp.with(holder.image)
|
||||
.load(imageUri)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(holder.image)
|
||||
}
|
||||
holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true)
|
||||
holder.delete.onClick(deleteListener?.takeIf { enabled })
|
||||
}
|
||||
|
||||
|
@ -16,41 +16,25 @@
|
||||
|
||||
package im.vector.app.features.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
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.allGranted
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.lib.multipicker.MultiPicker
|
||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.android.synthetic.main.activity_big_image_viewer.*
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Simple Activity to display an avatar in fullscreen
|
||||
*/
|
||||
class BigImageViewerActivity : VectorBaseActivity() {
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
|
||||
private var uri: Uri? = null
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_big_avatar_viewer
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
@ -66,7 +50,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
uri = sessionHolder.getSafeActiveSession()
|
||||
val uri = sessionHolder.getSafeActiveSession()
|
||||
?.contentUrlResolver()
|
||||
?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
|
||||
?.toUri()
|
||||
@ -78,117 +62,14 @@ class BigImageViewerActivity : VectorBaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
menu.findItem(R.id.bigAvatarEditAction).isVisible = shouldShowEditAction()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.bigAvatarEditAction) {
|
||||
showAvatarSelector()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun shouldShowEditAction(): Boolean {
|
||||
return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false)
|
||||
}
|
||||
|
||||
private fun showAvatarSelector() {
|
||||
AlertDialog.Builder(this)
|
||||
.setItems(arrayOf(
|
||||
getString(R.string.attachment_type_camera),
|
||||
getString(R.string.attachment_type_gallery)
|
||||
)) { dialog, which ->
|
||||
dialog.cancel()
|
||||
onAvatarTypeSelected(isCamera = (which == 0))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
private fun onAvatarTypeSelected(isCamera: Boolean) {
|
||||
if (isCamera) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this, takePhotoActivityResultLauncher)
|
||||
}
|
||||
} else {
|
||||
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRoomAvatarSelected(image: MultiPickerImageType) {
|
||||
val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
|
||||
val uri = image.contentUri
|
||||
createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName)
|
||||
.withAspectRatio(1f, 1f)
|
||||
.start(this)
|
||||
}
|
||||
|
||||
private val takePhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
avatarCameraUri?.let { uri ->
|
||||
MultiPicker.get(MultiPicker.CAMERA)
|
||||
.getTakenPhoto(this, uri)
|
||||
?.let {
|
||||
onRoomAvatarSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
MultiPicker
|
||||
.get(MultiPicker.IMAGE)
|
||||
.getSelectedFiles(this, activityResult.data)
|
||||
.firstOrNull()?.let {
|
||||
onRoomAvatarSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (allGranted(grantResults)) {
|
||||
when (requestCode) {
|
||||
PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarCropped(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
setResult(Activity.RESULT_OK, Intent().setData(uri))
|
||||
this@BigImageViewerActivity.finish()
|
||||
} else {
|
||||
Toast.makeText(this, "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_TITLE = "EXTRA_TITLE"
|
||||
private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL"
|
||||
private const val EXTRA_CAN_EDIT_IMAGE = "EXTRA_CAN_EDIT_IMAGE"
|
||||
|
||||
fun newIntent(context: Context, title: String?, imageUrl: String, canEditImage: Boolean = false): Intent {
|
||||
fun newIntent(context: Context, title: String?, imageUrl: String): Intent {
|
||||
return Intent(context, BigImageViewerActivity::class.java).apply {
|
||||
putExtra(EXTRA_TITLE, title)
|
||||
putExtra(EXTRA_IMAGE_URL, imageUrl)
|
||||
putExtra(EXTRA_CAN_EDIT_IMAGE, canEditImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,14 +17,12 @@
|
||||
|
||||
package im.vector.app.features.roomprofile
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
|
||||
sealed class RoomProfileAction : VectorViewModelAction {
|
||||
object LeaveRoom : RoomProfileAction()
|
||||
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
|
||||
data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction()
|
||||
object ShareRoomProfile : RoomProfileAction()
|
||||
object CreateShortcut : RoomProfileAction()
|
||||
}
|
||||
|
@ -17,35 +17,26 @@
|
||||
|
||||
package im.vector.app.features.roomprofile
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.animations.AppBarStateChangeListener
|
||||
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.configureWith
|
||||
import im.vector.app.core.extensions.copyOnLongClick
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.copyToClipboard
|
||||
import im.vector.app.core.utils.startSharePlainTextIntent
|
||||
@ -56,8 +47,6 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomS
|
||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
|
||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||
import im.vector.app.features.media.BigImageViewerActivity
|
||||
import im.vector.app.features.media.createUCropWithDefaultSettings
|
||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
|
||||
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
|
||||
@ -65,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
@ -78,8 +66,7 @@ class RoomProfileFragment @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
|
||||
) : VectorBaseFragment(),
|
||||
RoomProfileController.Callback,
|
||||
GalleryOrCameraDialogHelper.Listener {
|
||||
RoomProfileController.Callback {
|
||||
|
||||
private val roomProfileArgs: RoomProfileArgs by args()
|
||||
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
|
||||
@ -92,8 +79,6 @@ class RoomProfileFragment @Inject constructor(
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_room_profile
|
||||
|
||||
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
|
||||
@ -113,11 +98,10 @@ class RoomProfileFragment @Inject constructor(
|
||||
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
|
||||
roomProfileViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is RoomProfileViewEvents.Loading -> showLoading(it.message)
|
||||
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
|
||||
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
|
||||
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
|
||||
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
|
||||
is RoomProfileViewEvents.Loading -> showLoading(it.message)
|
||||
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
|
||||
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
|
||||
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
|
||||
}.exhaustive
|
||||
}
|
||||
roomListQuickActionsSharedActionViewModel
|
||||
@ -158,14 +142,6 @@ class RoomProfileFragment @Inject constructor(
|
||||
else -> Timber.v("$action not handled")
|
||||
}
|
||||
|
||||
private fun onLeaveRoom() {
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
|
||||
private fun showError(throwable: Throwable) {
|
||||
showErrorInSnackbar(throwable)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
roomProfileController.callback = this
|
||||
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
|
||||
@ -268,46 +244,12 @@ class RoomProfileFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
|
||||
if (matrixItem.avatarUrl?.isNotEmpty() == true) {
|
||||
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
|
||||
bigImageStartForActivityResult.launch(intent, options)
|
||||
} else if (it.canChangeAvatar) {
|
||||
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)
|
||||
}
|
||||
|
||||
private val bigImageStartForActivityResult = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
activityResult.data?.let { onAvatarCropped(it.data) }
|
||||
}
|
||||
}
|
||||
|
||||
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 -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarCropped(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
roomProfileViewModel.handle(RoomProfileAction.ChangeRoomAvatar(uri, getFilenameFromUri(context, uri)))
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
matrixItem.avatarUrl
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { avatarUrl ->
|
||||
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), avatarUrl)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
|
||||
startActivity(intent, options.toBundle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
|
||||
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
|
||||
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
|
||||
|
||||
object OnChangeAvatarSuccess : RoomProfileViewEvents()
|
||||
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
|
||||
data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents()
|
||||
}
|
||||
|
@ -28,18 +28,15 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.ShortcutCreator
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.rx.RxRoom
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
import java.util.UUID
|
||||
|
||||
class RoomProfileViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: RoomProfileViewState,
|
||||
@ -65,33 +62,23 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
val rxRoom = room.rx()
|
||||
observeRoomSummary(rxRoom)
|
||||
observeBannedRoomMembers(rxRoom)
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
val rxRoom = room.rx()
|
||||
private fun observeRoomSummary(rxRoom: RxRoom) {
|
||||
rxRoom.liveRoomSummary()
|
||||
.unwrap()
|
||||
.execute {
|
||||
copy(roomSummary = it)
|
||||
}
|
||||
}
|
||||
|
||||
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
|
||||
|
||||
powerLevelsContentLive
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
setState {
|
||||
copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR))
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
|
||||
private fun observeBannedRoomMembers(rxRoom: RxRoom) {
|
||||
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
|
||||
.execute {
|
||||
copy(
|
||||
bannedMembership = it
|
||||
)
|
||||
copy(bannedMembership = it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +87,6 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
|
||||
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
|
||||
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
|
||||
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
|
||||
}.exhaustive
|
||||
}
|
||||
@ -142,18 +128,4 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeAvatar(action: RoomProfileAction.ChangeRoomAvatar) {
|
||||
_viewEvents.post(RoomProfileViewEvents.Loading())
|
||||
room.rx().updateAvatar(action.uri, action.fileName ?: UUID.randomUUID().toString())
|
||||
.subscribe(
|
||||
{
|
||||
_viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess)
|
||||
},
|
||||
{
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(it))
|
||||
}
|
||||
)
|
||||
.disposeOnClear()
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
data class RoomProfileViewState(
|
||||
val roomId: String,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized,
|
||||
val canChangeAvatar: Boolean = false
|
||||
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
|
||||
|
@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
|
||||
sealed class RoomSettingsAction : VectorViewModelAction {
|
||||
data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction()
|
||||
data class SetRoomName(val newName: String) : RoomSettingsAction()
|
||||
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
|
||||
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
|
||||
|
@ -23,20 +23,27 @@ import im.vector.app.core.epoxy.profiles.buildProfileSection
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.form.formEditTextItem
|
||||
import im.vector.app.features.form.formEditableAvatarItem
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomSettingsController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<RoomSettingsViewState>() {
|
||||
|
||||
interface Callback {
|
||||
// Delete the avatar, or cancel an avatar change
|
||||
fun onAvatarDelete()
|
||||
fun onAvatarChange()
|
||||
fun onEnableEncryptionClicked()
|
||||
fun onNameChanged(name: String)
|
||||
fun onTopicChanged(topic: String)
|
||||
@ -58,6 +65,25 @@ class RoomSettingsController @Inject constructor(
|
||||
val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: ""
|
||||
val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) }
|
||||
|
||||
formEditableAvatarItem {
|
||||
id("avatar")
|
||||
enabled(data.actionPermissions.canChangeAvatar)
|
||||
when (val avatarAction = data.avatarAction) {
|
||||
RoomSettingsViewState.AvatarAction.None -> {
|
||||
// Use the current value
|
||||
avatarRenderer(avatarRenderer)
|
||||
// We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
|
||||
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
|
||||
}
|
||||
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
|
||||
imageUri(null)
|
||||
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
|
||||
imageUri(avatarAction.newAvatarUri)
|
||||
}
|
||||
clickListener { callback?.onAvatarChange() }
|
||||
deleteListener { callback?.onAvatarDelete() }
|
||||
}
|
||||
|
||||
buildProfileSection(
|
||||
stringProvider.getString(R.string.settings)
|
||||
)
|
||||
|
@ -16,30 +16,41 @@
|
||||
|
||||
package im.vector.app.features.roomprofile.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.yalantis.ucrop.UCrop
|
||||
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.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
|
||||
import im.vector.app.features.media.createUCropWithDefaultSettings
|
||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomSettingsFragment @Inject constructor(
|
||||
@ -47,10 +58,15 @@ class RoomSettingsFragment @Inject constructor(
|
||||
private val controller: RoomSettingsController,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment(), RoomSettingsController.Callback {
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
RoomSettingsController.Callback,
|
||||
OnBackPressed,
|
||||
GalleryOrCameraDialogHelper.Listener {
|
||||
|
||||
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
|
||||
private val roomProfileArgs: RoomProfileArgs by args()
|
||||
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this)
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
|
||||
|
||||
@ -161,4 +177,77 @@ class RoomSettingsFragment @Inject constructor(
|
||||
override fun onAliasChanged(alias: String) {
|
||||
viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias))
|
||||
}
|
||||
|
||||
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 onAvatarDelete() {
|
||||
withState(viewModel) {
|
||||
when (it.avatarAction) {
|
||||
RoomSettingsViewState.AvatarAction.None -> {
|
||||
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar))
|
||||
}
|
||||
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
|
||||
/* Should not happen */
|
||||
Unit
|
||||
}
|
||||
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
|
||||
// Cancel the update of the avatar
|
||||
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAvatarChange() {
|
||||
galleryOrCameraDialogHelper.show()
|
||||
}
|
||||
|
||||
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 -> {
|
||||
val uri = data?.let { UCrop.getOutput(it) } ?: return
|
||||
viewModel.handle(RoomSettingsAction.SetAvatarAction(
|
||||
RoomSettingsViewState.AvatarAction.UpdateAvatar(
|
||||
newAvatarUri = uri,
|
||||
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var ignoreChanges = false
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
if (ignoreChanges) return false
|
||||
|
||||
return withState(viewModel) {
|
||||
return@withState if (it.showSaveAction) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.dialog_title_warning)
|
||||
.setMessage(R.string.warning_unsaved_change)
|
||||
.setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ ->
|
||||
ignoreChanges = true
|
||||
vectorBaseActivity.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,13 @@ import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.rx.mapOptional
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
|
||||
@ -55,16 +59,19 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeRoomAvatar()
|
||||
observeState()
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
selectSubscribe(
|
||||
RoomSettingsViewState::avatarAction,
|
||||
RoomSettingsViewState::newName,
|
||||
RoomSettingsViewState::newCanonicalAlias,
|
||||
RoomSettingsViewState::newTopic,
|
||||
RoomSettingsViewState::newHistoryVisibility,
|
||||
RoomSettingsViewState::roomSummary) { newName,
|
||||
RoomSettingsViewState::roomSummary) { avatarAction,
|
||||
newName,
|
||||
newCanonicalAlias,
|
||||
newTopic,
|
||||
newHistoryVisibility,
|
||||
@ -72,7 +79,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
val summary = asyncSummary()
|
||||
setState {
|
||||
copy(
|
||||
showSaveAction = summary?.name != newName
|
||||
showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None
|
||||
|| summary?.name != newName
|
||||
|| summary?.topic != newTopic
|
||||
|| summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() }
|
||||
|| newHistoryVisibility != null
|
||||
@ -101,6 +109,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val permissions = RoomSettingsViewState.ActionPermissions(
|
||||
canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
|
||||
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
|
||||
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
|
||||
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
|
||||
@ -114,9 +123,24 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
/**
|
||||
* We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
|
||||
*/
|
||||
private fun observeRoomAvatar() {
|
||||
room.rx()
|
||||
.liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition)
|
||||
.mapOptional { it.content.toModel<RoomAvatarContent>() }
|
||||
.unwrap()
|
||||
.subscribe {
|
||||
setState { copy(currentRoomAvatarUrl = it.avatarUrl) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomSettingsAction) {
|
||||
when (action) {
|
||||
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
|
||||
is RoomSettingsAction.SetAvatarAction -> setState { copy(avatarAction = action.avatarAction) }
|
||||
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
|
||||
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
|
||||
is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
|
||||
@ -132,6 +156,15 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
|
||||
val summary = state.roomSummary.invoke()
|
||||
|
||||
when (val avatarAction = state.avatarAction) {
|
||||
RoomSettingsViewState.AvatarAction.None -> Unit
|
||||
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
|
||||
operationList.add(room.rx().deleteAvatar())
|
||||
}
|
||||
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
|
||||
operationList.add(room.rx().updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName))
|
||||
}
|
||||
}
|
||||
if (summary?.name != state.newName) {
|
||||
operationList.add(room.rx().updateName(state.newName ?: ""))
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.roomprofile.settings
|
||||
|
||||
import android.net.Uri
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
@ -29,6 +30,8 @@ data class RoomSettingsViewState(
|
||||
val historyVisibilityEvent: Event? = null,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val isLoading: Boolean = false,
|
||||
val currentRoomAvatarUrl: String? = null,
|
||||
val avatarAction: AvatarAction = AvatarAction.None,
|
||||
val newName: String? = null,
|
||||
val newTopic: String? = null,
|
||||
val newHistoryVisibility: RoomHistoryVisibility? = null,
|
||||
@ -40,10 +43,18 @@ data class RoomSettingsViewState(
|
||||
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
|
||||
|
||||
data class ActionPermissions(
|
||||
val canChangeAvatar: Boolean = false,
|
||||
val canChangeName: Boolean = false,
|
||||
val canChangeTopic: Boolean = false,
|
||||
val canChangeCanonicalAlias: Boolean = false,
|
||||
val canChangeHistoryReadability: Boolean = false,
|
||||
val canEnableEncryption: Boolean = false
|
||||
)
|
||||
|
||||
sealed class AvatarAction {
|
||||
object None : AvatarAction()
|
||||
object DeleteAvatar : AvatarAction()
|
||||
data class UpdateAvatar(val newAvatarUri: Uri,
|
||||
val newAvatarFileName: String) : AvatarAction()
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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">
|
||||
<item
|
||||
android:id="@+id/bigAvatarEditAction"
|
||||
android:title="@string/edit"
|
||||
android:icon="@drawable/ic_edit"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
@ -2630,4 +2630,6 @@
|
||||
|
||||
<!-- Universal link -->
|
||||
<string name="universal_link_malformed">The link was malformed</string>
|
||||
<string name="warning_unsaved_change">There are unsaved changes. Discard the changes?</string>
|
||||
<string name="warning_unsaved_change_discard">Discard changes</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user