Merge branch 'develop' into live_location_labs_flag

* develop:
  Space explore rooms screen alignment with design in figma (#5834)
  leaving space aligned with ios (#5942)
  Fix usage of System.currentTimeMillis(). This a bit mocky but anyway it's better to use SystemClock.elapsedRealtime() for this case.
This commit is contained in:
Onuray Sahin 2022-05-19 12:52:05 +03:00
commit 2e2dffd9c9
24 changed files with 541 additions and 461 deletions

1
changelog.d/5658.feature Normal file
View File

@ -0,0 +1 @@
Space explore screen changes: removed space card, added rooms filtering

1
changelog.d/5728.misc Normal file
View File

@ -0,0 +1 @@
leaving space experience changed to be aligned with iOS

View File

@ -16,6 +16,7 @@
package im.vector.lib.core.utils.flow
import android.os.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
@ -68,10 +69,10 @@ fun <T> Flow<T>.chunk(durationInMillis: Long): Flow<List<T>> {
@ExperimentalCoroutinesApi
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var windowStartTime = System.currentTimeMillis()
var windowStartTime = SystemClock.elapsedRealtime()
var emitted = false
collect { value ->
val currentTime = System.currentTimeMillis()
val currentTime = SystemClock.elapsedRealtime()
val delta = currentTime - windowStartTime
if (delta >= windowDuration) {
windowStartTime += delta / windowDuration * windowDuration

View File

@ -104,11 +104,10 @@ class SpaceMenuRobot {
fun leaveSpace() {
clickOnSheet(R.id.leaveSpace)
waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
clickOn(R.id.leave_selected)
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
}
clickOn(R.id.spaceLeaveSelectAll)
clickOn(R.id.spaceLeaveButton)
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2022 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.utils
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout
/**
* [AppBarLayout.Behavior] subclass with a possibility to disable behavior.
* Useful for cases when in some view state we want prevent toolbar from collapsing/expanding by scroll events
*/
class ToggleableAppBarLayoutBehavior : AppBarLayout.Behavior {
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
var isEnabled = true
override fun onStartNestedScroll(parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int): Boolean {
return isEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
}
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout,
child: AppBarLayout,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray) {
if (!isEnabled) return
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
}
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout,
child: AppBarLayout,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int) {
if (!isEnabled) return
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.features.home.room.list
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_space_directory_filter_no_results)
abstract class SpaceDirectoryFilterNoResults : VectorEpoxyModel<SpaceDirectoryFilterNoResults.Holder>() {
class Holder : VectorEpoxyHolder()
}

View File

@ -1,194 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces
import android.app.Activity
import android.graphics.Typeface
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.toSpannable
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.args
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.registerStartForActivityResult
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.core.utils.styleMatchingText
import im.vector.app.databinding.BottomSheetLeaveSpaceBinding
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import me.gujun.android.span.span
import org.matrix.android.sdk.api.util.toMatrixItem
import reactivecircus.flowbinding.android.widget.checkedChanges
import javax.inject.Inject
@AndroidEntryPoint
class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetLeaveSpaceBinding>() {
val settingsViewModel: SpaceMenuViewModel by parentFragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetLeaveSpaceBinding {
return BottomSheetLeaveSpaceBinding.inflate(inflater, container, false)
}
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var errorFormatter: ErrorFormatter
@Parcelize
data class Args(
val spaceId: String
) : Parcelable
override val showExpanded = true
private val spaceArgs: SpaceBottomSheetSettingsArgs by args()
private val cherryPickLeaveActivityResult = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
// nothing actually?
} else {
// move back to default
settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.autoLeaveRadioGroup.checkedChanges()
.onEach {
when (it) {
views.leaveAll.id -> {
settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll)
}
views.leaveNone.id -> {
settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveNone)
}
views.leaveSelected.id -> {
settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveSelected)
// launch dedicated activity
cherryPickLeaveActivityResult.launch(
SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId)
)
}
}
}
.launchIn(viewLifecycleOwner.lifecycleScope)
views.leaveButton.debouncedClicks {
settingsViewModel.handle(SpaceLeaveViewAction.LeaveSpace)
}
views.cancelButton.debouncedClicks {
dismiss()
}
}
override fun invalidate() = withState(settingsViewModel) { state ->
super.invalidate()
val spaceSummary = state.spaceSummary ?: return@withState
val bestName = spaceSummary.toMatrixItem().getBestName()
val commonText = getString(R.string.space_leave_prompt_msg_with_name, bestName)
.toSpannable().styleMatchingText(bestName, Typeface.BOLD)
val warningMessage: CharSequence = if (spaceSummary.otherMemberIds.isEmpty()) {
span {
+commonText
+"\n\n"
span(getString(R.string.space_leave_prompt_msg_only_you)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
}
} else if (state.isLastAdmin) {
span {
+commonText
+"\n\n"
span(getString(R.string.space_leave_prompt_msg_as_admin)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
}
} else if (!spaceSummary.isPublic) {
span {
+commonText
+"\n\n"
span(getString(R.string.space_leave_prompt_msg_private)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
}
} else {
commonText
}
views.bottomLeaveSpaceWarningText.setTextOrHide(warningMessage)
views.inlineErrorText.setTextOrHide(null)
if (state.leavingState is Loading) {
views.leaveButton.isInvisible = true
views.cancelButton.isInvisible = true
views.leaveProgress.isVisible = true
} else {
views.leaveButton.isInvisible = false
views.cancelButton.isInvisible = false
views.leaveProgress.isVisible = false
if (state.leavingState is Fail) {
views.inlineErrorText.setTextOrHide(errorFormatter.toHumanReadable(state.leavingState.error))
}
}
val hasChildren = (spaceSummary.spaceChildren?.size ?: 0) > 0
if (hasChildren) {
views.autoLeaveRadioGroup.isVisible = true
when (state.leaveMode) {
SpaceMenuState.LeaveMode.LEAVE_ALL -> {
views.autoLeaveRadioGroup.check(views.leaveAll.id)
}
SpaceMenuState.LeaveMode.LEAVE_NONE -> {
views.autoLeaveRadioGroup.check(views.leaveNone.id)
}
SpaceMenuState.LeaveMode.LEAVE_SELECTED -> {
views.autoLeaveRadioGroup.check(views.leaveSelected.id)
}
}
} else {
views.autoLeaveRadioGroup.isVisible = false
}
}
companion object {
fun newInstance(spaceId: String): LeaveSpaceBottomSheet {
return LeaveSpaceBottomSheet().apply {
setArguments(SpaceBottomSheetSettingsArgs(spaceId))
}
}
}
}

View File

@ -35,6 +35,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import kotlinx.parcelize.Parcelize
@ -109,7 +110,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
}
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {
LeaveSpaceBottomSheet.newInstance(spaceArgs.spaceId).show(childFragmentManager, "LOGOUT")
startActivity(SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId))
}
}

