Merge branch 'develop' into feature/bca/fix_3371

This commit is contained in:
Benoit Marty 2021-05-21 14:53:21 +02:00 committed by GitHub
commit 40bb58c9cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 540 additions and 96 deletions

View file

@ -5,6 +5,6 @@
- [ ] Changes has been tested on an Android device or Android emulator with API 21
- [ ] UI change has been tested on both light and dark themes
- [ ] Pull request is based on the develop branch
- [ ] Pull request updates [CHANGES.md](https://github.com/vector-im/element-android/blob/develop/CHANGES.md)
- [ ] Pull request includes a new file under ./newsfragment. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off)

View file

@ -14,6 +14,10 @@ Bugfix 🐛:
- Implement a workaround to render <del> and <u> in the timeline (#1817)
- Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304)
- Spaces | Explore room list: the RoomId is displayed instead of name (#3371)
- Spaces | Personal spaces add DM - Web Parity (#3271)
- Spaces | Improve 'Leave Space' UX/UI (#3359)
- Don't create private spaces with encryption enabled (#3363)
- #+ button on lower right when looking at an empty space goes to an empty 'Explore rooms' (#3327)
Translations 🗣:
-
@ -1380,36 +1384,3 @@ Changes in RiotX 0.1.0 (2019-07-11)
First release!
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771
=======================================================
+ TEMPLATE WHEN PREPARING A NEW RELEASE +
=======================================================
Changes in Element 1.1.X (2021-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
-
Bugfix 🐛:
-
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
-
Test:
-
Other changes:
-

View file

@ -51,9 +51,21 @@ If an issue does not exist yet, it may be relevant to open a new issue and let u
This project is full Kotlin. Please do not write Java classes.
### CHANGES.md
### Changelog
Please add a line to the top of the file `CHANGES.md` describing your change.
Please create at least one file under ./newsfragment containing details about your change. Towncrier will be used when preparing the release.
Towncrier says to use the PR number for the filename, but the issue number is also fine.
Supported filename extensions are:
- ``.feature``: Signifying a new feature in Element Android or in the Matrix SDK.
- ``.bugfix``: Signifying a bug fix.
- ``.doc``: Signifying a documentation improvement.
- ``.removal``: Signifying a deprecation or removal of public API. Can be used to notifying about API change in the Matrix SDK
- ``.misc``: A ticket has been closed, but it is not of interest to users. Note that in this case, the content of the file will not be output, but just the issue/PR number.
See https://github.com/twisted/towncrier#news-fragments if you need more details.
### Code quality

View file

@ -47,14 +47,15 @@ import java.io.FileNotFoundException
import java.io.IOException
import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated
private val okHttpClient: OkHttpClient,
private val globalErrorReceiver: GlobalErrorReceiver,
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
private val context: Context,
private val temporaryFileCreator: TemporaryFileCreator,
contentUrlResolver: ContentUrlResolver,
moshi: Moshi) {
internal class FileUploader @Inject constructor(
@Authenticated private val okHttpClient: OkHttpClient,
private val globalErrorReceiver: GlobalErrorReceiver,
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
private val context: Context,
private val temporaryFileCreator: TemporaryFileCreator,
contentUrlResolver: ContentUrlResolver,
moshi: Moshi
) {
private val uploadUrl = contentUrlResolver.uploadUrl
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
@ -120,11 +121,17 @@ internal class FileUploader @Inject constructor(@Authenticated
}
}
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
private suspend fun upload(uploadBody: RequestBody,
filename: String?,
progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
val httpUrl = urlBuilder
.addQueryParameter("filename", filename)
.apply {
if (filename != null) {
addQueryParameter("filename", filename)
}
}
.build()
val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody

View file

@ -229,7 +229,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val encryptedFile: File?
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## Encrypt file")
encryptedFile = temporaryFileCreator.create()
.also { filesToDelete.add(it) }
@ -239,16 +238,22 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
}
}
Timber.v("## Uploading file")
fileUploader
.uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
fileUploader.uploadFile(
file = encryptedFile,
filename = null,
mimeType = MimeTypes.OctetStream,
progressListener = progressListener
)
} else {
Timber.v("## Clear file")
Timber.v("## Uploading clear file")
encryptedFile = null
fileUploader
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
fileUploader.uploadFile(
file = fileToUpload,
filename = attachment.name,
mimeType = attachment.getSafeMimeType(),
progressListener = progressListener
)
}
Timber.v("## Update cache storage for ${contentUploadResponse.contentUri}")
@ -312,7 +317,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray(
byteArray = encryptionResult.encryptedByteArray,
filename = "thumb_${params.attachment.name}",
filename = null,
mimeType = MimeTypes.OctetStream,
progressListener = thumbnailProgressListener
)

View file

@ -344,10 +344,9 @@ internal class RoomSummaryUpdater @Inject constructor(
if (it != null) addAll(it)
}
}.distinct()
if (flattenRelated.isEmpty()) {
dmRoom.flattenParentIds = null
} else {
dmRoom.flattenParentIds = "|${flattenRelated.joinToString("|")}|"
if (flattenRelated.isNotEmpty()) {
// we keep real m.child/m.parent relations and add the one for common memberships
dmRoom.flattenParentIds += "|${flattenRelated.joinToString("|")}|"
}
// Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}")
}

View file

@ -81,7 +81,6 @@ internal class DefaultSpaceService @Inject constructor(
} else {
this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
}
})
}

