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 - [ ] 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 - [ ] UI change has been tested on both light and dark themes
- [ ] Pull request is based on the develop branch - [ ] 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 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) - [ ] 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) - 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) - 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 | 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 🗣: Translations 🗣:
- -
@ -1380,36 +1384,3 @@ Changes in RiotX 0.1.0 (2019-07-11)
First release! First release!
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771 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. 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 ### Code quality

View File

@ -47,14 +47,15 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated internal class FileUploader @Inject constructor(
private val okHttpClient: OkHttpClient, @Authenticated private val okHttpClient: OkHttpClient,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService, private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
private val context: Context, private val context: Context,
private val temporaryFileCreator: TemporaryFileCreator, private val temporaryFileCreator: TemporaryFileCreator,
contentUrlResolver: ContentUrlResolver, contentUrlResolver: ContentUrlResolver,
moshi: Moshi) { moshi: Moshi
) {
private val uploadUrl = contentUrlResolver.uploadUrl private val uploadUrl = contentUrlResolver.uploadUrl
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) 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 urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
val httpUrl = urlBuilder val httpUrl = urlBuilder
.addQueryParameter("filename", filename) .apply {
if (filename != null) {
addQueryParameter("filename", filename)
}
}
.build() .build()
val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody 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 encryptedFile: File?
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## Encrypt file") Timber.v("## Encrypt file")
encryptedFile = temporaryFileCreator.create() encryptedFile = temporaryFileCreator.create()
.also { filesToDelete.add(it) } .also { filesToDelete.add(it) }
@ -239,16 +238,22 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
} }
} }
Timber.v("## Uploading file") Timber.v("## Uploading file")
fileUploader.uploadFile(
fileUploader file = encryptedFile,
.uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener) filename = null,
mimeType = MimeTypes.OctetStream,
progressListener = progressListener
)
} else { } else {
Timber.v("## Clear file") Timber.v("## Uploading clear file")
encryptedFile = null encryptedFile = null
fileUploader fileUploader.uploadFile(
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) file = fileToUpload,
filename = attachment.name,
mimeType = attachment.getSafeMimeType(),
progressListener = progressListener
)
} }
Timber.v("## Update cache storage for ${contentUploadResponse.contentUri}") 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 encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray( val contentUploadResponse = fileUploader.uploadByteArray(
byteArray = encryptionResult.encryptedByteArray, byteArray = encryptionResult.encryptedByteArray,
filename = "thumb_${params.attachment.name}", filename = null,
mimeType = MimeTypes.OctetStream, mimeType = MimeTypes.OctetStream,
progressListener = thumbnailProgressListener progressListener = thumbnailProgressListener
) )

View File

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

View File