View File

@ -34,6 +34,8 @@ 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
import im.vector.app.features.home.room.list.spaceDirectoryFilterNoResults
import im.vector.app.features.spaces.manage.SpaceChildInfoMatchFilter
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span
import org.matrix.android.sdk.api.extensions.orFalse
@ -53,6 +55,7 @@ class SpaceDirectoryController @Inject constructor(
) : TypedEpoxyController<SpaceDirectoryState>() {
interface InteractionListener {
fun onFilterQueryChanged(query: String?)
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
@ -62,6 +65,7 @@ class SpaceDirectoryController @Inject constructor(
}
var listener: InteractionListener? = null
private val matchFilter = SpaceChildInfoMatchFilter()
override fun buildModels(data: SpaceDirectoryState?) {
val host = this
@ -76,7 +80,7 @@ class SpaceDirectoryController @Inject constructor(
val failure = results.error
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
genericPillItem {
id("HS no Support")
id("hs_no_support")
imageRes(R.drawable.error)
tintIcon(false)
text(
@ -132,43 +136,52 @@ class SpaceDirectoryController @Inject constructor(
}
}
} else {
flattenChildInfo.forEach { info ->
val isSpace = info.roomType == RoomType.SPACE
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
val error = (data?.changeMembershipStates?.get(info.childRoomId) as? ChangeMembershipState.FailedJoining)?.throwable
// if it's known use that matrixItem because it would have a better computed name
val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem()
?: info.toMatrixItem()
matchFilter.filter = data?.currentFilter ?: ""
val filteredChildInfo = flattenChildInfo.filter { matchFilter.test(it) }
spaceChildInfoItem {
id(info.childRoomId)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
topic(info.topic)
suggested(info.suggested.orFalse())
errorLabel(
error?.let {
host.stringProvider.getString(R.string.error_failed_to_join_room, host.errorFormatter.toHumanReadable(it))
if (filteredChildInfo.isEmpty()) {
spaceDirectoryFilterNoResults {
id("no_results")
}
} else {
filteredChildInfo.forEach { info ->
val isSpace = info.roomType == RoomType.SPACE
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
val error = (data?.changeMembershipStates?.get(info.childRoomId) as? ChangeMembershipState.FailedJoining)?.throwable
// if it's known use that matrixItem because it would have a better computed name
val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem()
?: info.toMatrixItem()
spaceChildInfoItem {
id(info.childRoomId)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
topic(info.topic)
suggested(info.suggested.orFalse())
errorLabel(
error?.let {
host.stringProvider.getString(R.string.error_failed_to_join_room, host.errorFormatter.toHumanReadable(it))
}
)
memberCount(info.activeMemberCount ?: 0)
loading(isLoading)
buttonLabel(
when {
error != null -> host.stringProvider.getString(R.string.global_retry)
isJoined -> host.stringProvider.getString(R.string.action_open)
else -> host.stringProvider.getString(R.string.action_join)
}
)
apply {
if (isSpace) {
itemClickListener { host.listener?.onSpaceChildClick(info) }
} else {
itemClickListener { host.listener?.onRoomClick(info) }
}
)
memberCount(info.activeMemberCount ?: 0)
loading(isLoading)
buttonLabel(
when {
error != null -> host.stringProvider.getString(R.string.global_retry)
isJoined -> host.stringProvider.getString(R.string.action_open)
else -> host.stringProvider.getString(R.string.action_join)
}
)
apply {
if (isSpace) {
itemClickListener { host.listener?.onSpaceChildClick(info) }
} else {
itemClickListener { host.listener?.onRoomClick(info) }
}
buttonClickListener { host.listener?.onButtonClick(info) }
}
buttonClickListener { host.listener?.onButtonClick(info) }
}
}
}

View File

@ -23,6 +23,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
@ -44,7 +45,6 @@ import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentSpaceDirectoryBinding
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.matrixto.SpaceCardRenderer
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceAddRoomSpaceChooserBottomSheet
@ -63,7 +63,6 @@ data class SpaceDirectoryArgs(
class SpaceDirectoryFragment @Inject constructor(
private val epoxyController: SpaceDirectoryController,
private val permalinkHandler: PermalinkHandler,
private val spaceCardRenderer: SpaceCardRenderer,
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentSpaceDirectoryBinding>(),
SpaceDirectoryController.InteractionListener,
@ -123,9 +122,6 @@ class SpaceDirectoryFragment @Inject constructor(
}
}
views.spaceCard.matrixToCardMainButton.isVisible = false
views.spaceCard.matrixToCardSecondaryButton.isVisible = false
// Hide FAB when list is scrolling
views.spaceDirectoryList.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
@ -167,18 +163,37 @@ class SpaceDirectoryFragment @Inject constructor(
// it's the root
toolbar?.setTitle(R.string.space_explore_activity_title)
} else {
toolbar?.title = state.currentRootSummary?.name
val spaceName = state.currentRootSummary?.name
?: state.currentRootSummary?.canonicalAlias
?: getString(R.string.space_explore_activity_title)
if (spaceName != null) {
toolbar?.title = spaceName
toolbar?.subtitle = getString(R.string.space_explore_activity_title)
} else {
toolbar?.title = getString(R.string.space_explore_activity_title)
}
}
spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard, showDescription = false)
views.addOrCreateChatRoomButton.isVisible = state.canAddRooms
}
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
menu.findItem(R.id.spaceSearch)?.let { searchItem ->
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
onFilterQueryChanged(newText)
return true
}
})
}
super.onPrepareOptionsMenu(menu)
}
@ -198,6 +213,10 @@ class SpaceDirectoryFragment @Inject constructor(
return super.onOptionsItemSelected(item)
}
override fun onFilterQueryChanged(query: String?) {
viewModel.handle(SpaceDirectoryViewAction.FilterRooms(query))
}
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
}

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
sealed class SpaceDirectoryViewAction : VectorViewModelAction {
data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class JoinOrOpen(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class FilterRooms(val query: String?) : SpaceDirectoryViewAction()
data class ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
object CreateNewRoom : SpaceDirectoryViewAction()

View File

@ -225,9 +225,16 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToCreateNewRoom(state.currentRootSummary?.roomId ?: initialState.spaceId))
}
}
is SpaceDirectoryViewAction.FilterRooms -> {
filter(action.query)
}
}
}
private fun filter(query: String?) {
setState { copy(currentFilter = query.orEmpty()) }
}
private fun handleBack() = withState { state ->
if (state.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)

View File

@ -21,6 +21,9 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
data class SetFilteringEnabled(val isEnabled: Boolean) : SpaceLeaveAdvanceViewAction()
object DoLeave : SpaceLeaveAdvanceViewAction()
object ClearError : SpaceLeaveAdvanceViewAction()
object SelectAll : SpaceLeaveAdvanceViewAction()
object SelectNone : SpaceLeaveAdvanceViewAction()
}