1
newsfragment/3293.misc Normal file
View file

@ -0,0 +1 @@
Setup towncrier tool

View file

@ -0,0 +1,47 @@
{% if top_line %}
{{ top_line }}
{{ top_underline * ((top_line)|length)}}
{% elif versiondata.name %}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
{% else %}
{{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
{% endif %}
{% for section, _ in sections.items() %}
{% set underline = underlines[0] %}{% if section %}{{section}}
{{ underline * section|length }}{% set underline = underlines[1] %}
{% endif %}
{% if sections[section] %}
{% for category, val in definitions.items() if category in sections[section]%}
{% if definitions[category]['name'] == "Features" %}
Features ✨:
{% elif definitions[category]['name'] == "Bugfixes" %}
Bugfixes 🐛:
{% elif definitions[category]['name'] == "Deprecations and Removals" %}
SDK API changes ⚠️:
{% elif definitions[category]['name'] == "Improved Documentation" %}
Improved Documentation 📚:
{% elif definitions[category]['name'] == "Misc" %}
Other changes:
{% else %}
{{ definitions[category]['name'] }}
{% endif %}
{% if definitions[category]['showcontent'] %}
{% for text, values in sections[section][category].items() %}
- {{ text }} ({{ values|join(', ') }})
{% endfor %}
{% else %}
- {{ sections[section][category]['']|join(', ') }}
{% endif %}
{% if sections[section][category]|length == 0 %}
No significant changes.
{% else %}
{% endif %}
{% endfor %}
{% else %}
No significant changes.
{% endif %}
{% endfor %}

View file

@ -23,7 +23,7 @@ branch=${TRAVIS_BRANCH}
# If not on develop, exit, else we cannot get the list of modified files
# It is ok to check only when on develop branch
if [[ "${branch}" -eq 'develop' ]]; then
echo "Check that the file 'CHANGES.md' has been modified"
echo "Check that a file has been added to /newsfragment"
else
echo "Not on develop branch"
exit 0
@ -37,9 +37,9 @@ listOfModifiedFiles=`git diff --name-only HEAD ${branch}`
# echo ${listOfModifiedFiles}
if [[ ${listOfModifiedFiles} = *"CHANGES.md"* ]]; then
echo "CHANGES.md has been modified!"
if [[ ${listOfModifiedFiles} = *"newsfragment"* ]]; then
echo "A file has been added to /newsfragment!"
else
echo "❌ Please add a line describing your change in CHANGES.md"
echo "❌ Please add a file describing your changes in /newsfragment. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog"
exit 1
fi

7
towncrier.toml Normal file
View file

@ -0,0 +1,7 @@
[tool.towncrier]
directory = "newsfragment"
filename = "CHANGES.md"
name = "Changes in Element"
# Note: there is a bug, if I use title_format, the title is printed twice
# title_format = "Changes in Element {version} ({project_date})"
template="tools/towncrier/template.md"

View file

@ -0,0 +1,92 @@
/*
* Copyright 2019 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.ui.list
import android.content.res.ColorStateList
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
/**
* A generic list item to display when there is no results, with an optional CTA
*/
@EpoxyModelClass(layout = R.layout.item_generic_empty_state)
abstract class GenericEmptyWithActionItem : VectorEpoxyModel<GenericEmptyWithActionItem.Holder>() {
class Action(var title: String) {
var perform: Runnable? = null
}
@EpoxyAttribute
var title: CharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int = -1
@EpoxyAttribute
@ColorInt
var iconTint: Int? = null
@EpoxyAttribute
var buttonAction: Action? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.titleText.setTextOrHide(title)
holder.descriptionText.setTextOrHide(description)
if (iconRes != -1) {
holder.imageView.setImageResource(iconRes)
holder.imageView.isVisible = true
if (iconTint != null) {
ImageViewCompat.setImageTintList(holder.imageView, ColorStateList.valueOf(iconTint!!))
} else {
ImageViewCompat.setImageTintList(holder.imageView, null)
}
} else {
holder.imageView.isVisible = false
}
holder.actionButton.setTextOrHide(buttonAction?.title)
holder.actionButton.setOnClickListener {
buttonAction?.perform?.run()
}
}
class Holder : VectorEpoxyHolder() {
val root by bind<View>(R.id.item_generic_root)
val titleText by bind<TextView>(R.id.emptyItemTitleView)
val descriptionText by bind<TextView>(R.id.emptyItemMessageView)
val imageView by bind<ImageView>(R.id.emptyItemImageView)
val actionButton by bind<Button>(R.id.emptyItemButton)
}
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.spaces
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -27,8 +28,10 @@ import com.airbnb.mvrx.args
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.BottomSheetSpaceSettingsBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator
@ -43,9 +46,11 @@ import im.vector.app.features.spaces.manage.SpaceManageActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import me.gujun.android.span.span
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
import javax.inject.Inject
@ -55,6 +60,7 @@ data class SpaceBottomSheetSettingsArgs(
val spaceId: String
) : Parcelable
// XXX make proper view model before leaving beta
class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpaceSettingsBinding>() {
@Inject lateinit var navigator: Navigator
@ -62,6 +68,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
@Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var bugReporter: BugReporter
@Inject lateinit var colorProvider: ColorProvider
private val spaceArgs: SpaceBottomSheetSettingsArgs by args()
@ -71,10 +78,14 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
var interactionListener: InteractionListener? = null
override val showExpanded = true
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
var isLastAdmin: Boolean = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceSettingsBinding {
return BottomSheetSpaceSettingsBinding.inflate(inflater, container, false)
}
@ -108,6 +119,13 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
views.invitePeople.isVisible = canInvite || roomSummary?.isPublic.orFalse()
views.addRooms.isVisible = canAddChild
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
val otherAdminCount = roomSummary?.otherMemberIds
?.map { powerLevelsHelper.getUserRole(it) }
?.count { it is Role.Admin }
?: 0
isLastAdmin = isAdmin && otherAdminCount == 0
}.disposeOnDestroyView()
views.spaceBetaTag.setOnClickListener {
@ -138,8 +156,27 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
}
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {
val spaceSummary = activeSessionHolder.getSafeActiveSession()?.getRoomSummary(spaceArgs.spaceId)
?: return@debouncedClicks
val warningMessage: CharSequence? = if (spaceSummary.otherMemberIds.isEmpty()) {
span(getString(R.string.space_leave_prompt_msg_only_you)) {
textColor = colorProvider.getColor(R.color.riotx_destructive_accent)
}
} else if (isLastAdmin) {
span(getString(R.string.space_leave_prompt_msg_as_admin)) {
textColor = colorProvider.getColor(R.color.riotx_destructive_accent)
}
} else if (!spaceSummary.isPublic) {
span(getString(R.string.space_leave_prompt_msg_private)) {
textColor = colorProvider.getColor(R.color.riotx_destructive_accent)
}
} else {
null
}
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.space_leave_prompt_msg))
.setMessage(warningMessage)
.setTitle(getString(R.string.space_leave_prompt_msg))
.setPositiveButton(R.string.leave) { _, _ ->
session.coroutineScope.launch {
try {
@ -152,6 +189,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
}
.setNegativeButton(R.string.cancel, null)
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
}

View file

@ -26,7 +26,8 @@ import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.GenericEmptyWithActionItem
import im.vector.app.core.ui.list.genericEmptyWithActionItem
import im.vector.app.core.ui.list.genericPillItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.spaceChildInfoItem
@ -50,6 +51,7 @@ class SpaceDirectoryController @Inject constructor(
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry()
fun addExistingRooms(spaceId: String)
}
var listener: InteractionListener? = null
@ -97,9 +99,23 @@ class SpaceDirectoryController @Inject constructor(
?: emptyList()
if (flattenChildInfo.isEmpty()) {
genericFooterItem {
id("empty_footer")
host.stringProvider.getString(R.string.no_result_placeholder)
genericEmptyWithActionItem {
id("empty_res")
title(host.stringProvider.getString(R.string.this_space_has_no_rooms))
iconRes(R.drawable.ic_empty_icon_room)
iconTint(host.colorProvider.getColorFromAttribute(R.attr.riotx_reaction_background_on))
apply {
if (data?.canAddRooms == true) {
description(host.stringProvider.getString(R.string.this_space_has_no_rooms_admin))
val action = GenericEmptyWithActionItem.Action(host.stringProvider.getString(R.string.space_add_existing_rooms))
action.perform = Runnable {
host.listener?.addExistingRooms(data.spaceId)
}
buttonAction(action)
} else {
description(host.stringProvider.getString(R.string.this_space_has_no_rooms_not_admin))
}
}
}
} else {
flattenChildInfo.forEach { info ->

View file

@ -19,6 +19,8 @@ package im.vector.app.features.spaces.explore
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
@ -26,9 +28,12 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject
@ -44,6 +49,8 @@ class SpaceDirectoryFragment @Inject constructor(
SpaceDirectoryController.InteractionListener,
OnBackPressed {
override fun getMenuRes() = R.menu.menu_space_directory
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false)
@ -60,6 +67,10 @@ class SpaceDirectoryFragment @Inject constructor(
}
epoxyController.listener = this
views.roomDirectoryPickerList.configureWith(epoxyController)
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu()
}
}
override fun onDestroyView() {
@ -77,6 +88,28 @@ class SpaceDirectoryFragment @Inject constructor(
views.toolbar.title = title
}
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms
menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.spaceAddRoom -> {
withState(viewModel) { state ->
addExistingRooms(state.spaceId)
}
return true
}
R.id.spaceCreateRoom -> {
// not implemented yet
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
}
@ -97,6 +130,14 @@ class SpaceDirectoryFragment @Inject constructor(
override fun retry() {
viewModel.handle(SpaceDirectoryViewAction.Retry)
}
private val addExistingRoomActivityResult = registerStartForActivityResult { _ ->
viewModel.handle(SpaceDirectoryViewAction.Retry)
}
override fun addExistingRooms(spaceId: String) {
addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms))
}
// override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// }

View file

@ -36,7 +36,8 @@ data class SpaceDirectoryState(
// Set of joined roomId / spaces,
val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap()
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val canAddRooms: Boolean = false
) : MvRxState {
constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId

View file

@ -28,12 +28,15 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
@ -70,6 +73,23 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
refreshFromApi()
observeJoinedRooms()
observeMembershipChanges()
observePermissions()
}
private fun observePermissions() {
val room = session.getRoom(initialState.spaceId) ?: return
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
powerLevelsContentLive
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState {
copy(canAddRooms = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_SPACE_CHILD))
}
}
.disposeOnClear()
}
private fun refreshFromApi() {

View file

@ -62,6 +62,22 @@ class AddRoomListController @Inject constructor(
var initialLoadOccurred = false
var expanded: Boolean = true
set(value) {
if (value != field) {
field = value
requestForcedModelBuild()
}
}
var disabled: Boolean = false
set(value) {
if (value != field) {
field = value
requestForcedModelBuild()
}
}
fun boundaryChange(boundary: ResultBoundaries) {
val boundaryHasLoadedSomething = boundary.frontLoaded || boundary.zeroItemLoaded
if (initialLoadOccurred != boundaryHasLoadedSomething) {
@ -88,6 +104,10 @@ class AddRoomListController @Inject constructor(
}
override fun addModels(models: List<EpoxyModel<*>>) {
if (disabled) {
super.addModels(emptyList())
return
}
val host = this
val filteredModel = if (ignoreRooms == null) {
models
@ -102,10 +122,13 @@ class AddRoomListController @Inject constructor(
RoomCategoryItem_().apply {
id("header")
title(host.sectionName ?: "")
expanded(true)
expanded(host.expanded)
listener {
host.expanded = !host.expanded
}
}
)
if (subHeaderText != null) {
if (expanded && subHeaderText != null) {
add(
GenericPillItem_().apply {
id("sub_header")
@ -115,11 +138,13 @@ class AddRoomListController @Inject constructor(
)
}
}
super.addModels(filteredModel)
if (!initialLoadOccurred) {
add(
RoomSelectionPlaceHolderItem_().apply { id("loading") }
)
if (expanded) {
super.addModels(filteredModel)
if (!initialLoadOccurred) {
add(
RoomSelectionPlaceHolderItem_().apply { id("loading") }
)
}
}
}
@ -129,7 +154,7 @@ class AddRoomListController @Inject constructor(
return RoomSelectionItem_().apply {
id(item.roomId)
matrixItem(item.toMatrixItem())
avatarRenderer(this@AddRoomListController.avatarRenderer)
avatarRenderer(host.avatarRenderer)
space(item.roomType == RoomType.SPACE)
selected(host.selectedItems[item.roomId] ?: false)
itemClickListener(DebouncedClickListener({

View file

@ -42,6 +42,7 @@ import javax.inject.Inject
class SpaceAddRoomFragment @Inject constructor(
private val spaceEpoxyController: AddRoomListController,
private val roomEpoxyController: AddRoomListController,
private val dmEpoxyController: AddRoomListController,
private val viewModelFactory: SpaceAddRoomsViewModel.Factory
) : VectorBaseFragment<FragmentSpaceAddRoomsBinding>(),
OnBackPressed, AddRoomListController.Listener, SpaceAddRoomsViewModel.Factory {
@ -84,6 +85,7 @@ class SpaceAddRoomFragment @Inject constructor(
viewModel.selectionListLiveData.observe(viewLifecycleOwner) {
spaceEpoxyController.selectedItems = it
roomEpoxyController.selectedItems = it
dmEpoxyController.selectedItems = it
saveNeeded = it.values.any { it }
invalidateOptionsMenu()
}
@ -95,6 +97,7 @@ class SpaceAddRoomFragment @Inject constructor(
viewModel.selectSubscribe(this, SpaceAddRoomsState::ignoreRooms) {
spaceEpoxyController.ignoreRooms = it
roomEpoxyController.ignoreRooms = it
dmEpoxyController.ignoreRooms = it
}.disposeOnDestroyView()
viewModel.selectSubscribe(this, SpaceAddRoomsState::isSaving) {
@ -105,6 +108,10 @@ class SpaceAddRoomFragment @Inject constructor(
}
}.disposeOnDestroyView()
viewModel.selectSubscribe(this, SpaceAddRoomsState::shouldShowDMs) {
dmEpoxyController.disabled = !it
}.disposeOnDestroyView()
views.createNewRoom.debouncedClicks {
sharedViewModel.handle(SpaceManagedSharedAction.CreateRoom)
}
@ -121,11 +128,11 @@ class SpaceAddRoomFragment @Inject constructor(
.setNegativeButton(R.string.cancel, null)
.show()
}
is SpaceAddRoomsViewEvents.SaveFailed -> {
is SpaceAddRoomsViewEvents.SaveFailed -> {
showErrorInSnackbar(it.reason)
invalidateOptionsMenu()
}
SpaceAddRoomsViewEvents.SavedDone -> {
SpaceAddRoomsViewEvents.SavedDone -> {
sharedViewModel.handle(SpaceManagedSharedAction.HandleBack)
}
}
@ -149,6 +156,7 @@ class SpaceAddRoomFragment @Inject constructor(
views.roomList.cleanup()
spaceEpoxyController.listener = null
roomEpoxyController.listener = null
dmEpoxyController.listener = null
super.onDestroyView()
}
@ -181,6 +189,19 @@ class SpaceAddRoomFragment @Inject constructor(
concatAdapter.addAdapter(roomEpoxyController.adapter)
concatAdapter.addAdapter(spaceEpoxyController.adapter)
// This controller can be disabled depending on the space type (public or not)
viewModel.updatableDMLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
dmEpoxyController.boundaryChange(it)
}
viewModel.updatableDMLivePageResult.livePagedList.observe(viewLifecycleOwner) {
dmEpoxyController.totalSize = it.size
dmEpoxyController.submitList(it)
}
dmEpoxyController.sectionName = getString(R.string.direct_chats_header)
dmEpoxyController.listener = this
concatAdapter.addAdapter(dmEpoxyController.adapter)
views.roomList.adapter = concatAdapter
}

View file

@ -26,7 +26,8 @@ data class SpaceAddRoomsState(
val currentFilter: String = "",
val spaceName: String = "",
val ignoreRooms: List<String> = emptyList(),
val isSaving: Async<List<String>> = Uninitialized
val isSaving: Async<List<String>> = Uninitialized,
val shouldShowDMs : Boolean = false
// val selectionList: Map<String, Boolean> = emptyMap()
) : MvRxState {
constructor(args: SpaceManageArgs) : this(

View file

@ -98,6 +98,26 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
)
}
val updatableDMLivePageResult: UpdatableLivePageResult by lazy {
session.getFilteredPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.excludeType = listOf(RoomType.SPACE)
this.includeType = null
this.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
this.activeSpaceFilter = ActiveSpaceFilter.ExcludeSpace(initialState.spaceId)
this.displayName = QueryStringValue.Contains(initialState.currentFilter, QueryStringValue.Case.INSENSITIVE)
},
pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
.setPrefetchDistance(10)
.build(),
sortOrder = RoomSortOrder.NAME
)
}
private val selectionList = mutableMapOf<String, Boolean>()
val selectionListLiveData = MutableLiveData<Map<String, Boolean>>()
@ -106,7 +126,8 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
setState {
copy(
spaceName = spaceSummary?.displayName ?: "",
ignoreRooms = (spaceSummary?.flattenParentIds ?: emptyList()) + listOf(initialState.spaceId)
ignoreRooms = (spaceSummary?.flattenParentIds ?: emptyList()) + listOf(initialState.spaceId),
shouldShowDMs = spaceSummary?.isPublic == false
)
}
}

View file

@ -19,9 +19,12 @@ package im.vector.app.features.spaces.manage
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType
@ -31,7 +34,8 @@ import javax.inject.Inject
class SpaceManageRoomsController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter
private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpaceManageRoomViewState>() {
interface Listener {
@ -67,17 +71,24 @@ class SpaceManageRoomsController @Inject constructor(
matchFilter.filter = data.currentFilter
val filteredResult = directChildren.filter { matchFilter.test(it) }
filteredResult.forEach { childInfo ->
roomManageSelectionItem {
id(childInfo.childRoomId)
matrixItem(childInfo.toMatrixItem())
avatarRenderer(host.avatarRenderer)
suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({
host.listener?.toggleSelection(childInfo)
}))
if (filteredResult.isEmpty()) {
genericFooterItem {
id("empty_result")
text(host.stringProvider.getString(R.string.no_result_placeholder))
}
} else {
filteredResult.forEach { childInfo ->
roomManageSelectionItem {
id(childInfo.childRoomId)
matrixItem(childInfo.toMatrixItem())
avatarRenderer(host.avatarRenderer)
suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({
host.listener?.toggleSelection(childInfo)
}))
}
}
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M21.5187,26.2723H25.8404L26.3357,21.6964H22.014L21.5187,26.2723Z"
android:fillColor="#C1C6CD"/>
<path
android:pathData="M44,24C44,35.0457 35.0457,44 24,44C12.9543,44 4,35.0457 4,24C4,12.9543 12.9543,4 24,4C35.0457,4 44,12.9543 44,24ZM21.0505,12.0116C22.1487,12.1305 22.9425,13.1171 22.8237,14.2152L22.4469,17.6964H26.7686L27.192,13.7848C27.3109,12.6866 28.2974,11.8928 29.3956,12.0116C30.4938,12.1305 31.2876,13.1171 31.1688,14.2152L30.792,17.6964H32.6C33.7046,17.6964 34.6,18.5918 34.6,19.6964C34.6,20.801 33.7046,21.6964 32.6,21.6964H30.3591L29.8638,26.2723H32.6C33.7046,26.2723 34.6,27.1677 34.6,28.2723C34.6,29.3769 33.7046,30.2723 32.6,30.2723H29.4308L29.0041,34.2152C28.8852,35.3134 27.8986,36.1072 26.8005,35.9884C25.7023,35.8695 24.9084,34.8829 25.0273,33.7848L25.4075,30.2723H21.0857L20.659,34.2152C20.5401,35.3134 19.5535,36.1072 18.4554,35.9884C17.3572,35.8695 16.5633,34.8829 16.6822,33.7848L17.0624,30.2723H15C13.8954,30.2723 13,29.3769 13,28.2723C13,27.1677 13.8954,26.2723 15,26.2723H17.4953L17.9906,21.6964H15.8784C14.7739,21.6964 13.8784,20.801 13.8784,19.6964C13.8784,18.5918 14.7739,17.6964 15.8784,17.6964H18.4235L18.8469,13.7848C18.9658,12.6866 19.9524,11.8928 21.0505,12.0116Z"
android:fillColor="#C1C6CD"
android:fillType="evenOdd"/>
</vector>

View file

@ -5,7 +5,7 @@
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
android:background="?riotx_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View file

@ -0,0 +1,72 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/emptyItemImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="16dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/emptyItemTitleView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_reaction_background_on"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_empty_icon_room" />
<TextView
android:id="@+id/emptyItemTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/emptyItemMessageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyItemImageView"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/this_space_has_no_rooms" />
<TextView
android:id="@+id/emptyItemMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:ellipsize="end"
android:gravity="center"
android:maxWidth="300dp"
android:maxLines="10"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/emptyItemButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emptyItemTitleView"
tools:text="@string/this_space_has_no_rooms_admin" />
<com.google.android.material.button.MaterialButton
android:id="@+id/emptyItemButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:minWidth="190dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyItemMessageView"
tools:text="@string/space_add_existing_rooms" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,15 @@
<?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/spaceAddRoom"
android:title="@string/space_add_existing_rooms"
app:showAsAction="never" />
<item
android:id="@+id/spaceCreateRoom"
android:title="@string/create_new_room"
app:iconTint="?attr/colorAccent"
app:showAsAction="never" />
</menu>

View file

@ -3353,9 +3353,13 @@
<string name="space_add_child_title">Add rooms</string>
<string name="leave_space">Leave Space</string>
<string name="space_leave_prompt_msg">Are you sure you want to leave the space?</string>
<string name="space_leave_prompt_msg_only_you">You are the only person here. If you leave, no one will be able to join in the future, including you.</string>
<string name="space_leave_prompt_msg_private">This space is not public. You will not be able to rejoin without an invite.</string>
<string name="space_leave_prompt_msg_as_admin">You are admin of this space, ensure that you have transferred admin right to another member before leaving.</string>
<string name="space_add_existing_rooms">Add existing rooms and space</string>
<string name="space_add_rooms">Add rooms</string>
<string name="spaces_beta_welcome_to_spaces">Welcome to Spaces!</string>
<string name="spaces_beta_welcome_to_spaces_desc">Spaces are a new way to group rooms and people.</string>
<string name="you_are_invited">You are invited</string>
@ -3377,5 +3381,10 @@
<string name="labs_space_show_orphan_in_home">Experimental Space - Only show orphans in Home</string>
<string name="spaces_feeling_experimental_subspace">Feeling experimental?\nYou can add existing spaces to a space.</string>
<string name="spaces_no_server_support_title">It looks like your homeserver does not support Spaces yet</string>
<string name="spaces_no_server_support_description">Please contact your homserver admin for further information</string>
<string name="spaces_no_server_support_description">Please contact your homeserver admin for further information</string>
<string name="this_space_has_no_rooms">This space has no rooms</string>
<string name="this_space_has_no_rooms_not_admin">Some rooms may be hidden because theyre private and you need an invite.\nYou dont have permission to add rooms.</string>
<string name="this_space_has_no_rooms_admin">Some rooms may be hidden because theyre private and you need an invite.</string>
</resources>