new app layouts invites (#6911)

This commit is contained in:
Nikita Fedrunov 2022-08-25 16:34:04 +02:00 committed by GitHub
parent 225d05e0be
commit 70cfe00271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 625 additions and 1 deletions

1
changelog.d/6889.wip Normal file
View File

@ -0,0 +1 @@
[App Layout] new room invites screen

View File

@ -349,6 +349,7 @@
<activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
<activity android:name=".features.call.dialpad.PstnDialActivity" />
<activity android:name=".features.home.room.list.home.invites.InvitesActivity"/>
<!-- Services -->

View File

@ -52,6 +52,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsV
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel
import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.home.room.list.home.invites.InvitesViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel
@ -618,4 +619,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(HomeRoomListViewModel::class)
fun homeRoomListViewModel(factory: HomeRoomListViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(InvitesViewModel::class)
fun invitesViewModel(factory: InvitesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.list.home
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -48,6 +49,8 @@ import im.vector.app.features.home.room.list.actions.RoomListSharedAction
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
import im.vector.app.features.home.room.list.home.invites.InvitesActivity
import im.vector.app.features.home.room.list.home.invites.InvitesCounterController
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
import im.vector.app.features.spaces.SpaceListBottomSheet
import kotlinx.coroutines.flow.launchIn
@ -66,6 +69,7 @@ class HomeRoomListFragment :
@Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory
@Inject lateinit var userPreferencesProvider: UserPreferencesProvider
@Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController
@Inject lateinit var invitesCounterController: InvitesCounterController
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
@ -266,9 +270,19 @@ class HomeRoomListFragment :
controller.submitList(list)
}
}.adapter
is HomeRoomSection.InvitesCountData -> invitesCounterController.also { controller ->
controller.clickListener = ::onInvitesCounterClicked
section.count.observe(viewLifecycleOwner) { count ->
controller.submitData(count)
}
}.adapter
}
}
private fun onInvitesCounterClicked() {
startActivity(Intent(activity, InvitesActivity::class.java))
}
private fun onRoomFilterChanged(filter: HomeRoomFilter) {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomFilter(filter))
}
@ -285,6 +299,7 @@ class HomeRoomListFragment :
override fun onDestroyView() {
views.roomListView.cleanup()
recentRoomCarouselController.listener = null
invitesCounterController.clickListener = null
super.onDestroyView()
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.list.home
import androidx.lifecycle.map
import androidx.paging.PagedList
import arrow.core.toOption
import com.airbnb.mvrx.MavericksViewModelFactory
@ -100,9 +101,9 @@ class HomeRoomListViewModel @AssistedInject constructor(
private fun configureSections() = viewModelScope.launch {
val newSections = mutableSetOf<HomeRoomSection>()
newSections.add(getInvitesCountSection())
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
if (areSettingsEnabled) {
newSections.add(getRecentRoomsSection())
}
@ -127,6 +128,19 @@ class HomeRoomListViewModel @AssistedInject constructor(
)
}
private fun getInvitesCountSection(): HomeRoomSection.InvitesCountData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.INVITE)
}
val liveCount = session.roomService().getRoomSummariesLive(
builder.build(),
RoomSortOrder.ACTIVITY
).map { it.count() }
return HomeRoomSection.InvitesCountData(liveCount)
}
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)

View File

@ -32,4 +32,8 @@ sealed class HomeRoomSection {
data class RecentRoomsData(
val list: LiveData<List<RoomSummary>>
) : HomeRoomSection()
data class InvitesCountData(
val count: LiveData<Int>
) : HomeRoomSection()
}

View File