View File

@ -28,8 +28,11 @@ data class SpaceLeaveAdvanceViewState(
val allChildren: Async<List<RoomSummary>> = Uninitialized,
val selectedRooms: List<String> = emptyList(),
val currentFilter: String = "",
val leaveState: Async<Unit> = Uninitialized
val leaveState: Async<Unit> = Uninitialized,
val isFilteringEnabled: Boolean = false,
val isLastAdmin: Boolean = false
) : MavericksState {
constructor(args: SpaceBottomSheetSettingsArgs) : this(
spaceId = args.spaceId
)

View File

@ -18,20 +18,23 @@ package im.vector.app.features.spaces.leave
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.appcompat.widget.SearchView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
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.platform.VectorBaseFragment
import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior
import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import reactivecircus.flowbinding.appcompat.queryTextChanges
import javax.inject.Inject
class SpaceLeaveAdvancedFragment @Inject constructor(
@ -44,11 +47,33 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
override fun getMenuRes() = R.menu.menu_space_leave
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.toolbar)
.allowBack()
controller.listener = this
withState(viewModel) { state ->
setupToolbar(views.toolbar)
.setSubtitle(state.spaceSummary?.name)
.allowBack()
state.spaceSummary?.let { summary ->
val warningMessage: CharSequence? = when {
summary.otherMemberIds.isEmpty() -> getString(R.string.space_leave_prompt_msg_only_you)
state.isLastAdmin -> getString(R.string.space_leave_prompt_msg_as_admin)
!summary.isPublic -> getString(R.string.space_leave_prompt_msg_private)
else -> null
}
views.spaceLeavePromptDescription.isVisible = warningMessage != null
views.spaceLeavePromptDescription.text = warningMessage
}
views.spaceLeavePromptTitle.text = getString(R.string.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
}
views.roomList.configureWith(controller)
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
@ -56,12 +81,23 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
}
views.publicRoomsFilter.queryTextChanges()
.debounce(100)
.onEach {
viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString()))
}
.launchIn(viewLifecycleOwner.lifecycleScope)
views.spaceLeaveSelectGroup.setOnCheckedChangeListener { _, optionId ->
when (optionId) {
R.id.spaceLeaveSelectAll -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectAll)
R.id.spaceLeaveSelectNone -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectNone)
}
}
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.menu_space_leave_search)?.let { searchItem ->
searchItem.bind(
onExpanded = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = true)) },
onCollapsed = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = false)) },
onTextChanged = { viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it)) }
)
}
super.onPrepareOptionsMenu(menu)
}
override fun onDestroyView() {
@ -72,10 +108,63 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
if (state.isFilteringEnabled) {
views.appBarLayout.setExpanded(false)
}
updateAppBarBehaviorState(state)
updateRadioButtonsState(state)
controller.setData(state)
}
override fun onItemSelected(roomSummary: RoomSummary) {
viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId))
}
private fun updateAppBarBehaviorState(state: SpaceLeaveAdvanceViewState) {
val behavior = (views.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior as ToggleableAppBarLayoutBehavior
behavior.isEnabled = !state.isFilteringEnabled
}
private fun updateRadioButtonsState(state: SpaceLeaveAdvanceViewState) {
(state.allChildren as? Success)?.invoke()?.size?.let { allChildrenCount ->
when (state.selectedRooms.size) {
0 -> views.spaceLeaveSelectNone.isChecked = true
allChildrenCount -> views.spaceLeaveSelectAll.isChecked = true
else -> views.spaceLeaveSelectSemi.isChecked = true
}
}
}
private fun MenuItem.bind(
onExpanded: () -> Unit,
onCollapsed: () -> Unit,
onTextChanged: (String) -> Unit) {
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onExpanded()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
onCollapsed()
return true
}
})
val searchView = actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
onTextChanged(newText ?: "")
return true
}
})
}
}

