Sharing: start extracting from RoomList as it's getting messy

This commit is contained in:
Ganard 2020-02-07 18:59:24 +01:00 committed by Benoit Marty
parent b7a7aa2f15
commit eccc52fe13
20 changed files with 531 additions and 349 deletions

View file

@ -75,6 +75,7 @@ import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragme
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@Module
@ -355,4 +356,9 @@ interface FragmentModule {
@FragmentKey(AttachmentsPreviewFragment::class)
fun bindAttachmentsPreviewFragment(fragment: AttachmentsPreviewFragment): Fragment
@Binds
@IntoMap
@FragmentKey(IncomingShareFragment::class)
fun bindIncomingShareFragment(fragment: IncomingShareFragment): Fragment
}

View file

@ -47,7 +47,6 @@ import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.ShareRoomListDataSource
import im.vector.riotx.features.ui.UiStateRepository
import javax.inject.Singleton
@ -97,8 +96,6 @@ interface VectorComponent {
fun homeRoomListObservableStore(): HomeRoomListDataSource
fun shareRoomListObservableStore(): ShareRoomListDataSource
fun selectedGroupStore(): SelectedGroupDataSource
fun activeSessionObservableStore(): ActiveSessionDataSource

View file

@ -23,6 +23,5 @@ enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
HOME(R.string.bottom_action_home),
PEOPLE(R.string.bottom_action_people_x),
ROOMS(R.string.bottom_action_rooms),
FILTERED(/* Not used */ 0),
SHARE(/* Not used */ 0)
FILTERED(/* Not used */ 0)
}

View file

@ -16,20 +16,18 @@
package im.vector.riotx.features.home.room.list
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.share.SharedData
sealed class RoomListAction : VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : RoomListAction()
data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction()
data class ToggleCategory(val category: RoomCategory) : RoomListAction()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class FilterWith(val filter: String) : RoomListAction()
data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction()
data class LeaveRoom(val roomId: String) : RoomListAction()
data class ShareToSelectedRooms(val sharedData: SharedData, val optionalMessage: String? = null): RoomListAction()
object MarkAllRoomsRead : RoomListAction()
}

View file

@ -33,7 +33,6 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
RoomListDisplayMode.SHARE -> roomSummary.membership == Membership.JOIN
}
}
}

View file

@ -50,15 +50,13 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsShare
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.share.SharedData
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
import javax.inject.Inject
@Parcelize
data class RoomListParams(
val displayMode: RoomListDisplayMode,
val sharedData: SharedData? = null
val displayMode: RoomListDisplayMode
) : Parcelable
class RoomListFragment @Inject constructor(
@ -110,11 +108,6 @@ class RoomListFragment @Inject constructor(
}.exhaustive
}
sendShareButton.setOnClickListener { _ ->
roomListViewModel.handle(RoomListAction.ShareToSelectedRooms(roomListParams.sharedData!!))
requireActivity().finish()
}
createChatFabMenu.listener = this
sharedActionViewModel
@ -137,19 +130,7 @@ class RoomListFragment @Inject constructor(
}
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
if (roomListParams.displayMode == RoomListDisplayMode.SHARE) {
val sharedData = roomListParams.sharedData ?: return
AlertDialog.Builder(requireActivity())
.setTitle(R.string.send_attachment)
.setMessage(getString(R.string.share_confirm_room, event.roomSummary.displayName))
.setPositiveButton(R.string.send) { _, _ ->
navigator.openRoomForSharing(requireActivity(), event.roomSummary.roomId, sharedData)
}
.setNegativeButton(R.string.cancel, null)
.show()
} else {
navigator.openRoom(requireActivity(), event.roomSummary.roomId)
}
navigator.openRoom(requireActivity(), event.roomSummary.roomId)
}
private fun setupCreateRoomButton() {
@ -268,7 +249,6 @@ class RoomListFragment @Inject constructor(
is Fail -> renderFailure(state.asyncFilteredRooms.error)
}
roomController.update(state)
sendShareButton.isVisible = state.multiSelectionEnabled
// Mark all as read menu
when (roomListParams.displayMode) {
RoomListDisplayMode.HOME,
@ -356,18 +336,14 @@ class RoomListFragment @Inject constructor(
// RoomSummaryController.Callback **************************************************************
override fun onRoomClicked(room: RoomSummary) {
roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = false))
roomListViewModel.handle(RoomListAction.SelectRoom(room))
}
override fun onRoomLongClicked(room: RoomSummary): Boolean {
if (roomListParams.displayMode == RoomListDisplayMode.SHARE) {
roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = true))
} else {
roomController.onRoomLongClicked()
RoomListQuickActionsBottomSheet
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
}
roomController.onRoomLongClicked()
RoomListQuickActionsBottomSheet
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
return true
}

