diff --git a/CHANGES.md b/CHANGES.md index 863b1ca455..ac4b27596e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,8 @@ Features ✨: - Improvements 🙌: - - + - Render aliases and canonical alias change in the timeline + - Fix autocompletion issues and add support for rooms and groups Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index fc02cf4a61..cd4ce1206e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.permalinks import android.text.Spannable -import im.vector.matrix.android.api.MatrixPatterns /** * 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. */ + @Suppress("UNUSED_PARAMETER") 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 if (spannable.isEmpty()) { return false @@ -50,5 +55,7 @@ object MatrixLinkify { } } return hasMatch + */ + return false } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt index d10152f4fe..871c30e46a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt @@ -56,23 +56,23 @@ object PermalinkParser { val identifier = params.getOrNull(0) val extraParameter = params.getOrNull(1) - if (identifier.isNullOrEmpty()) { - return PermalinkData.FallbackLink(uri) - } return when { + identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isRoomId(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } MatrixPatterns.isRoomAlias(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } else -> PermalinkData.FallbackLink(uri) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 38c24fa89b..22bf564a8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -50,10 +50,10 @@ object EventType { const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" - const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias" - const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility" - const val STATE_RELATED_GROUPS = "m.room.related_groups" - const val STATE_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" + const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" + const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" // Call Events @@ -86,10 +86,12 @@ object EventType { STATE_ROOM_JOIN_RULES, STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, + STATE_ROOM_ALIASES, STATE_ROOM_TOMBSTONE, - STATE_HISTORY_VISIBILITY, - STATE_RELATED_GROUPS, - STATE_PINNED_EVENT + STATE_ROOM_CANONICAL_ALIAS, + STATE_ROOM_HISTORY_VISIBILITY, + STATE_ROOM_RELATED_GROUPS, + STATE_ROOM_PINNED_EVENT ) fun isStateEvent(type: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index ff63d1a9e7..2d55d0be57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -31,6 +31,13 @@ interface GroupService { */ 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. * @return the [LiveData] of [GroupSummary] diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index afe7cf8bc3..ba3b5ded78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -52,6 +52,13 @@ interface RoomService { */ 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. * @return the [LiveData] of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt index a66f23555d..0aec7f6c8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json 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) data class RoomCanonicalAliasContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 598aab2d30..bc1e941698 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -145,13 +145,13 @@ class CreateRoomParams { */ fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { // 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) { val contentMap = HashMap() contentMap["history_visibility"] = historyVisibility - val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, + val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "", content = contentMap.toContent()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index b3dd1c6f22..7d8f2f0bc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -98,7 +98,7 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * 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. * @param eventReplied the event referenced by the reply * @param replyText the reply text diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt index 71a422bac8..d191f5197b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt @@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send 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 */ -interface UserMentionSpan { +interface MatrixItemSpan { val matrixItem: MatrixItem } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index bdae5eaaa6..ac1b50bbcb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,7 +29,7 @@ interface SendService { /** * 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. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 4fed773ae2..4c8082b77e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -62,6 +62,9 @@ sealed class MatrixItem( init { 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, @@ -71,9 +74,12 @@ sealed class MatrixItem( init { 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 } @@ -95,7 +101,7 @@ sealed class MatrixItem( } fun firstLetterOfDisplayName(): String { - return getBestName() + return (displayName?.takeIf { it.isNotBlank() } ?: id) .let { dn -> var startIndex = 0 val initial = dn[startIndex] @@ -138,4 +144,5 @@ sealed class MatrixItem( fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index c50b9e2e10..28a9fad35f 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -145,17 +145,17 @@ internal class DefaultCryptoService @Inject constructor( fun onStateEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } fun onLiveEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index be059038f3..192c6fe40c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -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.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.util.fetchCopyMap import javax.inject.Inject 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 } + override fun getGroupSummary(groupId: String): GroupSummary? { + return monarchy.fetchCopyMap( + { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() }, + { it, _ -> it.asDomain() } + ) + } + override fun liveGroupSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index de60e6e7e4..b53fa3ce33 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -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.RoomSummaryEntity 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.session.room.alias.GetRoomIdByAliasTask 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.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm 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> { return monarchy.findAllMappedWithChanges( { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 126d13c5db..c4d0d50283 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, @@ -93,7 +93,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) 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() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 2271631932..21270308ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -62,7 +62,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: 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()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index c303e1c215..de3eb1eab2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -105,10 +105,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M private fun computeAllowedKeys(type: String): List { // Add filtered content, allowed keys in content depends on the event type return when (type) { - EventType.STATE_ROOM_MEMBER -> listOf("membership") - EventType.STATE_ROOM_CREATE -> listOf("creator") - EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") - EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", "users_default", "events", "events_default", @@ -117,10 +117,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M "kick", "redact", "invite") - EventType.STATE_ROOM_ALIASES -> listOf("aliases") - EventType.STATE_CANONICAL_ALIAS -> listOf("alias") - EventType.FEEDBACK -> listOf("type", "target_event_id") - else -> emptyList() + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt index 5ad61b5441..055eade5e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt @@ -16,10 +16,10 @@ 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( - val span: UserMentionSpan, + val span: MatrixItemSpan, val start: Int, val end: Int ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index c079e456c0..1a7b8228b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -16,15 +16,13 @@ package im.vector.matrix.android.internal.session.room.send.pills 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 javax.inject.Inject /** * Utility class to detect special span in CharSequence and turn them into * formatted text to send them as a Matrix messages. - * - * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ internal class TextPillsUtils @Inject constructor( private val mentionLinkSpecComparator: MentionLinkSpecComparator @@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor( private fun transformPills(text: CharSequence, template: String): String? { val spannableString = SpannableString.valueOf(text) 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)) } ?.toMutableList() ?.takeIf { it.isNotEmpty() } @@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor( // append text before pill append(text, currIndex, start) // 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 } // append text after the last pill diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index 03bc6d3684..84aa9d9f5c 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -2,6 +2,19 @@ + + %1$s added %2$s as an address for this room. + %1$s added %2$s as addresses for this room. + + + %1$s removed %2$s as an address for this room. + %1$s removed %3$s as addresses for this room. + - \ No newline at end of file + %1$s added %2$s and removed %3$s as addresses for this room. + + "%1$s set the main address for this room to %2$s." + "%1$s removed the main address for this room." + + diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt similarity index 71% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt index 8581ba8e2c..d5eb90a62c 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.autocomplete.user +package im.vector.riotx.features.autocomplete import android.view.View 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.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_autocomplete_user) -abstract class AutocompleteUserItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item) +abstract class AutocompleteMatrixItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var subName: String? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) holder.nameView.text = matrixItem.getBestName() + holder.subNameView.setTextOrHide(subName) avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { - val nameView by bind(R.id.userAutocompleteName) - val avatarImageView by bind(R.id.userAutocompleteAvatar) + val nameView by bind(R.id.matrixItemAutocompleteName) + val subNameView by bind(R.id.matrixItemAutocompleteSubname) + val avatarImageView by bind(R.id.matrixItemAutocompleteAvatar) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt deleted file mode 100644 index 227f1b2f9c..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt +++ /dev/null @@ -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(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { - - private var recyclerView: EpoxyRecyclerView? = null - private var clicks: AutocompletePresenter.ClickProvider? = null - private var observer: Observer? = null - - override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) { - 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() - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt index 915689fbeb..84ae8db217 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -17,21 +17,28 @@ package im.vector.riotx.features.autocomplete.command import android.content.Context -import com.airbnb.epoxy.EpoxyController -import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.command.Command import javax.inject.Inject class AutocompleteCommandPresenter @Inject constructor(context: Context, private val controller: AutocompleteCommandController) : - EpoxyAutocompletePresenter(context) { + RecyclerViewPresenter(context), AutocompleteClickListener { init { controller.listener = this } - override fun providesController(): EpoxyController { - return controller + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null + return controller.adapter + } + + override fun onItemClick(t: Command) { + dispatchClick(t) } override fun onQuery(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt new file mode 100644 index 0000000000..5d0d43d9ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt @@ -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>() { + + var listener: AutocompleteClickListener? = null + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { groupSummary -> + autocompleteMatrixItem { + id(groupSummary.groupId) + matrixItem(groupSummary.toMatrixItem()) + avatarRenderer(avatarRenderer) + clickListener { _ -> + listener?.onItemClick(groupSummary) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt new file mode 100644 index 0000000000..822ce451e7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -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(context), AutocompleteClickListener { + + 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>) { + if (groups is Success) { + controller.setData(groups()) + } + } + + interface Callback { + fun onQueryGroups(query: CharSequence?) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt new file mode 100644 index 0000000000..51285b02b7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt @@ -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>() { + + var listener: AutocompleteClickListener? = null + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { roomSummary -> + autocompleteMatrixItem { + id(roomSummary.roomId) + matrixItem(roomSummary.toMatrixItem()) + subName(roomSummary.canonicalAlias) + avatarRenderer(avatarRenderer) + clickListener { _ -> + listener?.onItemClick(roomSummary) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt new file mode 100644 index 0000000000..53fed7f859 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -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(context), AutocompleteClickListener { + + 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>) { + if (rooms is Success) { + controller.setData(rooms()) + } + } + + interface Callback { + fun onQueryRooms(query: CharSequence?) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt index 8f0090001f..53a87fe27a 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt @@ -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.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 @@ -34,7 +35,7 @@ class AutocompleteUserController @Inject constructor() : TypedEpoxyController
  • - autocompleteUserItem { + autocompleteMatrixItem { id(user.userId) matrixItem(user.toMatrixItem()) avatarRenderer(avatarRenderer) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt index 5c2d2c49c0..01dceb5399 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -17,16 +17,17 @@ package im.vector.riotx.features.autocomplete.user import android.content.Context -import com.airbnb.epoxy.EpoxyController +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.user.model.User -import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener import javax.inject.Inject class AutocompleteUserPresenter @Inject constructor(context: Context, private val controller: AutocompleteUserController -) : EpoxyAutocompletePresenter(context) { +) : RecyclerViewPresenter(context), AutocompleteClickListener { var callback: Callback? = null @@ -34,8 +35,14 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, controller.listener = this } - override fun providesController(): EpoxyController { - return controller + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null + return controller.adapter + } + + override fun onItemClick(t: User) { + dispatchClick(t) } override fun onQuery(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 49f23f7f2c..cb54aba651 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -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.content.ContentAttachmentData 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.RoomSummary 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.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.util.MatrixItem 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.core.dialogs.withColoredButton 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.autocomplete.command.AutocompleteCommandPresenter 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.command.Command import im.vector.riotx.features.home.AvatarRenderer @@ -140,6 +145,8 @@ class RoomDetailFragment @Inject constructor( private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteRoomPresenter: AutocompleteRoomPresenter, + private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -150,6 +157,8 @@ class RoomDetailFragment @Inject constructor( VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -582,6 +591,98 @@ class RoomDetailFragment @Inject constructor( }) .build() + autocompleteRoomPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) + .with(CharPolicy('#', true)) + .with(autocompleteRoomPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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(composerLayout.composerEditText) + .with(CharPolicy('+', true)) + .with(autocompleteGroupPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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 Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) @@ -724,6 +825,8 @@ class RoomDetailFragment @Inject constructor( private fun renderTextComposerState(state: TextComposerViewState) { autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) } private fun renderTombstoneEventHandling(async: Async) { @@ -1056,6 +1159,18 @@ class RoomDetailFragment @Inject constructor( 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) { when (action) { is EventSharedAction.AddReaction -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt index 5d60fa1cef..0f5bf2a8c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt @@ -20,4 +20,6 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class TextComposerAction : VectorViewModelAction { data class QueryUsers(val query: CharSequence?) : TextComposerAction() + data class QueryRooms(val query: CharSequence?) : TextComposerAction() + data class QueryGroups(val query: CharSequence?) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 88548e12b4..f7ec78c6c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -24,6 +24,8 @@ import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.rx.rx import im.vector.riotx.core.platform.VectorViewModel @@ -32,16 +34,17 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import java.util.concurrent.TimeUnit -typealias AutocompleteUserQuery = CharSequence +typealias AutocompleteQuery = CharSequence class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState, private val session: Session ) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! - private val roomId = initialState.roomId - private val usersQueryObservable = BehaviorRelay.create>() + private val usersQueryObservable = BehaviorRelay.create>() + private val roomsQueryObservable = BehaviorRelay.create>() + private val groupsQueryObservable = BehaviorRelay.create>() @AssistedInject.Factory interface Factory { @@ -59,11 +62,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: init { observeUsersQuery() + observeRoomsQuery() + observeGroupsQuery() } override fun handle(action: TextComposerAction) { 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) } + 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() { - Observable.combineLatest, Option, List>( + Observable.combineLatest, Option, List>( room.rx().liveRoomMemberIds(), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { roomMemberIds, query -> @@ -84,9 +101,10 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: users } else { users.filter { - it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false + it.displayName?.contains(filter, ignoreCase = true) ?: false } } + .sortedBy { it.displayName } } ).execute { async -> copy( @@ -94,4 +112,47 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: ) } } + + private fun observeRoomsQuery() { + Observable.combineLatest, Option, List>( + 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, Option, List>( + 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 + ) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt index b2cec09096..e863970afe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt @@ -19,11 +19,15 @@ package im.vector.riotx.features.home.room.detail.composer import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState 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.riotx.features.home.room.detail.RoomDetailArgs data class TextComposerViewState(val roomId: String, - val asyncUsers: Async> = Uninitialized + val asyncUsers: Async> = Uninitialized, + val asyncRooms: Async> = Uninitialized, + val asyncGroups: Async> = Uninitialized ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 1303c3aad9..d537b66ec3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -187,7 +187,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, 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_HANGUP, EventType.CALL_ANSWER -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 5b6dec9900..4a7a1e2a86 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -45,8 +45,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 75100e6c03..9cb045c01e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -33,19 +33,21 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(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_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(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_MEMBER -> formatRoomMemberEvent(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.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, - EventType.REDACTION -> formatDebug(timelineEvent.root) - else -> { + EventType.REDACTION -> formatDebug(timelineEvent.root) + else -> { Timber.v("Type $type not handled by this formatter") null } @@ -54,16 +56,16 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(event: Event, senderName: String?): CharSequence? { return when (val type = event.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) - EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(event, senderName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(senderName) - else -> { + EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(senderName) + else -> { Timber.v("Type $type not handled by this formatter") null } @@ -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 { val displayText = StringBuilder() // Check display name has been changed diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 3331fbf774..7ee8486ba2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -33,6 +33,7 @@ import me.gujun.android.span.span 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 */ class MessageInformationDataFactory @Inject constructor(private val session: Session, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 1cd851f8c8..b0f3e617a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -27,7 +27,9 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, 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_HANGUP, EventType.CALL_ANSWER, diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index 3f16666221..76f7dfaabd 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -20,6 +20,7 @@ import android.content.Context import android.text.style.URLSpan import im.vector.matrix.android.api.permalinks.PermalinkData 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.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideRequests @@ -39,26 +40,47 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, val link = tag.attributes()["href"] if (link != null) { val permalinkData = PermalinkParser.parse(link) - when (permalinkData) { - is PermalinkData.UserLink -> { + val matrixItem = when (permalinkData) { + is PermalinkData.UserLink -> { val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, context, MatrixItem.UserItem(permalinkData.userId, user?.displayName - ?: permalinkData.userId, user?.avatarUrl)) - SpannableBuilder.setSpans( - visitor.builder(), - span, - tag.start(), - tag.end() - ) - // also add clickable span - SpannableBuilder.setSpans( - visitor.builder(), - URLSpan(link), - tag.start(), - tag.end() - ) + MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } - else -> super.handle(visitor, renderer, tag) + 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( + visitor.builder(), + span, + tag.start(), + tag.end() + ) + SpannableBuilder.setSpans( + visitor.builder(), + URLSpan(link), + tag.start(), + tag.end() + ) } } else { super.handle(visitor, renderer, tag) diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index 8b57006439..a609541a62 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -28,7 +28,7 @@ import androidx.annotation.UiThread import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition 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.riotx.R 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] * 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, private val avatarRenderer: AvatarRenderer, private val context: Context, override val matrixItem: MatrixItem -) : ReplacementSpan(), UserMentionSpan { +) : ReplacementSpan(), MatrixItemSpan { private val pillDrawable = createChipDrawable() private val target = PillImageSpanTarget(this) diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 69b5ce2fac..0bd6665325 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -1,5 +1,4 @@ - - - - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="8dp" + android:paddingTop="6dp" + android:paddingEnd="8dp" + android:paddingBottom="6dp"> diff --git a/vector/src/main/res/layout/item_autocomplete_matrix_item.xml b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml new file mode 100644 index 0000000000..84a85d0102 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_autocomplete_user.xml b/vector/src/main/res/layout/item_autocomplete_user.xml deleted file mode 100644 index f2fdb354a9..0000000000 --- a/vector/src/main/res/layout/item_autocomplete_user.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml index 4738f331b1..7889efacc4 100644 --- a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml @@ -1,5 +1,4 @@ - - - - diff --git a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml index 137d96408d..548594fcd4 100644 --- a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml +++ b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml @@ -1,5 +1,4 @@ - diff --git a/vector/src/main/res/layout/vector_settings_round_avatar.xml b/vector/src/main/res/layout/vector_settings_round_avatar.xml index 66c2d2412b..fba69dc588 100644 --- a/vector/src/main/res/layout/vector_settings_round_avatar.xml +++ b/vector/src/main/res/layout/vector_settings_round_avatar.xml @@ -1,5 +1,4 @@ - - diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index f713561084..134ad47c92 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -1,5 +1,4 @@ - -