@ -81,7 +81,6 @@ internal class DefaultSpaceService @Inject constructor(
} else { } else {
this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
visibility = RoomDirectoryVisibility.PRIVATE 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 # If not on develop, exit, else we cannot get the list of modified files
# It is ok to check only when on develop branch # It is ok to check only when on develop branch
if [[ "${branch}" -eq 'develop' ]]; then 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 else
echo "Not on develop branch" echo "Not on develop branch"
exit 0 exit 0
@ -37,9 +37,9 @@ listOfModifiedFiles=`git diff --name-only HEAD ${branch}`
# echo ${listOfModifiedFiles} # echo ${listOfModifiedFiles}
if [[ ${listOfModifiedFiles} = *"CHANGES.md"* ]]; then if [[ ${listOfModifiedFiles} = *"newsfragment"* ]]; then
echo "CHANGES.md has been modified!" echo "A file has been added to /newsfragment!"
else 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 exit 1
fi 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 package im.vector.app.features.spaces
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -27,8 +28,10 @@ import com.airbnb.mvrx.args
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent 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.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.BottomSheetSpaceSettingsBinding import im.vector.app.databinding.BottomSheetSpaceSettingsBinding
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator 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 io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import me.gujun.android.span.span
import org.matrix.android.sdk.api.extensions.orFalse 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.events.model.EventType
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper 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 org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -55,6 +60,7 @@ data class SpaceBottomSheetSettingsArgs(
val spaceId: String val spaceId: String
) : Parcelable ) : Parcelable
// XXX make proper view model before leaving beta
class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpaceSettingsBinding>() { class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpaceSettingsBinding>() {
@Inject lateinit var navigator: Navigator @Inject lateinit var navigator: Navigator
@ -62,6 +68,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var bugReporter: BugReporter @Inject lateinit var bugReporter: BugReporter
@Inject lateinit var colorProvider: ColorProvider
private val spaceArgs: SpaceBottomSheetSettingsArgs by args() private val spaceArgs: SpaceBottomSheetSettingsArgs by args()
@ -71,10 +78,14 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
override val showExpanded = true
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
var isLastAdmin: Boolean = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceSettingsBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceSettingsBinding {
return BottomSheetSpaceSettingsBinding.inflate(inflater, container, false) return BottomSheetSpaceSettingsBinding.inflate(inflater, container, false)
} }
@ -108,6 +119,13 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
views.invitePeople.isVisible = canInvite || roomSummary?.isPublic.orFalse() views.invitePeople.isVisible = canInvite || roomSummary?.isPublic.orFalse()
views.addRooms.isVisible = canAddChild 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() }.disposeOnDestroyView()
views.spaceBetaTag.setOnClickListener { views.spaceBetaTag.setOnClickListener {
@ -138,8 +156,27 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
} }
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks { 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()) 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) { _, _ -> .setPositiveButton(R.string.leave) { _, _ ->
session.coroutineScope.launch { session.coroutineScope.launch {
try { try {
@ -152,6 +189,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .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.error.ErrorFormatter
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.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.core.ui.list.genericPillItem
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.spaceChildInfoItem import im.vector.app.features.home.room.list.spaceChildInfoItem
@ -50,6 +51,7 @@ class SpaceDirectoryController @Inject constructor(
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo) fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo) fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry() fun retry()
fun addExistingRooms(spaceId: String)
} }
var listener: InteractionListener? = null var listener: InteractionListener? = null
@ -97,9 +99,23 @@ class SpaceDirectoryController @Inject constructor(
?: emptyList() ?: emptyList()
if (flattenChildInfo.isEmpty()) { if (flattenChildInfo.isEmpty()) {
genericFooterItem { genericEmptyWithActionItem {
id("empty_footer") id("empty_res")
host.stringProvider.getString(R.string.no_result_placeholder) 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 { } else {
flattenChildInfo.forEach { info -> flattenChildInfo.forEach { info ->

View File

@ -19,6 +19,8 @@ package im.vector.app.features.spaces.explore
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
@ -26,9 +28,12 @@ import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
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.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed 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.databinding.FragmentRoomDirectoryPickerBinding 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 kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject import javax.inject.Inject
@ -44,6 +49,8 @@ class SpaceDirectoryFragment @Inject constructor(
SpaceDirectoryController.InteractionListener, SpaceDirectoryController.InteractionListener,
OnBackPressed { OnBackPressed {
override fun getMenuRes() = R.menu.menu_space_directory
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false) FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false)
@ -60,6 +67,10 @@ class SpaceDirectoryFragment @Inject constructor(
} }
epoxyController.listener = this epoxyController.listener = this
views.roomDirectoryPickerList.configureWith(epoxyController) views.roomDirectoryPickerList.configureWith(epoxyController)
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu()
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -77,6 +88,28 @@ class SpaceDirectoryFragment @Inject constructor(
views.toolbar.title = title 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) { override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo)) viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
} }
@ -97,6 +130,14 @@ class SpaceDirectoryFragment @Inject constructor(
override fun retry() { override fun retry() {
viewModel.handle(SpaceDirectoryViewAction.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) { // override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId)) // viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// } // }

View File

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

View File

@ -28,12 +28,15 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType 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.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.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
@ -70,6 +73,23 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
refreshFromApi() refreshFromApi()
observeJoinedRooms() observeJoinedRooms()
observeMembershipChanges() 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() { private fun refreshFromApi() {

View File

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

View File

@ -42,6 +42,7 @@ import javax.inject.Inject
class SpaceAddRoomFragment @Inject constructor( class SpaceAddRoomFragment @Inject constructor(
private val spaceEpoxyController: AddRoomListController, private val spaceEpoxyController: AddRoomListController,
private val roomEpoxyController: AddRoomListController, private val roomEpoxyController: AddRoomListController,
private val dmEpoxyController: AddRoomListController,
private val viewModelFactory: SpaceAddRoomsViewModel.Factory private val viewModelFactory: SpaceAddRoomsViewModel.Factory
) : VectorBaseFragment<FragmentSpaceAddRoomsBinding>(), ) : VectorBaseFragment<FragmentSpaceAddRoomsBinding>(),
OnBackPressed, AddRoomListController.Listener, SpaceAddRoomsViewModel.Factory { OnBackPressed, AddRoomListController.Listener, SpaceAddRoomsViewModel.Factory {
@ -84,6 +85,7 @@ class SpaceAddRoomFragment @Inject constructor(
viewModel.selectionListLiveData.observe(viewLifecycleOwner) { viewModel.selectionListLiveData.observe(viewLifecycleOwner) {
spaceEpoxyController.selectedItems = it spaceEpoxyController.selectedItems = it
roomEpoxyController.selectedItems = it roomEpoxyController.selectedItems = it
dmEpoxyController.selectedItems = it
saveNeeded = it.values.any { it } saveNeeded = it.values.any { it }
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -95,6 +97,7 @@ class SpaceAddRoomFragment @Inject constructor(
viewModel.selectSubscribe(this, SpaceAddRoomsState::ignoreRooms) { viewModel.selectSubscribe(this, SpaceAddRoomsState::ignoreRooms) {
spaceEpoxyController.ignoreRooms = it spaceEpoxyController.ignoreRooms = it
roomEpoxyController.ignoreRooms = it roomEpoxyController.ignoreRooms = it
dmEpoxyController.ignoreRooms = it
}.disposeOnDestroyView() }.disposeOnDestroyView()
viewModel.selectSubscribe(this, SpaceAddRoomsState::isSaving) { viewModel.selectSubscribe(this, SpaceAddRoomsState::isSaving) {
@ -105,6 +108,10 @@ class SpaceAddRoomFragment @Inject constructor(
} }
}.disposeOnDestroyView() }.disposeOnDestroyView()
viewModel.selectSubscribe(this, SpaceAddRoomsState::shouldShowDMs) {
dmEpoxyController.disabled = !it
}.disposeOnDestroyView()
views.createNewRoom.debouncedClicks { views.createNewRoom.debouncedClicks {
sharedViewModel.handle(SpaceManagedSharedAction.CreateRoom) sharedViewModel.handle(SpaceManagedSharedAction.CreateRoom)
} }
@ -121,11 +128,11 @@ class SpaceAddRoomFragment @Inject constructor(
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
} }
is SpaceAddRoomsViewEvents.SaveFailed -> { is SpaceAddRoomsViewEvents.SaveFailed -> {
showErrorInSnackbar(it.reason) showErrorInSnackbar(it.reason)
invalidateOptionsMenu() invalidateOptionsMenu()
} }
SpaceAddRoomsViewEvents.SavedDone -> { SpaceAddRoomsViewEvents.SavedDone -> {
sharedViewModel.handle(SpaceManagedSharedAction.HandleBack) sharedViewModel.handle(SpaceManagedSharedAction.HandleBack)
} }
} }
@ -149,6 +156,7 @@ class SpaceAddRoomFragment @Inject constructor(
views.roomList.cleanup() views.roomList.cleanup()
spaceEpoxyController.listener = null spaceEpoxyController.listener = null
roomEpoxyController.listener = null roomEpoxyController.listener = null
dmEpoxyController.listener = null
super.onDestroyView() super.onDestroyView()
} }
@ -181,6 +189,19 @@ class SpaceAddRoomFragment @Inject constructor(
concatAdapter.addAdapter(roomEpoxyController.adapter) concatAdapter.addAdapter(roomEpoxyController.adapter)
concatAdapter.addAdapter(spaceEpoxyController.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 views.roomList.adapter = concatAdapter
} }

View File

@ -26,7 +26,8 @@ data class SpaceAddRoomsState(
val currentFilter: String = "", val currentFilter: String = "",
val spaceName: String = "", val spaceName: String = "",
val ignoreRooms: List<String> = emptyList(), 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() // val selectionList: Map<String, Boolean> = emptyMap()
) : MvRxState { ) : MvRxState {
constructor(args: SpaceManageArgs) : this( 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>() private val selectionList = mutableMapOf<String, Boolean>()
val selectionListLiveData = MutableLiveData<Map<String, Boolean>>() val selectionListLiveData = MutableLiveData<Map<String, Boolean>>()
@ -106,7 +126,8 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
spaceName = spaceSummary?.displayName ?: "", 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.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem 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.ui.list.genericFooterItem
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
@ -31,7 +34,8 @@ import javax.inject.Inject
class SpaceManageRoomsController @Inject constructor( class SpaceManageRoomsController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpaceManageRoomViewState>() { ) : TypedEpoxyController<SpaceManageRoomViewState>() {
interface Listener { interface Listener {
@ -67,17 +71,24 @@ class SpaceManageRoomsController @Inject constructor(
matchFilter.filter = data.currentFilter matchFilter.filter = data.currentFilter
val filteredResult = directChildren.filter { matchFilter.test(it) } val filteredResult = directChildren.filter { matchFilter.test(it) }
filteredResult.forEach { childInfo -> if (filteredResult.isEmpty()) {
roomManageSelectionItem { genericFooterItem {
id(childInfo.childRoomId) id("empty_result")
matrixItem(childInfo.toMatrixItem()) text(host.stringProvider.getString(R.string.no_result_placeholder))
avatarRenderer(host.avatarRenderer) }
suggested(childInfo.suggested ?: false) } else {
space(childInfo.roomType == RoomType.SPACE) filteredResult.forEach { childInfo ->
selected(data.selectedRooms.contains(childInfo.childRoomId)) roomManageSelectionItem {
itemClickListener(DebouncedClickListener({ id(childInfo.childRoomId)
host.listener?.toggleSelection(childInfo) 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:id="@+id/coordinatorLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?riotx_header_panel_background"> android:background="?riotx_background">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" 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="space_add_child_title">Add rooms</string>
<string name="leave_space">Leave Space</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">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_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">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="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> <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="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_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_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> </resources>