View file

@ -28,7 +28,6 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.features.share.SharedData
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
@ -69,38 +68,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomListAction.ShareToSelectedRooms -> handleShareToSelectedRooms(action)
}.exhaustive
}
private fun handleShareToSelectedRooms(action: RoomListAction.ShareToSelectedRooms) = withState {
val sharedData = action.sharedData
it.selectedRoomIds.forEach { roomId ->
val room = session.getRoom(roomId)
if (sharedData is SharedData.Text) {
room?.sendTextMessage(sharedData.text)
} else if (sharedData is SharedData.Attachments) {
room?.sendMedias(sharedData.attachmentData)
}
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
if (it.multiSelectionEnabled) {
val selectedRooms = it.selectedRoomIds
val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) {
selectedRooms.minus(action.roomSummary.roomId)
} else {
selectedRooms.plus(action.roomSummary.roomId)
}
setState { copy(multiSelectionEnabled = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
} else if (action.enableMultiSelect) {
setState { copy(multiSelectionEnabled = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
} else {
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
}
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
}
private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState {
@ -231,54 +205,35 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
if (displayMode == RoomListDisplayMode.SHARE) {
val recentRooms = ArrayList<RoomSummary>(20)
val otherRooms = ArrayList<RoomSummary>(rooms.size)
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
when (room.breadcrumbsIndex) {
RoomSummary.NOT_IN_BREADCRUMBS -> otherRooms.add(room)
else -> recentRooms.add(room)
}
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}
return RoomSummaries().apply {
put(RoomCategory.RECENT_ROOMS, recentRooms)
put(RoomCategory.OTHER_ROOMS, otherRooms)
}
} else {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
}
}
}

View file

@ -18,21 +18,18 @@ package im.vector.riotx.features.home.room.list
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.features.share.ShareRoomListDataSource
import javax.inject.Inject
import javax.inject.Provider
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>,
private val shareRoomListDataSource: Provider<ShareRoomListDataSource>)
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>)
: RoomListViewModel.Factory {
override fun create(initialState: RoomListViewState): RoomListViewModel {
return RoomListViewModel(
initialState,
session.get(),
if (initialState.displayMode == RoomListDisplayMode.SHARE) shareRoomListDataSource.get() else homeRoomListDataSource.get()
homeRoomListDataSource.get()
)
}
}

View file