@ -0,0 +1,42 @@
/*
* 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.features.home.room.list.home.invites
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
@EpoxyModelClass
abstract class InviteCounterItem : VectorEpoxyModel<InviteCounterItem.Holder>(R.layout.item_invites_count) {
@EpoxyAttribute var invitesCount: Int = 0
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener(listener)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(invitesCount, true))
}
class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.invites_count_badge)
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.features.home.room.list.home.invites
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class InvitesAction : VectorViewModelAction {
data class AcceptInvitation(val roomSummary: RoomSummary) : InvitesAction()
data class RejectInvitation(val roomSummary: RoomSummary) : InvitesAction()
}

View File

@ -0,0 +1,34 @@
/*
* 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.features.home.room.list.home.invites
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
@AndroidEntryPoint
class InvitesActivity : VectorBaseActivity<ActivitySimpleBinding>() {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(views.simpleFragmentContainer, InvitesFragment::class.java)
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.features.home.room.list.home.invites
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class InvitesController @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = null
set(value) {
field = value
requestForcedModelBuild()
}
var listener: RoomListListener? = null
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener)
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.features.home.room.list.home.invites
import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.resources.StringProvider
import javax.inject.Inject
class InvitesCounterController @Inject constructor(
val stringProvider: StringProvider
) : EpoxyController() {
private var count = 0
var clickListener: (() -> Unit)? = null
override fun buildModels() {
val host = this
if (count != 0) {
inviteCounterItem {
id("invites_counter")
invitesCount(host.count)
listener { host.clickListener?.invoke() }
}
}
}
fun submitData(count: Int?) {
this.count = count ?: 0
requestModelBuild()
}
}

View File

@ -0,0 +1,111 @@
/*
* 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.features.home.room.list.home.invites
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentInvitesBinding
import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.notifications.NotificationDrawerManager
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject
@AndroidEntryPoint
class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListListener {
@Inject lateinit var controller: InvitesController
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private val viewModel by fragmentViewModel(InvitesViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentInvitesBinding {
return FragmentInvitesBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.invitesToolbar)
.allowBack()
views.invitesRecycler.configureWith(controller)
controller.listener = this
viewModel.onEach(InvitesViewState::roomMembershipChanges) {
controller.roomChangeMembershipStates = it
}
viewModel.observeViewEvents {
when (it) {
is InvitesViewEvents.Failure -> showFailure(it.throwable)
is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView)
InvitesViewEvents.Close -> handleClose()
}
}
}
private fun handleClose() {
requireActivity().finish()
}
private fun handleOpenRoom(roomSummary: RoomSummary, shouldCloseInviteView: Boolean) {
navigator.openRoom(
context = requireActivity(),
roomId = roomSummary.roomId,
isInviteAlreadyAccepted = true,
trigger = ViewRoom.Trigger.RoomList // #6508
)
if (shouldCloseInviteView) {
requireActivity().finish()
}
}
override fun invalidate(): Unit = withState(viewModel) { state ->
super.invalidate()
state.pagedList?.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}
override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
viewModel.handle(InvitesAction.RejectInvitation(room))
}
override fun onAcceptRoomInvitation(room: RoomSummary) {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
viewModel.handle(InvitesAction.AcceptInvitation(room))
}
override fun onJoinSuggestedRoom(room: SpaceChildInfo) = Unit
override fun onSuggestedRoomClicked(room: SpaceChildInfo) = Unit
override fun onRoomClicked(room: RoomSummary) = Unit
override fun onRoomLongClicked(room: RoomSummary): Boolean = false
}

View File

@ -0,0 +1,26 @@
/*
* 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.features.home.room.list.home.invites
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class InvitesViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : InvitesViewEvents()
data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents()
object Close : InvitesViewEvents()
}

View File

@ -0,0 +1,136 @@
/*
* 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.features.home.room.list.home.invites
import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import timber.log.Timber
class InvitesViewModel @AssistedInject constructor(
@Assisted val initialState: InvitesViewState,
private val session: Session,
) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {
private val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
.setPrefetchDistance(10)
.build()
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<InvitesViewModel, InvitesViewState> {
override fun create(initialState: InvitesViewState): InvitesViewModel
}
companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()
init {
observeInvites()
}
override fun handle(action: InvitesAction) {
when (action) {
is InvitesAction.AcceptInvitation -> handleAcceptInvitation(action)
is InvitesAction.RejectInvitation -> handleRejectInvitation(action)
}
}
private fun handleRejectInvitation(action: InvitesAction.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to left an already leaving or joining room. Should not happen")
return@withState
}
val shouldCloseInviteView = state.pagedList?.value?.size == 1
viewModelScope.launch {
try {
session.roomService().leaveRoom(roomId)
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
// Instead, we wait for the room to be rejected
// Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons.
// If we update the state, the button will be displayed again, so it's not ideal...
if (shouldCloseInviteView) {
_viewEvents.post(InvitesViewEvents.Close)
}
} catch (failure: Throwable) {
// Notify the user
_viewEvents.post(InvitesViewEvents.Failure(failure))
}
}
}
private fun handleAcceptInvitation(action: InvitesAction.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
// close invites view when navigate to a room from the last one invite
val shouldCloseInviteView = state.pagedList?.value?.size == 1
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
// quick echo
setState {
copy(
roomMembershipChanges = roomMembershipChanges.mapValues {
if (it.key == roomId) {
ChangeMembershipState.Joining
} else {
it.value
}
}
)
}
}
private fun observeInvites() {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.INVITE)
}
val pagedList = session.roomService().getPagedRoomSummariesLive(
queryParams = builder.build(),
pagedListConfig = pagedListConfig,
sortOrder = RoomSortOrder.ACTIVITY
)
setState {
copy(pagedList = pagedList)
}
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.features.home.room.list.home.invites
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class InvitesViewState(
val pagedList: LiveData<PagedList<RoomSummary>>? = null,
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
) : MavericksState

View File

@ -0,0 +1,36 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/invites_toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/invites_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/invites_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,48 @@
<?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:background="?vctr_toolbar_background"
android:clickable="true"
android:focusable="true"
tools:viewBindingIgnore="true">
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/invites_count_badge"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:gravity="center"
android:minWidth="16dp"
android:minHeight="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textColor="?colorOnError"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_unread_highlight"
tools:text="4"
tools:visibility="visible" />
<TextView
android:id="@+id/invites_count_title"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="8dp"
android:text="@string/invites_title"
android:textAllCaps="true"
android:textColor="?colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/invites_count_badge"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -441,6 +441,9 @@
<string name="system_alerts_header">"System Alerts"</string>
<string name="suggested_header">Suggested Rooms</string>
<!-- Invites fragment -->
<string name="invites_title">Invites</string>
<!-- People fragment -->
<string name="direct_chats_header">Conversations</string>
<string name="matrix_only_filter">Matrix contacts only</string>