View File

@ -36,9 +36,14 @@ import okhttp3.internal.toImmutableList
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter
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.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
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.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
@ -50,52 +55,24 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
private val appStateHandler: AppStateHandler
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(initialState) {
override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state ->
when (action) {
is SpaceLeaveAdvanceViewAction.ToggleSelection -> {
val existing = state.selectedRooms.toMutableList()
if (existing.contains(action.roomId)) {
existing.remove(action.roomId)
} else {
existing.add(action.roomId)
}
setState {
copy(
selectedRooms = existing.toImmutableList()
)
}
}
is SpaceLeaveAdvanceViewAction.UpdateFilter -> {
setState { copy(currentFilter = action.filter) }
}
SpaceLeaveAdvanceViewAction.DoLeave -> {
setState { copy(leaveState = Loading()) }
viewModelScope.launch {
try {
state.selectedRooms.forEach {
try {
session.roomService().leaveRoom(it)
} catch (failure: Throwable) {
// silently ignore?
Timber.e(failure, "Fail to leave sub rooms/spaces")
}
}
init {
val space = session.getRoom(initialState.spaceId)
val spaceSummary = space?.roomSummary()
session.spaceService().leaveSpace(initialState.spaceId)
// We observe the membership and to dismiss when we have remote echo of leaving
} catch (failure: Throwable) {
setState { copy(leaveState = Fail(failure)) }
}
}
}
SpaceLeaveAdvanceViewAction.ClearError -> {
setState { copy(leaveState = Uninitialized) }
val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
powerLevelsEvent?.content?.toModel<PowerLevelsContent>()?.let { powerLevelsContent ->
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
val otherAdminCount = spaceSummary?.otherMemberIds
?.map { powerLevelsHelper.getUserRole(it) }
?.count { it is Role.Admin }
?: 0
val isLastAdmin = isAdmin && otherAdminCount == 0
setState {
copy(isLastAdmin = isLastAdmin)
}
}
}
init {
val spaceSummary = session.getRoomSummary(initialState.spaceId)
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)?.let { room ->
room.flow().liveRoomSummary()
@ -127,6 +104,62 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
}
}
override fun handle(action: SpaceLeaveAdvanceViewAction) {
when (action) {
is SpaceLeaveAdvanceViewAction.UpdateFilter -> setState { copy(currentFilter = action.filter) }
SpaceLeaveAdvanceViewAction.ClearError -> setState { copy(leaveState = Uninitialized) }
SpaceLeaveAdvanceViewAction.SelectNone -> setState { copy(selectedRooms = emptyList()) }
is SpaceLeaveAdvanceViewAction.SetFilteringEnabled -> setState { copy(isFilteringEnabled = action.isEnabled) }
is SpaceLeaveAdvanceViewAction.ToggleSelection -> handleSelectionToggle(action)
SpaceLeaveAdvanceViewAction.DoLeave -> handleLeave()
SpaceLeaveAdvanceViewAction.SelectAll -> handleSelectAll()
}
}
private fun handleSelectAll() = withState { state ->
val filteredRooms = (state.allChildren as? Success)?.invoke()?.filter {
it.name.contains(state.currentFilter, true)
}
filteredRooms?.let {
setState { copy(selectedRooms = it.map { it.roomId }) }
}
}
private fun handleLeave() = withState { state ->
setState { copy(leaveState = Loading()) }
viewModelScope.launch {
try {
state.selectedRooms.forEach {
try {
session.roomService().leaveRoom(it)
} catch (failure: Throwable) {
// silently ignore?
Timber.e(failure, "Fail to leave sub rooms/spaces")
}
}
session.spaceService().leaveSpace(initialState.spaceId)
// We observe the membership and to dismiss when we have remote echo of leaving
} catch (failure: Throwable) {
setState { copy(leaveState = Fail(failure)) }
}
}
}
private fun handleSelectionToggle(action: SpaceLeaveAdvanceViewAction.ToggleSelection) = withState { state ->
val existing = state.selectedRooms.toMutableList()
if (existing.contains(action.roomId)) {
existing.remove(action.roomId)
} else {
existing.add(action.roomId)
}
setState {
copy(
selectedRooms = existing.toImmutableList(),
)
}
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<SpaceLeaveAdvancedViewModel, SpaceLeaveAdvanceViewState> {
override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel

View File

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:orientation="vertical">
<TextView
android:id="@+id/bottom_leave_space_warning_text"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:textColor="?vctr_content_primary"
tools:text="@string/space_leave_prompt_msg_with_name" />
<RadioGroup
android:id="@+id/autoLeaveRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingBottom="12dp">
<RadioButton
android:id="@+id/leave_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/leave_all_rooms_and_spaces"
tools:checked="true" />
<RadioButton
android:id="@+id/leave_none"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/dont_leave_any" />
<RadioButton
android:id="@+id/leave_selected"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/leave_specific_ones" />
</RadioGroup>
<TextView
android:id="@+id/inlineErrorText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?colorError"
tools:visibility="visible"
tools:text="@string/error_no_network"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp">
<ProgressBar
android:id="@+id/leaveProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/leaveButton"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="center_horizontal"
android:text="@string/leave_space" />
</FrameLayout>
<Button
android:id="@+id/cancelButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/action_cancel" />
</LinearLayout>

View File

@ -11,35 +11,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/spaceExploreCollapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?android:colorBackground"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:scrimAnimationDuration="250"
app:scrimVisibleHeightTrigger="120dp"
app:titleEnabled="false"
app:toolbarId="@id/toolbar">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp">
<include
android:id="@+id/spaceCard"
layout="@layout/fragment_matrix_to_room_space_card" />
</FrameLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
android:layout_height="?attr/actionBarSize"
app:contentInsetStart="0dp">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
@ -57,7 +34,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp "
android:layout_marginBottom="16dp"
android:contentDescription="@string/a11y_create_room"
android:scaleType="center"
android:src="@drawable/ic_fab_add"

View File

@ -16,41 +16,107 @@
tools:listitem="@layout/item_room_to_add_in_space" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:layout_behavior="im.vector.app.core.utils.ToggleableAppBarLayoutBehavior">
<!-- minHeight="0dp" is important to collapse on scroll -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/spaceExploreCollapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="0dp"
app:title="@string/pick_tings_to_leave"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"/>
android:layout_height="match_parent"
app:contentScrim="?android:colorBackground"
app:layout_scrollFlags="scroll|exitUntilCollapsed|enterAlways|snap"
app:scrimAnimationDuration="250"
app:scrimVisibleHeightTrigger="120dp"
app:titleEnabled="false"
app:toolbarId="@id/toolbar">
<androidx.appcompat.widget.SearchView
android:id="@+id/publicRoomsFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addRoomToSpaceToolbar"
app:queryHint="@string/search_hint_room_name" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:minHeight="0dp"
android:orientation="vertical"
android:paddingHorizontal="16dp">
<TextView
android:id="@+id/spaceLeavePromptTitle"
style="@style/TextAppearance.Vector.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/space_leave_prompt_msg_with_name" />
<TextView
android:id="@+id/spaceLeavePromptDescription"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/space_leave_prompt_msg_only_you"
android:textColor="?vctr_content_secondary" />
<TextView
android:id="@+id/spaceLeaveRadioButtonsTitle"
style="@style/TextAppearance.Vector.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/space_leave_radio_buttons_title"
android:textAllCaps="true"
android:textColor="?vctr_content_primary" />
<RadioGroup
android:id="@+id/spaceLeaveSelectGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/spaceLeaveSelectAll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/space_leave_radio_button_all" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/spaceLeaveSelectNone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/space_leave_radio_button_none" />
<!-- This view should never be visible! There are three possible states but only two buttons by design.-->
<!-- Third button is needed to make radiogroup work as expected, it's selected, but never shown-->
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/spaceLeaveSelectSemi"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
</RadioGroup>
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:title="Leave space" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/spacePreviewButtonBar"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?vctr_system"
android:background="@color/palette_white"
android:elevation="2dp"
android:orientation="horizontal"
android:padding="8dp">
android:padding="8dp"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
<Button
android:id="@+id/spaceLeaveCancel"
@ -68,7 +134,6 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/leave_space" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -136,8 +136,8 @@
android:layout_height="1dp"
android:background="?vctr_list_separator_system"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/joinSuggestedRoomButton"
app:layout_constraintStart_toStartOf="@id/roomNameView"
app:layout_constraintTop_toBottomOf="@id/inlineErrorText" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/space_explore_rooms_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:paddingHorizontal="32dp"
android:paddingTop="16dp"
tools:viewBindingIgnore="true">
<TextView
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/space_explore_filter_no_result_title" />
<TextView
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/space_explore_filter_no_result_description"
android:textColor="?vctr_content_secondary" />
<TextView
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/create_new_room"
android:textColor="?colorSecondary" />
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/spaceSearch"
android:icon="@drawable/ic_filter"
android:title="@string/search"
app:searchIcon="@drawable/ic_filter"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/spaceAddRoom"
android:title="@string/space_add_existing_rooms"

View File

@ -0,0 +1,13 @@
<?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/menu_space_leave_search"
android:icon="@drawable/ic_filter"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:searchIcon="@drawable/ic_filter"
app:showAsAction="ifRoom|collapseActionView" />
</menu>

View File

@ -2837,14 +2837,27 @@
<string name="space_explore_activity_title">Explore rooms</string>
<string name="space_add_child_title">Add rooms</string>
<string name="leave_space">Leave</string>
<string name="space_leave_radio_buttons_title">Things in this space</string>
<string name="space_leave_radio_button_all">Leave all</string>
<string name="space_leave_radio_button_none">Leave none</string>
<string name="space_leave_prompt_msg_with_name">Are you sure you want to leave %s?</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">You won\'t be able to rejoin unless you are re-invited.</string>
<string name="space_leave_prompt_msg_as_admin">You\'re the only admin of this space. Leaving it will mean no one has control over it.</string>
<string name="leave_all_rooms_and_spaces">Leave all rooms and spaces</string>
<string name="dont_leave_any">Dont leave any rooms and spaces</string>
<string name="leave_specific_ones">Leave specific rooms and spaces…</string>
<string name="pick_tings_to_leave">Pick things to leave</string>
<!-- TODO delete -->
<string name="leave_all_rooms_and_spaces" tools:ignore="UnusedResources">Leave all rooms and spaces</string>
<!-- TODO delete -->
<string name="dont_leave_any" tools:ignore="UnusedResources">Dont leave any rooms and spaces</string>
<!-- TODO delete -->
<string name="leave_specific_ones" tools:ignore="UnusedResources">Leave specific rooms and spaces…</string>
<!-- TODO delete -->
<string name="pick_tings_to_leave" tools:ignore="UnusedResources">Pick things to leave</string>
<string name="space_explore_filter_no_result_title">No results found</string>
<string name="space_explore_filter_no_result_description">Some results may be hidden because theyre private and you need an invite to them.</string>
<string name="space_add_existing_rooms">Add existing rooms and space</string>
<string name="space_add_existing_rooms_only">Add existing rooms</string>