@ -43,12 +43,7 @@ data class RoomListViewState(
val isDirectRoomsExpanded: Boolean = true,
val isGroupRoomsExpanded: Boolean = true,
val isLowPriorityRoomsExpanded: Boolean = true,
val isServerNoticeRoomsExpanded: Boolean = true,
// For sharing
val isRecentExpanded: Boolean = true,
val isOtherExpanded: Boolean = true,
val selectedRoomIds: Set<String> = emptySet(),
val multiSelectionEnabled: Boolean = false
val isServerNoticeRoomsExpanded: Boolean = true
) : MvRxState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
@ -61,8 +56,6 @@ data class RoomListViewState(
RoomCategory.GROUP -> isGroupRoomsExpanded
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
RoomCategory.RECENT_ROOMS -> isRecentExpanded
RoomCategory.OTHER_ROOMS -> isOtherExpanded
}
}
@ -74,8 +67,6 @@ data class RoomListViewState(
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
RoomCategory.RECENT_ROOMS -> copy(isRecentExpanded = !isRecentExpanded)
RoomCategory.OTHER_ROOMS -> copy(isOtherExpanded = !isOtherExpanded)
}
}
@ -95,11 +86,7 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
DIRECT(R.string.bottom_action_people_x),
GROUP(R.string.bottom_action_rooms),
LOW_PRIORITY(R.string.low_priority_header),
SERVER_NOTICE(R.string.system_alerts_header),
// For Sharing
RECENT_ROOMS(R.string.room_list_sharing_header_recent_rooms),
OTHER_ROOMS(R.string.room_list_sharing_header_other_rooms)
SERVER_NOTICE(R.string.system_alerts_header)
}
fun RoomSummaries?.isNullOrEmpty(): Boolean {

View file

@ -60,7 +60,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
val nonNullViewState = viewState ?: return
when (nonNullViewState.displayMode) {
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
RoomListDisplayMode.SHARE -> buildShareRooms(nonNullViewState)
else -> buildRooms(nonNullViewState)
}
}
@ -78,44 +77,11 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
viewState.selectedRoomIds)
emptySet())
addFilterFooter(viewState)
}
private fun buildShareRooms(viewState: RoomListViewState) {
var hasResult = false
val roomSummaries = viewState.asyncFilteredRooms()
roomListNameFilter.filter = viewState.roomFilter
roomSummaries?.forEach { (category, summaries) ->
val filteredSummaries = summaries
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
if (filteredSummaries.isEmpty()) {
return@forEach
} else {
hasResult = true
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, emptyList(), category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(filteredSummaries,
emptySet(),
emptySet(),
emptySet(),
emptySet(),
viewState.selectedRoomIds
)
}
}
}
if (!hasResult) {
addNoResultItem()
}
}
private fun buildRooms(viewState: RoomListViewState) {
var showHelp = false
val roomSummaries = viewState.asyncFilteredRooms()
@ -133,7 +99,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
viewState.selectedRoomIds)
emptySet())
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
@ -162,13 +128,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
}
private fun addNoResultItem() {
noResultItem {
id("no_result")
text(stringProvider.getString(R.string.no_result_placeholder))
}
}
private fun buildRoomCategory(viewState: RoomListViewState,
summaries: List<RoomSummary>,
@StringRes titleRes: Int,

View file

@ -25,7 +25,6 @@ import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.DateProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
@ -50,16 +49,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
else -> createRoomItem(roomSummary, selectedRoomIds, listener)
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
}
}
private fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.latestPreviewableEvent?.root?.senderId
} else {
@ -82,7 +81,12 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.listener { listener?.onRoomClicked(roomSummary) }
}
private fun createRoomItem(roomSummary: RoomSummary, selectedRoomIds: Set<String>, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
fun createRoomItem(
roomSummary: RoomSummary,
selectedRoomIds: Set<String>,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
): VectorEpoxyModel<*> {
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
@ -124,11 +128,11 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.hasUnreadMessage(roomSummary.hasUnreadMessages)
.hasDraft(roomSummary.userDrafts.isNotEmpty())
.itemLongClickListener { _ ->
listener?.onRoomLongClicked(roomSummary) ?: false
onLongClick?.invoke(roomSummary) ?: false
}
.itemClickListener(
DebouncedClickListener(View.OnClickListener { _ ->
listener?.onRoomClicked(roomSummary)
onClick?.invoke(roomSummary)
})
)
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 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.riotx.features.share
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class IncomingShareAction: VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction()
object ShareToSelectedRooms: IncomingShareAction()
data class FilterWith(val filter: String) : IncomingShareAction()
data class UpdateSharedData(val sharedData: SharedData): IncomingShareAction()
}

View file

@ -9,127 +9,31 @@
*
* 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.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.V
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.share
import android.content.ClipDescription
import android.content.Intent
import android.os.Bundle
import android.view.Window
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import com.airbnb.mvrx.viewModel
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import androidx.appcompat.widget.Toolbar
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.login.LoginActivity
import kotlinx.android.synthetic.main.activity_incoming_share.*
import javax.inject.Inject
class IncomingShareActivity :
VectorBaseActivity(), AttachmentsHelper.Callback {
class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory
private lateinit var attachmentsHelper: AttachmentsHelper
// Do not remove, even if not used, it instantiates the view model
@Suppress("unused")
private val viewModel: IncomingShareViewModel by viewModel()
private val roomListFragment: RoomListFragment?
get() {
return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment
}
override fun getLayoutRes() = R.layout.activity_simple
override fun getLayoutRes() = R.layout.activity_incoming_share
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If we are not logged in, stop the sharing process and open login screen.
// In the future, we might want to relaunch the sharing process after login.
if (!sessionHolder.hasActiveSession()) {
startLoginActivity()
return
}
configureToolbar(incomingShareToolbar)
override fun initUiAndData() {
if (isFirstCreation()) {
replaceFragment(R.id.shareRoomListFragmentContainer, LoadingFragment::class.java)
addFragment(R.id.simpleFragmentContainer, IncomingShareFragment::class.java)
}
attachmentsHelper = AttachmentsHelper.create(this, this).register()
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
var isShareManaged = attachmentsHelper.handleShareIntent(
IntentUtils.getPickerIntentForSharing(intent)
)
if (!isShareManaged) {
isShareManaged = handleTextShare(intent)
}
if (!isShareManaged) {
cannotManageShare()
}
} else {
cannotManageShare()
}
incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
roomListFragment?.filterRoomsWith(newText)
return true
}
})
}
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
}
override fun onAttachmentsProcessFailed() {
cannotManageShare()
}
private fun cannotManageShare() {
Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
finish()
}
private fun handleTextShare(intent: Intent): Boolean {
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
return if (sharedText.isNullOrEmpty()) {
false
} else {
val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
true
}
}
return false
}
private fun startLoginActivity() {
val intent = LoginActivity.newIntent(this, null)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar, displayBack = false)
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 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.riotx.features.share
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Incomplete
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.list.RoomSummaryItemFactory
import javax.inject.Inject
class IncomingShareController @Inject constructor(private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val stringProvider: StringProvider) : TypedEpoxyController<IncomingShareViewState>() {
interface Callback {
fun onRoomClicked(roomSummary: RoomSummary)
fun onRoomLongClicked(roomSummary: RoomSummary): Boolean
}
var callback: Callback? = null
override fun buildModels(data: IncomingShareViewState) {
if (data.sharedData == null || data.filteredRoomSummaries is Incomplete) {
loadingItem {
id("loading")
}
return
}
val roomSummaries = data.filteredRoomSummaries()
if (roomSummaries.isNullOrEmpty()) {
noResultItem {
id("no_result")
text(stringProvider.getString(R.string.no_result_placeholder))
}
} else {
roomSummaries.forEach { roomSummary ->
roomSummaryItemFactory
.createRoomItem(roomSummary, data.selectedRoomIds, callback?.let { it::onRoomClicked }, callback?.let { it::onRoomLongClicked })
.addTo(this)
}
}
}
}

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2020 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.riotx.features.share
import android.content.ClipDescription
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.login.LoginActivity
import kotlinx.android.synthetic.main.fragment_incoming_share.*
import javax.inject.Inject
class IncomingShareFragment @Inject constructor(
val incomingShareViewModelFactory: IncomingShareViewModel.Factory,
private val incomingShareController: IncomingShareController,
private val sessionHolder: ActiveSessionHolder
) : VectorBaseFragment(), AttachmentsHelper.Callback, IncomingShareController.Callback {
private lateinit var attachmentsHelper: AttachmentsHelper
private val incomingShareViewModel: IncomingShareViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_incoming_share
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// If we are not logged in, stop the sharing process and open login screen.
// In the future, we might want to relaunch the sharing process after login.
if (!sessionHolder.hasActiveSession()) {
startLoginActivity()
return
}
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupToolbar(incomingShareToolbar)
attachmentsHelper = AttachmentsHelper.create(this, this).register()
val intent = vectorBaseActivity.intent
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
var isShareManaged = attachmentsHelper.handleShareIntent(
IntentUtils.getPickerIntentForSharing(intent)
)
if (!isShareManaged) {
isShareManaged = handleTextShare(intent)
}
if (!isShareManaged) {
cannotManageShare()
}
} else {
cannotManageShare()
}
incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
incomingShareViewModel.handle(IncomingShareAction.FilterWith(newText))
return true
}
})
sendShareButton.setOnClickListener { _ ->
handleSendShare()
}
incomingShareViewModel.observeViewEvents {
when (it) {
is IncomingShareViewEvents.ShareToRoom -> handleShareToRoom(it)
}.exhaustive
}
}
private fun handleShareToRoom(event: IncomingShareViewEvents.ShareToRoom) {
if (event.showAlert) {
showConfirmationDialog(event.roomSummary, event.sharedData)
} else {
navigator.openRoomForSharing(requireActivity(), event.roomSummary.roomId, event.sharedData)
}
}
private fun handleSendShare() {
incomingShareViewModel.handle(IncomingShareAction.ShareToSelectedRooms)
}
override fun onDestroyView() {
incomingShareController.callback = null
incomingShareRoomList.cleanup()
super.onDestroyView()
}
private fun setupRecyclerView() {
incomingShareRoomList.configureWith(incomingShareController, hasFixedSize = true)
incomingShareController.callback = this
}
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
val sharedData = SharedData.Attachments(attachments)
incomingShareViewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
}
override fun onAttachmentsProcessFailed() {
cannotManageShare()
}
private fun cannotManageShare() {
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
requireActivity().finish()
}
private fun handleTextShare(intent: Intent): Boolean {
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
return if (sharedText.isNullOrEmpty()) {
false
} else {
val sharedData = SharedData.Text(sharedText)
incomingShareViewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
true
}
}
return false
}
private fun showConfirmationDialog(roomSummary: RoomSummary, sharedData: SharedData) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.send_attachment)
.setMessage(getString(R.string.share_confirm_room, roomSummary.displayName))
.setPositiveButton(R.string.send) { _, _ ->
navigator.openRoomForSharing(requireActivity(), roomSummary.roomId, sharedData)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun startLoginActivity() {
val intent = LoginActivity.newIntent(requireActivity(), null)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finish()
}
override fun invalidate() = withState(incomingShareViewModel) {
sendShareButton.isVisible = it.multiSelectionEnabled
incomingShareController.setData(it)
}
override fun onRoomClicked(roomSummary: RoomSummary) {
incomingShareViewModel.handle(IncomingShareAction.SelectRoom(roomSummary, false))
}
override fun onRoomLongClicked(roomSummary: RoomSummary): Boolean {
incomingShareViewModel.handle(IncomingShareAction.SelectRoom(roomSummary, true))
return true
}
}

