Merge branch 'develop' into feature/initial_sync

This commit is contained in:
Benoit Marty 2019-12-20 17:55:04 +01:00 committed by GitHub
commit 63828bc159
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 723 additions and 254 deletions

View File

@ -6,6 +6,8 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- The initial sync is now handled by a foreground service - The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline
- Fix autocompletion issues and add support for rooms and groups
Other changes: Other changes:
- -

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.permalinks package im.vector.matrix.android.api.permalinks
import android.text.Spannable import android.text.Spannable
import im.vector.matrix.android.api.MatrixPatterns
/** /**
* MatrixLinkify take a piece of text and turns all of the * MatrixLinkify take a piece of text and turns all of the
@ -30,7 +29,13 @@ object MatrixLinkify {
* *
* @param spannable the text in which the matrix items has to be clickable. * @param spannable the text in which the matrix items has to be clickable.
*/ */
@Suppress("UNUSED_PARAMETER")
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {
/**
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/
/*
// sanity checks // sanity checks
if (spannable.isEmpty()) { if (spannable.isEmpty()) {
return false return false
@ -50,5 +55,7 @@ object MatrixLinkify {
} }
} }
return hasMatch return hasMatch
*/
return false
} }
} }

View File

@ -56,23 +56,23 @@ object PermalinkParser {
val identifier = params.getOrNull(0) val identifier = params.getOrNull(0)
val extraParameter = params.getOrNull(1) val extraParameter = params.getOrNull(1)
if (identifier.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
return when { return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
val eventId = extraParameter.takeIf { PermalinkData.RoomLink(
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it) roomIdOrAlias = identifier,
} isRoomAlias = false,
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
} }
MatrixPatterns.isRoomAlias(identifier) -> { MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf { PermalinkData.RoomLink(
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it) roomIdOrAlias = identifier,
} isRoomAlias = true,
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
} }
else -> PermalinkData.FallbackLink(uri) else -> PermalinkData.FallbackLink(uri)
} }

View File

