Autocompletion for room canonical alias

This commit is contained in:
Benoit Marty 2019-12-20 00:38:42 +01:00
parent 3a829bdfe8
commit 92f43a591a
11 changed files with 272 additions and 43 deletions

View File

@ -62,6 +62,9 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() if (BuildConfig.DEBUG) checkId()
} }
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id
} }
data class GroupItem(override val id: String, data class GroupItem(override val id: String,
@ -73,7 +76,7 @@ sealed class MatrixItem(
} }
} }
fun getBestName(): String { open fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id return displayName?.takeIf { it.isNotBlank() } ?: id
} }
@ -95,7 +98,7 @@ sealed class MatrixItem(
} }
fun firstLetterOfDisplayName(): String { fun firstLetterOfDisplayName(): String {
return getBestName() return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn -> .let { dn ->
var startIndex = 0 var startIndex = 0
val initial = dn[startIndex] val initial = dn[startIndex]
@ -138,4 +141,5 @@ sealed class MatrixItem(
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.autocomplete.user package im.vector.riotx.features.autocomplete
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -25,23 +25,27 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_autocomplete_user) @EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item)
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() { abstract class AutocompleteMatrixItem : VectorEpoxyModel<AutocompleteMatrixItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var subName: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
holder.nameView.text = matrixItem.getBestName() holder.nameView.text = matrixItem.getBestName()
holder.subNameView.setTextOrHide(subName)
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.userAutocompleteName) val nameView by bind<TextView>(R.id.matrixItemAutocompleteName)
val avatarImageView by bind<ImageView>(R.id.userAutocompleteAvatar) val subNameView by bind<TextView>(R.id.matrixItemAutocompleteSubname)
val avatarImageView by bind<ImageView>(R.id.matrixItemAutocompleteAvatar)
} }
} }

View File