View file

@ -1,11 +1,11 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright (c) 2020 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
* 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,
@ -17,9 +17,8 @@
package im.vector.riotx.features.share
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.utils.BehaviorDataSource
import javax.inject.Inject
import javax.inject.Singleton
import im.vector.riotx.core.platform.VectorViewEvents
@Singleton
class ShareRoomListDataSource @Inject constructor() : BehaviorDataSource<List<RoomSummary>>()
sealed class IncomingShareViewEvents : VectorViewEvents {
data class ShareToRoom(val roomSummary: RoomSummary, val sharedData: SharedData, val showAlert: Boolean) : IncomingShareViewEvents()
}

View file

@ -16,71 +16,127 @@
package im.vector.riotx.features.share
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
import java.util.concurrent.TimeUnit
data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
/**
* View model used to observe the room list and post update to the ShareRoomListObservableStore
*/
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
private val sessionObservableStore: ActiveSessionDataSource,
private val shareRoomListObservableStore: ShareRoomListDataSource,
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
: VectorViewModel<IncomingShareState, EmptyAction, EmptyViewEvents>(initialState) {
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareViewState,
private val session: Session,
private val chronologicalRoomComparator: ChronologicalRoomComparator)
: VectorViewModel<IncomingShareViewState, IncomingShareAction, IncomingShareViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: IncomingShareState): IncomingShareViewModel
fun create(initialState: IncomingShareViewState): IncomingShareViewModel
}
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareState> {
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? {
val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.incomingShareViewModelFactory.create(state)
override fun create(viewModelContext: ViewModelContext, state: IncomingShareViewState): IncomingShareViewModel? {
val fragment: IncomingShareFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.incomingShareViewModelFactory.create(state)
}
}
private val filterStream: BehaviorRelay<String> = BehaviorRelay.createDefault("")
init {
observeRoomSummaries()
}
private fun observeRoomSummaries() {
val queryParams = roomSummaryQueryParams()
sessionObservableStore.observe()
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
it.orNull()?.rx()?.liveRoomSummaries(queryParams)
?: Observable.just(emptyList())
val queryParams = roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
}
session
.rx().liveRoomSummaries(queryParams)
.execute {
copy(roomSummaries = it)
}
filterStream
.switchMap { filter ->
val displayNameQuery = if (filter.isEmpty()) {
QueryStringValue.NoCondition
} else {
QueryStringValue.Contains(filter, QueryStringValue.Case.INSENSITIVE)
}
val filterQueryParams = roomSummaryQueryParams {
displayName = displayNameQuery
memberships = listOf(Membership.JOIN)
}
session.rx().liveRoomSummaries(filterQueryParams)
}
.throttleLast(300, TimeUnit.MILLISECONDS)
.map {
it.sortedWith(breadcrumbsRoomComparator)
.map { it.sortedWith(chronologicalRoomComparator) }
.execute {
copy(filteredRoomSummaries = it)
}
.subscribe {
shareRoomListObservableStore.post(it)
}
.disposeOnClear()
}
override fun handle(action: EmptyAction) {
// No op
override fun handle(action: IncomingShareAction) {
when (action) {
is IncomingShareAction.SelectRoom -> handleSelectRoom(action)
is IncomingShareAction.ShareToSelectedRooms -> handleShareToSelectedRooms()
is IncomingShareAction.FilterWith -> handleFilter(action)
is IncomingShareAction.UpdateSharedData -> handleUpdateSharedData(action)
}.exhaustive
}
private fun handleUpdateSharedData(action: IncomingShareAction.UpdateSharedData) {
setState { copy(sharedData = action.sharedData) }
}
private fun handleFilter(action: IncomingShareAction.FilterWith) {
filterStream.accept(action.filter)
}
private fun handleShareToSelectedRooms() = withState { state ->
val sharedData = state.sharedData ?: return@withState
if (state.selectedRoomIds.size == 1) {
val selectedRoomId = state.selectedRoomIds.first()
val selectedRoom = state.roomSummaries()?.find { it.roomId == selectedRoomId } ?: return@withState
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(selectedRoom, sharedData, showAlert = false))
} else {
state.selectedRoomIds.forEach { roomId ->
val room = session.getRoom(roomId)
if (sharedData is SharedData.Text) {
room?.sendTextMessage(sharedData.text)
} else if (sharedData is SharedData.Attachments) {
room?.sendMedias(sharedData.attachmentData)
}
}
}
}
private fun handleSelectRoom(action: IncomingShareAction.SelectRoom) = withState {
if (it.multiSelectionEnabled) {
val selectedRooms = it.selectedRoomIds
val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) {
selectedRooms.minus(action.roomSummary.roomId)
} else {
selectedRooms.plus(action.roomSummary.roomId)
}
setState { copy(multiSelectionEnabled = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
} else if (action.enableMultiSelect) {
setState { copy(multiSelectionEnabled = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
} else {
val sharedData = it.sharedData ?: return@withState
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, showAlert = true))
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 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.riotx.features.share
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class IncomingShareViewState(
val sharedData: SharedData? = null,
val roomSummaries: Async<List<RoomSummary>> = Uninitialized,
val filteredRoomSummaries: Async<List<RoomSummary>> = Uninitialized,
val selectedRoomIds: Set<String> = emptySet(),
val multiSelectionEnabled: Boolean = false
) : MvRxState

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/incomingShareToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:contentInsetStart="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/incomingShareSearchView"
style="@style/VectorSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:queryHint="@string/room_filtering_filter_hint"
app:searchIcon="@drawable/ic_filter" />
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/incomingShareRoomList"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/incomingShareToolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/sendShareButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:accessibilityTraversalBefore="@id/incomingShareRoomList"
android:contentDescription="@string/a11y_create_room"
android:src="@drawable/ic_send"
android:visibility="gone"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -55,17 +55,4 @@
tools:layout_marginEnd="144dp"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/sendShareButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:accessibilityTraversalBefore="@+id/roomListView"
android:contentDescription="@string/a11y_create_room"
android:src="@drawable/ic_send"
android:visibility="gone"
tools:visibility="visible" />
</im.vector.riotx.core.platform.StateView>