@ -50,10 +50,10 @@ object EventType {
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_ALIASES = "m.room.aliases"
const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias" const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility" const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_RELATED_GROUPS = "m.room.related_groups" const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
const val STATE_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
// Call Events // Call Events
@ -86,10 +86,12 @@ object EventType {
STATE_ROOM_JOIN_RULES, STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS, STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS, STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE, STATE_ROOM_TOMBSTONE,
STATE_HISTORY_VISIBILITY, STATE_ROOM_CANONICAL_ALIAS,
STATE_RELATED_GROUPS, STATE_ROOM_HISTORY_VISIBILITY,
STATE_PINNED_EVENT STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT
) )
fun isStateEvent(type: String): Boolean { fun isStateEvent(type: String): Boolean {

View File

@ -31,6 +31,13 @@ interface GroupService {
*/ */
fun getGroup(groupId: String): Group? fun getGroup(groupId: String): Group?
/**
* Get a groupSummary from a groupId
* @param groupId the groupId to look for.
* @return the groupSummary with groupId or null
*/
fun getGroupSummary(groupId: String): GroupSummary?
/** /**
* Get a live list of group summaries. This list is refreshed as soon as the data changes. * Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary] * @return the [LiveData] of [GroupSummary]

View File

@ -52,6 +52,13 @@ interface RoomService {
*/ */
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
/**
* Get a roomSummary from a roomId or a room alias
* @param roomIdOrAlias the roomId or the alias of a room to look for.
* @return a matching room summary or null
*/
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
/** /**
* Get a live list of room summaries. This list is refreshed as soon as the data changes. * Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [RoomSummary] * @return the [LiveData] of [RoomSummary]

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/** /**
* Class representing the EventType.STATE_CANONICAL_ALIAS state event content * Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomCanonicalAliasContent( data class RoomCanonicalAliasContent(

View File

@ -145,13 +145,13 @@ class CreateRoomParams {
*/ */
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
// Remove the existing value if any. // Remove the existing value if any.
initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY } initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY }
if (historyVisibility != null) { if (historyVisibility != null) {
val contentMap = HashMap<String, RoomHistoryVisibility>() val contentMap = HashMap<String, RoomHistoryVisibility>()
contentMap["history_visibility"] = historyVisibility contentMap["history_visibility"] = historyVisibility
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "", stateKey = "",
content = contentMap.toContent()) content = contentMap.toContent())

View File

@ -98,7 +98,7 @@ interface RelationService {
/** /**
* Reply to an event in the timeline (must be in same room) * Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated * The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills. * by the sdk into pills.
* @param eventReplied the event referenced by the reply * @param eventReplied the event referenced by the reply
* @param replyText the reply text * @param replyText the reply text

View File

@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
/** /**
* Tag class for spans that should mention a user. * Tag class for spans that should mention a matrix item.
* These Spans will be transformed into pills when detected in message to send * These Spans will be transformed into pills when detected in message to send
*/ */
interface UserMentionSpan { interface MatrixItemSpan {
val matrixItem: MatrixItem val matrixItem: MatrixItem
} }

View File

@ -29,7 +29,7 @@ interface SendService {
/** /**
* Method to send a text message asynchronously. * Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills. * by the sdk into pills.
* @param text the text message to send * @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE

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,
@ -71,9 +74,12 @@ 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
} }
fun getBestName(): String { open fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id return displayName?.takeIf { it.isNotBlank() } ?: id
} }
@ -95,7 +101,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 +144,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

@ -147,7 +147,7 @@ internal class DefaultCryptoService @Inject constructor(
when { when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }
@ -155,7 +155,7 @@ internal class DefaultCryptoService @Inject constructor(
when { when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopyMap
import javax.inject.Inject import javax.inject.Inject
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService {
@ -33,6 +34,13 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon
return null return null
} }
override fun getGroupSummary(groupId: String): GroupSummary? {
return monarchy.fetchCopyMap(
{ realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
{ it, _ -> it.asDomain() }
)
}
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> { override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) },

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
@ -38,6 +39,7 @@ import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
@ -69,6 +71,21 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
} }
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
return monarchy
.fetchCopyMap({
if (roomIdOrAlias.startsWith("!")) {
// It's a roomId
RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst()
} else {
// Assume it's a room alias
RoomSummaryEntity.findByAlias(it, roomIdOrAlias)
}
}, { entity, _ ->
roomSummaryMapper.map(entity)
})
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> { realm ->

View File

@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER, EventType.CALL_ANSWER,
@ -91,7 +91,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0

View File

@ -62,7 +62,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return@doWithRealm return@doWithRealm
} }
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) { if (!name.isNullOrEmpty()) {
return@doWithRealm return@doWithRealm

View File

@ -118,7 +118,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
"redact", "redact",
"invite") "invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases") EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias") EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id") EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList() else -> emptyList()
} }

View File

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.session.room.send.pills package im.vector.matrix.android.internal.session.room.send.pills
import im.vector.matrix.android.api.session.room.send.UserMentionSpan import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
internal data class MentionLinkSpec( internal data class MentionLinkSpec(
val span: UserMentionSpan, val span: MatrixItemSpan,
val start: Int, val start: Int,
val end: Int val end: Int
) )

View File

@ -16,15 +16,13 @@
package im.vector.matrix.android.internal.session.room.send.pills package im.vector.matrix.android.internal.session.room.send.pills
import android.text.SpannableString import android.text.SpannableString
import im.vector.matrix.android.api.session.room.send.UserMentionSpan import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** /**
* Utility class to detect special span in CharSequence and turn them into * Utility class to detect special span in CharSequence and turn them into
* formatted text to send them as a Matrix messages. * formatted text to send them as a Matrix messages.
*
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
*/ */
internal class TextPillsUtils @Inject constructor( internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator private val mentionLinkSpecComparator: MentionLinkSpecComparator
@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor(
private fun transformPills(text: CharSequence, template: String): String? { private fun transformPills(text: CharSequence, template: String): String? {
val spannableString = SpannableString.valueOf(text) val spannableString = SpannableString.valueOf(text)
val pills = spannableString val pills = spannableString
?.getSpans(0, text.length, UserMentionSpan::class.java) ?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.toMutableList() ?.toMutableList()
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // append the pill
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName)) append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName()))
currIndex = end currIndex = end
} }
// append text after the last pill // append text after the last pill

View File

@ -2,6 +2,19 @@
<resources> <resources>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s added %2$s as an address for this room.</item>
<item quantity="other">%1$s added %2$s as addresses for this room.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s removed %2$s as an address for this room.</item>
<item quantity="other">%1$s removed %3$s as addresses for this room.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string>
<string name="notice_room_canonical_alias_set">"%1$s set the main address for this room to %2$s."</string>
<string name="notice_room_canonical_alias_unset">"%1$s removed the main address for this room."</string>
</resources> </resources>

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

@ -1,91 +0,0 @@
/*
* 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
import android.content.Context
import android.database.DataSetObserver
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompletePresenter
abstract class EpoxyAutocompletePresenter<T>(context: Context) : AutocompletePresenter<T>(context), AutocompleteClickListener<T> {
private var recyclerView: EpoxyRecyclerView? = null
private var clicks: AutocompletePresenter.ClickProvider<T>? = null
private var observer: Observer? = null
override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider<T>) {
this.clicks = provider
}
override fun registerDataSetObserver(observer: DataSetObserver) {
this.observer = Observer(observer)
}
override fun getView(): ViewGroup? {
recyclerView = EpoxyRecyclerView(context).apply {
setController(providesController())
observer?.let {
adapter?.registerAdapterDataObserver(it)
}
itemAnimator = null
}
return recyclerView
}
override fun onViewShown() {}
override fun onViewHidden() {
recyclerView = null
observer = null
}
abstract fun providesController(): EpoxyController
protected fun dispatchLayoutChange() {
observer?.onChanged()
}
override fun onItemClick(t: T) {
clicks?.click(t)
}
private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
root.onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
root.onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
root.onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
root.onChanged()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
root.onChanged()
}
}
}

View File

@ -17,21 +17,28 @@
package im.vector.riotx.features.autocomplete.command package im.vector.riotx.features.autocomplete.command
import android.content.Context import android.content.Context
import com.airbnb.epoxy.EpoxyController import androidx.recyclerview.widget.RecyclerView
import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import javax.inject.Inject import javax.inject.Inject
class AutocompleteCommandPresenter @Inject constructor(context: Context, class AutocompleteCommandPresenter @Inject constructor(context: Context,
private val controller: AutocompleteCommandController) : private val controller: AutocompleteCommandController) :
EpoxyAutocompletePresenter<Command>(context) { RecyclerViewPresenter<Command>(context), AutocompleteClickListener<Command> {
init { init {
controller.listener = this controller.listener = this
} }
override fun providesController(): EpoxyController { override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller // Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: Command) {
dispatchClick(t)
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {

View File

@ -0,0 +1,48 @@
/*
* 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.group
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary
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 AutocompleteGroupController @Inject constructor() : TypedEpoxyController<List<GroupSummary>>() {
var listener: AutocompleteClickListener<GroupSummary>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<GroupSummary>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach { groupSummary ->
autocompleteMatrixItem {
id(groupSummary.groupId)
matrixItem(groupSummary.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
listener?.onItemClick(groupSummary)
}
}
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.group
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.group.model.GroupSummary
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteGroupPresenter @Inject constructor(context: Context,
private val controller: AutocompleteGroupController
) : RecyclerViewPresenter<GroupSummary>(context), AutocompleteClickListener<GroupSummary> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: GroupSummary) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryGroups(query)
}
fun render(groups: Async<List<GroupSummary>>) {
if (groups is Success) {
controller.setData(groups())
}
}
interface Callback {
fun onQueryGroups(query: CharSequence?)
}
}

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,61 @@
/*
* 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<*> {
// Also remove animation
recyclerView?.itemAnimator = null
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

@ -17,16 +17,17 @@
package im.vector.riotx.features.autocomplete.user package im.vector.riotx.features.autocomplete.user
import android.content.Context import android.content.Context
import com.airbnb.epoxy.EpoxyController import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
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.autocomplete.EpoxyAutocompletePresenter import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject import javax.inject.Inject
class AutocompleteUserPresenter @Inject constructor(context: Context, class AutocompleteUserPresenter @Inject constructor(context: Context,
private val controller: AutocompleteUserController private val controller: AutocompleteUserController
) : EpoxyAutocompletePresenter<User>(context) { ) : RecyclerViewPresenter<User>(context), AutocompleteClickListener<User> {
var callback: Callback? = null var callback: Callback? = null
@ -34,8 +35,14 @@ class AutocompleteUserPresenter @Inject constructor(context: Context,
controller.listener = this controller.listener = this
} }
override fun providesController(): EpoxyController { override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller // Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: User) {
dispatchClick(t)
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {

View File

@ -59,7 +59,9 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session 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.group.model.GroupSummary
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 +70,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 +86,8 @@ 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.group.AutocompleteGroupPresenter
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 +145,8 @@ 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 autocompleteGroupPresenter: AutocompleteGroupPresenter,
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 +157,8 @@ class RoomDetailFragment @Inject constructor(
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -592,6 +601,98 @@ 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()
autocompleteGroupPresenter.callback = this
Autocomplete.on<GroupSummary>(composerLayout.composerEditText)
.with(CharPolicy('+', true))
.with(autocompleteGroupPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): 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.toMatrixItem()
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))
@ -734,6 +835,8 @@ 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)
autocompleteGroupPresenter.render(state.asyncGroups)
} }
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {
@ -1066,6 +1169,18 @@ 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))
}
// AutocompleteGroupPresenter.Callback
override fun onQueryGroups(query: CharSequence?) {
textComposerViewModel.handle(TextComposerAction.QueryGroups(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,6 @@ 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()
data class QueryGroups(val query: CharSequence?) : TextComposerAction()
} }

View File

@ -24,6 +24,8 @@ 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.group.model.GroupSummary
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 +34,17 @@ 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>>()
private val groupsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -59,11 +62,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
init { init {
observeUsersQuery() observeUsersQuery()
observeRoomsQuery()
observeGroupsQuery()
} }
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)
is TextComposerAction.QueryGroups -> handleQueryGroups(action)
} }
} }
@ -72,8 +79,18 @@ 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 handleQueryGroups(action: TextComposerAction.QueryGroups) {
val query = Option.fromNullable(action.query)
groupsQueryObservable.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 ->
@ -84,9 +101,10 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
users users
} else { } else {
users.filter { users.filter {
it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false it.displayName?.contains(filter, ignoreCase = true) ?: false
} }
} }
.sortedBy { it.displayName }
} }
).execute { async -> ).execute { async ->
copy( copy(
@ -94,4 +112,47 @@ 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
)
}
}
private fun observeGroupsQuery() {
Observable.combineLatest<List<GroupSummary>, Option<AutocompleteQuery>, List<GroupSummary>>(
session.rx().liveGroupSummaries(),
groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { groupSummaries, query ->
val filter = query.orNull()
if (filter.isNullOrBlank()) {
groupSummaries
} else {
groupSummaries
.filter {
it.groupId.contains(filter, ignoreCase = true)
}
}
.sortedBy { it.displayName }
}
).execute { async ->
copy(
asyncGroups = async
)
}
}
} }

View File

@ -19,11 +19,15 @@ 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.group.model.GroupSummary
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,
val asyncGroups: Async<List<GroupSummary>> = Uninitialized
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId)

View File

@ -187,7 +187,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {

View File

@ -45,8 +45,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER, EventType.CALL_ANSWER,

View File

@ -37,7 +37,9 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
@ -58,7 +60,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName) EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
@ -136,6 +138,34 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
} }
} }
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
val eventContent: RoomAliasesContent? = event.getClearContent().toModel()
val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel()
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty()
return if (addedAliases.isNotEmpty() && removedAliases.isNotEmpty()) {
sp.getString(R.string.notice_room_aliases_added_and_removed, senderName, addedAliases.joinToString(), removedAliases.joinToString())
} else if (addedAliases.isNotEmpty()) {
sp.getQuantityString(R.plurals.notice_room_aliases_added, addedAliases.size, senderName, addedAliases.joinToString())
} else if (removedAliases.isNotEmpty()) {
sp.getQuantityString(R.plurals.notice_room_aliases_removed, removedAliases.size, senderName, removedAliases.joinToString())
} else {
Timber.w("Alias event without any change...")
null
}
}
private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? {
val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel()
val canonicalAlias = eventContent?.canonicalAlias
return canonicalAlias
?.takeIf { it.isNotBlank() }
?.let { sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) }
?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String { private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
val displayText = StringBuilder() val displayText = StringBuilder()
// Check display name has been changed // Check display name has been changed

View File

@ -33,6 +33,7 @@ import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
/** /**
* TODO Update this comment
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
*/ */
class MessageInformationDataFactory @Inject constructor(private val session: Session, class MessageInformationDataFactory @Inject constructor(private val session: Session,

View File

@ -27,7 +27,9 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER, EventType.CALL_ANSWER,

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.text.style.URLSpan import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.core.glide.GlideRequests
@ -39,18 +40,41 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
val link = tag.attributes()["href"] val link = tag.attributes()["href"]
if (link != null) { if (link != null) {
val permalinkData = PermalinkParser.parse(link) val permalinkData = PermalinkParser.parse(link)
when (permalinkData) { val matrixItem = when (permalinkData) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, avatarRenderer, context, MatrixItem.UserItem(permalinkData.userId, user?.displayName MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl)
?: permalinkData.userId, user?.avatarUrl)) }
is PermalinkData.RoomLink -> {
if (permalinkData.eventId == null) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
if (permalinkData.isRoomAlias) {
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
} else {
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
}
} else {
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
null
}
}
is PermalinkData.GroupLink -> {
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
}
else -> null
}
if (matrixItem == null) {
super.handle(visitor, renderer, tag)
} else {
val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem)
SpannableBuilder.setSpans( SpannableBuilder.setSpans(
visitor.builder(), visitor.builder(),
span, span,
tag.start(), tag.start(),
tag.end() tag.end()
) )
// also add clickable span
SpannableBuilder.setSpans( SpannableBuilder.setSpans(
visitor.builder(), visitor.builder(),
URLSpan(link), URLSpan(link),
@ -58,8 +82,6 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
tag.end() tag.end()
) )
} }
else -> super.handle(visitor, renderer, tag)
}
} else { } else {
super.handle(visitor, renderer, tag) super.handle(visitor, renderer, tag)
} }