@ -0,0 +1,49 @@
/*
* 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.riotx.features.autocomplete.room
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class AutocompleteRoomController @Inject constructor() : TypedEpoxyController<List<RoomSummary>>() {
var listener: AutocompleteClickListener<RoomSummary>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<RoomSummary>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach { roomSummary ->
autocompleteMatrixItem {
id(roomSummary.roomId)
matrixItem(roomSummary.toMatrixItem())
subName(roomSummary.canonicalAlias)
avatarRenderer(avatarRenderer)
clickListener { _ ->
listener?.onItemClick(roomSummary)
}
}
}
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.riotx.features.autocomplete.room
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteRoomPresenter @Inject constructor(context: Context,
private val controller: AutocompleteRoomController
) : RecyclerViewPresenter<RoomSummary>(context), AutocompleteClickListener<RoomSummary> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller.adapter
}
override fun onItemClick(t: RoomSummary) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryRooms(query)
}
fun render(rooms: Async<List<RoomSummary>>) {
if (rooms is Success) {
controller.setData(rooms())
}
}
interface Callback {
fun onQueryRooms(query: CharSequence?)
}
}

View File

@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
@ -34,7 +35,7 @@ class AutocompleteUserController @Inject constructor() : TypedEpoxyController<Li
return return
} }
data.forEach { user -> data.forEach { user ->
autocompleteUserItem { autocompleteMatrixItem {
id(user.userId) id(user.userId)
matrixItem(user.toMatrixItem()) matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)

View File

@ -60,6 +60,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
@ -68,6 +69,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
@ -83,6 +85,7 @@ import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.attachments.ContactAttachment
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -140,6 +143,7 @@ class RoomDetailFragment @Inject constructor(
private val commandAutocompletePolicy: CommandAutocompletePolicy, private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
@ -150,6 +154,7 @@ class RoomDetailFragment @Inject constructor(
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
AutocompleteRoomPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -582,6 +587,52 @@ class RoomDetailFragment @Inject constructor(
}) })
.build() .build()
autocompleteRoomPresenter.callback = this
Autocomplete.on<RoomSummary>(composerLayout.composerEditText)
.with(CharPolicy('#', true))
.with(autocompleteRoomPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
// Detect last '#' and remove it
var startIndex = editable.lastIndexOf("#")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toRoomAliasMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteUserPresenter.callback = this autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerLayout.composerEditText) Autocomplete.on<User>(composerLayout.composerEditText)
.with(CharPolicy('@', true)) .with(CharPolicy('@', true))
@ -724,6 +775,7 @@ class RoomDetailFragment @Inject constructor(
private fun renderTextComposerState(state: TextComposerViewState) { private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers) autocompleteUserPresenter.render(state.asyncUsers)
autocompleteRoomPresenter.render(state.asyncRooms)
} }
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {
@ -1056,6 +1108,12 @@ class RoomDetailFragment @Inject constructor(
textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) textComposerViewModel.handle(TextComposerAction.QueryUsers(query))
} }
// AutocompleteRoomPresenter.Callback
override fun onQueryRooms(query: CharSequence?) {
textComposerViewModel.handle(TextComposerAction.QueryRooms(query))
}
private fun handleActions(action: EventSharedAction) { private fun handleActions(action: EventSharedAction) {
when (action) { when (action) {
is EventSharedAction.AddReaction -> { is EventSharedAction.AddReaction -> {

View File

@ -20,4 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class TextComposerAction : VectorViewModelAction { sealed class TextComposerAction : VectorViewModelAction {
data class QueryUsers(val query: CharSequence?) : TextComposerAction() data class QueryUsers(val query: CharSequence?) : TextComposerAction()
data class QueryRooms(val query: CharSequence?) : TextComposerAction()
} }

View File

@ -24,6 +24,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -32,16 +33,16 @@ import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
typealias AutocompleteUserQuery = CharSequence typealias AutocompleteQuery = CharSequence
class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState, class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState,
private val session: Session private val session: Session
) : VectorViewModel<TextComposerViewState, TextComposerAction>(initialState) { ) : VectorViewModel<TextComposerViewState, TextComposerAction>(initialState) {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId
private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteUserQuery>>() private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
private val roomsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -59,11 +60,13 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
init { init {
observeUsersQuery() observeUsersQuery()
observeRoomsQuery()
} }
override fun handle(action: TextComposerAction) { override fun handle(action: TextComposerAction) {
when (action) { when (action) {
is TextComposerAction.QueryUsers -> handleQueryUsers(action) is TextComposerAction.QueryUsers -> handleQueryUsers(action)
is TextComposerAction.QueryRooms -> handleQueryRooms(action)
} }
} }
@ -72,8 +75,14 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
usersQueryObservable.accept(query) usersQueryObservable.accept(query)
} }
private fun handleQueryRooms(action: TextComposerAction.QueryRooms) {
val query = Option.fromNullable(action.query)
roomsQueryObservable.accept(query)
}
private fun observeUsersQuery() { private fun observeUsersQuery() {
Observable.combineLatest<List<String>, Option<AutocompleteUserQuery>, List<User>>( Observable.combineLatest<List<String>, Option<AutocompleteQuery>, List<User>>(
room.rx().liveRoomMemberIds(), room.rx().liveRoomMemberIds(),
usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomMemberIds, query -> BiFunction { roomMemberIds, query ->
@ -87,6 +96,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false
} }
} }
.sortedBy { it.displayName }
} }
).execute { async -> ).execute { async ->
copy( copy(
@ -94,4 +104,24 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
) )
} }
} }
private fun observeRoomsQuery() {
Observable.combineLatest<List<RoomSummary>, Option<AutocompleteQuery>, List<RoomSummary>>(
session.rx().liveRoomSummaries(),
roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomSummaries, query ->
val filter = query.orNull() ?: ""
// Keep only room with a canonical alias
roomSummaries
.filter {
it.canonicalAlias?.contains(filter, ignoreCase = true) == true
}
.sortedBy { it.displayName }
}
).execute { async ->
copy(
asyncRooms = async
)
}
}
} }

View File

@ -19,11 +19,13 @@ package im.vector.riotx.features.home.room.detail.composer
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.RoomDetailArgs
data class TextComposerViewState(val roomId: String, data class TextComposerViewState(val roomId: String,
val asyncUsers: Async<List<User>> = Uninitialized val asyncUsers: Async<List<User>> = Uninitialized,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId)

View File

@ -0,0 +1,51 @@
<?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="wrap_content"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/matrixItemAutocompleteAvatar"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/matrixItemAutocompleteName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
android:textStyle="bold"
tools:text="name" />
<TextView
android:id="@+id/matrixItemAutocompleteSubname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="2dp"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
android:visibility="gone"
tools:text="name"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View File

@ -1,30 +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="wrap_content"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/userAutocompleteAvatar"
android:layout_width="28dp"
android:layout_height="28dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/userAutocompleteName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
android:textStyle="bold"
tools:text="name" />
</LinearLayout>