View File

@ -28,7 +28,7 @@ import androidx.annotation.UiThread
import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import im.vector.matrix.android.api.session.room.send.UserMentionSpan import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.core.glide.GlideRequests
@ -38,13 +38,13 @@ import java.lang.ref.WeakReference
/** /**
* This span is able to replace a text by a [ChipDrawable] * This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
* Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills. * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills.
*/ */
class PillImageSpan(private val glideRequests: GlideRequests, class PillImageSpan(private val glideRequests: GlideRequests,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val context: Context, private val context: Context,
override val matrixItem: MatrixItem override val matrixItem: MatrixItem
) : ReplacementSpan(), UserMentionSpan { ) : ReplacementSpan(), MatrixItemSpan {
private val pillDrawable = createChipDrawable() private val pillDrawable = createChipDrawable()
private val target = PillImageSpanTarget(this) private val target = PillImageSpanTarget(this)

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottomSheetRecyclerView" android:id="@+id/bottomSheetRecyclerView"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"

View File

@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background" android:background="?riotx_background"
android:padding="6dp"> android:foreground="?attr/selectableItemBackground"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="8dp"
android:paddingBottom="6dp">
<TextView <TextView
android:id="@+id/commandName" android:id="@+id/commandName"
@ -41,7 +44,7 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:maxLines="1" android:maxLines="2"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textSize="12sp" android:textSize="12sp"
tools:text="@string/command_description_invite_user" /> tools:text="@string/command_description_invite_user" />

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
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="8dp"
android:layout_marginLeft="8dp"
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>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp"> android:layout_height="20dp">

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp"> android:layout_height="20dp">

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="40dp" android:layout_width="40dp"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp"> android:layout_height="80dp">

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"