From e73970d61b263eefba70c16e5ac9c56c95361504 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:39:35 +0100 Subject: [PATCH 01/20] Render aliases and canonical alias change in the timeline --- CHANGES.md | 2 +- .../src/main/res/values/strings_RiotX.xml | 15 +++++++++- .../action/MessageActionsViewModel.kt | 2 ++ .../timeline/factory/TimelineItemFactory.kt | 2 ++ .../timeline/format/NoticeEventFormatter.kt | 30 +++++++++++++++++++ .../helper/MessageInformationDataFactory.kt | 1 + .../helper/TimelineDisplayableEvents.kt | 2 ++ 7 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 863b1ca455..89c1942f39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Render aliases and canonical alias change in the timeline Other changes: - 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/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..be969fd532 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,6 +187,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_CANONICAL_ALIAS, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, 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..1c1155d7b2 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,6 +45,8 @@ 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_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, 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..2bf861a970 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 @@ -37,6 +37,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active 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_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, @@ -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..4bb0fc27d5 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,6 +27,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_CANONICAL_ALIAS, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, From 07817b69c24058ecdc775e2d6ad9fe3df49a7bbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:46:09 +0100 Subject: [PATCH 02/20] Rename some event type --- .../api/session/events/model/EventType.kt | 14 +++---- .../room/model/RoomCanonicalAliasContent.kt | 2 +- .../room/model/create/CreateRoomParams.kt | 4 +- .../internal/crypto/DefaultCryptoService.kt | 12 +++--- .../session/room/RoomSummaryUpdater.kt | 4 +- .../membership/RoomDisplayNameResolver.kt | 2 +- .../session/room/prune/PruneEventTask.kt | 16 ++++---- .../action/MessageActionsViewModel.kt | 4 +- .../timeline/factory/TimelineItemFactory.kt | 4 +- .../timeline/format/NoticeEventFormatter.kt | 38 +++++++++---------- .../helper/TimelineDisplayableEvents.kt | 4 +- 11 files changed, 52 insertions(+), 52 deletions(-) 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..bca2ded8bc 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 @@ -87,9 +87,9 @@ object EventType { STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, STATE_ROOM_TOMBSTONE, - STATE_HISTORY_VISIBILITY, - STATE_RELATED_GROUPS, - STATE_PINNED_EVENT + 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/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/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/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/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 be969fd532..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 @@ -188,8 +188,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, - EventType.STATE_HISTORY_VISIBILITY, + 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 1c1155d7b2..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 @@ -46,9 +46,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, + 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 2bf861a970..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,21 +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_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(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 } @@ -56,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 } 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 4bb0fc27d5..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 @@ -28,8 +28,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_CANONICAL_ALIAS, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, From d342356f29ea58300aebb815b1438663cc8e7e9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:48:30 +0100 Subject: [PATCH 03/20] Add missing state events to the list (not sure about the side effects) --- .../vector/matrix/android/api/session/events/model/EventType.kt | 2 ++ 1 file changed, 2 insertions(+) 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 bca2ded8bc..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 @@ -86,7 +86,9 @@ object EventType { STATE_ROOM_JOIN_RULES, STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, + STATE_ROOM_ALIASES, STATE_ROOM_TOMBSTONE, + STATE_ROOM_CANONICAL_ALIAS, STATE_ROOM_HISTORY_VISIBILITY, STATE_ROOM_RELATED_GROUPS, STATE_ROOM_PINNED_EVENT From c18be94986f9e77930a96cce0eccf26649af4185 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 20:03:10 +0100 Subject: [PATCH 04/20] Fix lots of trouble with the completion popup (resize, change mode, etc.) --- .../autocomplete/EpoxyAutocompletePresenter.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 index 227f1b2f9c..0862fc059f 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -19,18 +19,18 @@ package im.vector.riotx.features.autocomplete import android.content.Context import android.database.DataSetObserver import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager 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 recyclerView: RecyclerView? = null + private var clicks: ClickProvider? = null private var observer: Observer? = null - override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) { + override fun registerClickProvider(provider: ClickProvider) { this.clicks = provider } @@ -39,8 +39,10 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre } override fun getView(): ViewGroup? { - recyclerView = EpoxyRecyclerView(context).apply { - setController(providesController()) + recyclerView = RecyclerView(context).apply { + layoutManager = LinearLayoutManager(context) + setHasFixedSize(false) + adapter = providesController().adapter observer?.let { adapter?.registerAdapterDataObserver(it) } @@ -52,6 +54,7 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre override fun onViewShown() {} override fun onViewHidden() { + recyclerView?.adapter = null recyclerView = null observer = null } From 237b22df5923376275c34018b4a0398e780be678 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 20:31:36 +0100 Subject: [PATCH 05/20] Fix lots of trouble with the completion popup (resize, change mode, etc.) - next step --- .../EpoxyAutocompletePresenter.kt | 94 ------------------- .../command/AutocompleteCommandPresenter.kt | 15 ++- .../user/AutocompleteUserPresenter.kt | 15 ++- 3 files changed, 20 insertions(+), 104 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt 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 0862fc059f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt +++ /dev/null @@ -1,94 +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.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyController -import com.otaliastudios.autocomplete.AutocompletePresenter - -abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { - - private var recyclerView: RecyclerView? = null - private var clicks: ClickProvider? = null - private var observer: Observer? = null - - override fun registerClickProvider(provider: ClickProvider) { - this.clicks = provider - } - - override fun registerDataSetObserver(observer: DataSetObserver) { - this.observer = Observer(observer) - } - - override fun getView(): ViewGroup? { - recyclerView = RecyclerView(context).apply { - layoutManager = LinearLayoutManager(context) - setHasFixedSize(false) - adapter = providesController().adapter - observer?.let { - adapter?.registerAdapterDataObserver(it) - } - itemAnimator = null - } - return recyclerView - } - - override fun onViewShown() {} - - override fun onViewHidden() { - recyclerView?.adapter = null - 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..2f076c4c53 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,26 @@ 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<*> { + 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/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt index 5c2d2c49c0..d60ea5db67 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,12 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, controller.listener = this } - override fun providesController(): EpoxyController { - return controller + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return controller.adapter + } + + override fun onItemClick(t: User) { + dispatchClick(t) } override fun onQuery(query: CharSequence?) { From 3a829bdfe85d75bffd59fe6a4a10fb9b1f4d1c96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:05:04 +0100 Subject: [PATCH 06/20] Fix command truncation --- vector/src/main/res/layout/item_autocomplete_command.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/item_autocomplete_command.xml b/vector/src/main/res/layout/item_autocomplete_command.xml index 0d858f0e02..4fe815fbed 100644 --- a/vector/src/main/res/layout/item_autocomplete_command.xml +++ b/vector/src/main/res/layout/item_autocomplete_command.xml @@ -41,7 +41,7 @@ android:layout_alignParentStart="true" android:layout_alignParentLeft="true" android:layout_gravity="center_vertical" - android:maxLines="1" + android:maxLines="2" android:textColor="?riotx_text_secondary" android:textSize="12sp" tools:text="@string/command_description_invite_user" /> From 92f43a591acac58455cbb0f62f1e1d654cc70ad5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:38:42 +0100 Subject: [PATCH 07/20] Autocompletion for room canonical alias --- .../matrix/android/api/util/MatrixItem.kt | 8 ++- ...eUserItem.kt => AutocompleteMatrixItem.kt} | 14 +++-- .../room/AutocompleteRoomController.kt | 49 +++++++++++++++ .../room/AutocompleteRoomPresenter.kt | 59 +++++++++++++++++++ .../user/AutocompleteUserController.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 58 ++++++++++++++++++ .../detail/composer/TextComposerAction.kt | 1 + .../detail/composer/TextComposerViewModel.kt | 38 ++++++++++-- .../detail/composer/TextComposerViewState.kt | 4 +- .../layout/item_autocomplete_matrix_item.xml | 51 ++++++++++++++++ .../res/layout/item_autocomplete_user.xml | 30 ---------- 11 files changed, 272 insertions(+), 43 deletions(-) rename vector/src/main/java/im/vector/riotx/features/autocomplete/{user/AutocompleteUserItem.kt => AutocompleteMatrixItem.kt} (71%) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt create mode 100644 vector/src/main/res/layout/item_autocomplete_matrix_item.xml delete mode 100644 vector/src/main/res/layout/item_autocomplete_user.xml 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..1ca7237d3d 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, @@ -73,7 +76,7 @@ sealed class MatrixItem( } } - fun getBestName(): String { + open fun getBestName(): String { return displayName?.takeIf { it.isNotBlank() } ?: id } @@ -95,7 +98,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 +141,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/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/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..e2b4f38e19 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.room + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Success +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import javax.inject.Inject + +class AutocompleteRoomPresenter @Inject constructor(context: Context, + private val controller: AutocompleteRoomController +) : RecyclerViewPresenter(context), AutocompleteClickListener { + + var callback: Callback? = null + + init { + controller.listener = this + } + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return controller.adapter + } + + override fun onItemClick(t: RoomSummary) { + dispatchClick(t) + } + + override fun onQuery(query: CharSequence?) { + callback?.onQueryRooms(query) + } + + fun render(rooms: Async>) { + 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/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 49f23f7f2c..0d20ce851e 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 @@ -60,6 +60,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event 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 +69,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.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 +85,7 @@ 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.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 +143,7 @@ class RoomDetailFragment @Inject constructor( private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -150,6 +154,7 @@ class RoomDetailFragment @Inject constructor( VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -582,6 +587,52 @@ 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() + autocompleteUserPresenter.callback = this Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) @@ -724,6 +775,7 @@ class RoomDetailFragment @Inject constructor( private fun renderTextComposerState(state: TextComposerViewState) { autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) } private fun renderTombstoneEventHandling(async: Async) { @@ -1056,6 +1108,12 @@ class RoomDetailFragment @Inject constructor( textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) } + // AutocompleteRoomPresenter.Callback + + override fun onQueryRooms(query: CharSequence?) { + textComposerViewModel.handle(TextComposerAction.QueryRooms(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..9f94b086a2 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,5 @@ 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() } 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..c69ab7c5da 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,7 @@ 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.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 +33,16 @@ 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>() @AssistedInject.Factory interface Factory { @@ -59,11 +60,13 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: init { observeUsersQuery() + observeRoomsQuery() } override fun handle(action: TextComposerAction) { when (action) { is TextComposerAction.QueryUsers -> handleQueryUsers(action) + is TextComposerAction.QueryRooms -> handleQueryRooms(action) } } @@ -72,8 +75,14 @@ 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 observeUsersQuery() { - Observable.combineLatest, Option, List>( + Observable.combineLatest, Option, List>( room.rx().liveRoomMemberIds(), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { roomMemberIds, query -> @@ -87,6 +96,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false } } + .sortedBy { it.displayName } } ).execute { async -> copy( @@ -94,4 +104,24 @@ 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 + ) + } + } } 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..671a7ac556 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,13 @@ 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.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 ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId) 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..9696a08bb5 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ 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 From c31b64771bfa1692b02b558eaea99667e4bd7da3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:42:19 +0100 Subject: [PATCH 08/20] Autocompletion: disable animation on the recycler view items --- .../autocomplete/command/AutocompleteCommandPresenter.kt | 2 ++ .../features/autocomplete/room/AutocompleteRoomPresenter.kt | 2 ++ .../features/autocomplete/user/AutocompleteUserPresenter.kt | 2 ++ 3 files changed, 6 insertions(+) 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 2f076c4c53..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 @@ -32,6 +32,8 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } 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 index e2b4f38e19..53fed7f859 100644 --- 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 @@ -36,6 +36,8 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } 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 d60ea5db67..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 @@ -36,6 +36,8 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } From 05a788453f44b84461586842cbdfc498698240ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:47:47 +0100 Subject: [PATCH 09/20] More generic name --- .../api/session/room/model/relation/RelationService.kt | 2 +- .../room/send/{UserMentionSpan.kt => MatrixItemSpan.kt} | 4 ++-- .../matrix/android/api/session/room/send/SendService.kt | 2 +- .../internal/session/room/send/pills/MentionLinkSpec.kt | 4 ++-- .../internal/session/room/send/pills/TextPillsUtils.kt | 6 +++--- .../java/im/vector/riotx/features/html/PillImageSpan.kt | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/{UserMentionSpan.kt => MatrixItemSpan.kt} (90%) 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/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..b512f27602 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,7 +16,7 @@ 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 @@ -49,7 +49,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 +65,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/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) From 543c07fd695adc295168618d4784011caa09b7da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 01:23:45 +0100 Subject: [PATCH 10/20] Render pills for room links --- .../android/api/session/room/RoomService.kt | 7 +++ .../session/room/DefaultRoomService.kt | 17 ++++++ .../riotx/features/html/MxLinkTagHandler.kt | 53 ++++++++++++------- 3 files changed, 59 insertions(+), 18 deletions(-) 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/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/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..7e5a82060a 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,42 @@ 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 -> { + 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) + } + } + is PermalinkData.GroupLink -> { + // TODO val group = sessionHolder.getSafeActiveSession()?.getGroup(permalinkData.groupId) + MatrixItem.RoomItem(permalinkData.groupId /* TODO Group display name and avatar */) + } + 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) From 8dce98c53827187d1bc13542b1609c652e50e2a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 02:15:48 +0100 Subject: [PATCH 11/20] Autocompletion: group (including pills for groups) --- .../android/api/session/group/GroupService.kt | 7 +++ .../matrix/android/api/util/MatrixItem.kt | 3 + .../session/group/DefaultGroupService.kt | 8 +++ .../session/room/send/pills/TextPillsUtils.kt | 2 - .../group/AutocompleteGroupPresenter.kt | 61 +++++++++++++++++++ .../group/AutocompleteRoomController.kt | 48 +++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 57 +++++++++++++++++ .../detail/composer/TextComposerAction.kt | 1 + .../detail/composer/TextComposerViewModel.kt | 32 +++++++++- .../detail/composer/TextComposerViewState.kt | 4 +- .../riotx/features/html/MxLinkTagHandler.kt | 4 +- 11 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt 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/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 1ca7237d3d..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 @@ -74,6 +74,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 } open fun getBestName(): String { 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/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index b512f27602..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 @@ -23,8 +23,6 @@ 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 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/group/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt new file mode 100644 index 0000000000..5d0d43d9ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.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/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 0d20ce851e..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,6 +59,7 @@ 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.* @@ -85,6 +86,7 @@ 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 @@ -144,6 +146,7 @@ class RoomDetailFragment @Inject constructor( 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, @@ -155,6 +158,7 @@ class RoomDetailFragment @Inject constructor( TimelineEventController.Callback, AutocompleteUserPresenter.Callback, AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -633,6 +637,52 @@ class RoomDetailFragment @Inject constructor( }) .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)) @@ -776,6 +826,7 @@ 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) { @@ -1114,6 +1165,12 @@ class RoomDetailFragment @Inject constructor( 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 9f94b086a2..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 @@ -21,4 +21,5 @@ 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 c69ab7c5da..9d6d9aab2e 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,7 @@ 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 @@ -43,6 +44,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: private val usersQueryObservable = BehaviorRelay.create>() private val roomsQueryObservable = BehaviorRelay.create>() + private val groupsQueryObservable = BehaviorRelay.create>() @AssistedInject.Factory interface Factory { @@ -61,12 +63,14 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: init { observeUsersQuery() observeRoomsQuery() + observeGroupsQuery() } override fun handle(action: TextComposerAction) { when (action) { - is TextComposerAction.QueryUsers -> handleQueryUsers(action) - is TextComposerAction.QueryRooms -> handleQueryRooms(action) + is TextComposerAction.QueryUsers -> handleQueryUsers(action) + is TextComposerAction.QueryRooms -> handleQueryRooms(action) + is TextComposerAction.QueryGroups -> handleQueryGroups(action) } } @@ -81,6 +85,11 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: 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>( room.rx().liveRoomMemberIds(), @@ -124,4 +133,23 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: ) } } + + private fun observeGroupsQuery() { + Observable.combineLatest, Option, List>( + session.rx().liveGroupSummaries(), + groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), + BiFunction { groupSummaries, query -> + val filter = query.orNull() ?: "" + 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 671a7ac556..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,13 +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 asyncRooms: 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/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index 7e5a82060a..ff85a4d2cb 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 @@ -54,8 +54,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, } } is PermalinkData.GroupLink -> { - // TODO val group = sessionHolder.getSafeActiveSession()?.getGroup(permalinkData.groupId) - MatrixItem.RoomItem(permalinkData.groupId /* TODO Group display name and avatar */) + val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) + MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) } else -> null } From c79b35b089bb8018acc2db569056bdbfe1bd9986 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:01:06 +0100 Subject: [PATCH 12/20] Autocomplete item layout --- vector/src/main/res/layout/item_autocomplete_command.xml | 9 ++++++--- .../main/res/layout/item_autocomplete_matrix_item.xml | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/vector/src/main/res/layout/item_autocomplete_command.xml b/vector/src/main/res/layout/item_autocomplete_command.xml index 4fe815fbed..ce2cd6c731 100644 --- a/vector/src/main/res/layout/item_autocomplete_command.xml +++ b/vector/src/main/res/layout/item_autocomplete_command.xml @@ -1,11 +1,14 @@ - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="8dp" + android:paddingTop="6dp" + android:paddingEnd="8dp" + android:paddingBottom="6dp"> @@ -18,8 +19,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:layout_marginStart="12dp" - android:layout_marginLeft="12dp" + android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" android:orientation="vertical"> Date: Fri, 20 Dec 2019 10:15:53 +0100 Subject: [PATCH 13/20] Remove extra blank line --- vector/src/main/res/layout/bottom_sheet_generic_list.xml | 1 - vector/src/main/res/layout/fragment_public_rooms.xml | 1 - vector/src/main/res/layout/fragment_room_directory_picker.xml | 1 - .../src/main/res/layout/item_timeline_event_code_block_stub.xml | 1 - .../main/res/layout/item_timeline_event_merged_header_stub.xml | 1 - vector/src/main/res/layout/vector_message_merge_avatar_list.xml | 1 - .../src/main/res/layout/vector_settings_address_preference.xml | 1 - .../res/layout/vector_settings_list_preference_with_warning.xml | 1 - vector/src/main/res/layout/vector_settings_round_avatar.xml | 1 - .../src/main/res/layout/vector_settings_spinner_preference.xml | 1 - vector/src/main/res/layout/view_attachment_type_selector.xml | 1 - vector/src/main/res/layout/view_read_marker.xml | 1 - 12 files changed, 12 deletions(-) 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 @@ - - - - - - - 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 @@ - - Date: Fri, 20 Dec 2019 10:16:17 +0100 Subject: [PATCH 14/20] Update changes.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 89c1942f39..ac4b27596e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Render aliases and canonical alias change in the timeline + - Fix autocompletion issues and add support for rooms and groups Other changes: - From c992d32afdfaf9c66d8815e2f2e264480eff1274 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:23:45 +0100 Subject: [PATCH 15/20] Improve algo --- .../detail/composer/TextComposerViewModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 9d6d9aab2e..538d13bb4e 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 @@ -102,7 +102,7 @@ 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 } @@ -139,11 +139,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: session.rx().liveGroupSummaries(), groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { groupSummaries, query -> - val filter = query.orNull() ?: "" - groupSummaries - .filter { - it.groupId.contains(filter, ignoreCase = true) - } + val filter = query.orNull() + if (filter.isNullOrBlank()) { + groupSummaries + } else { + groupSummaries + .filter { + it.groupId.contains(filter, ignoreCase = true) + } + } .sortedBy { it.displayName } } ).execute { async -> From 3b0624ea401d6a4b1878804f78dd0f7a8298ca9b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:54:48 +0100 Subject: [PATCH 16/20] Fix issue with "in reply to" link --- .../vector/riotx/features/html/MxLinkTagHandler.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 ff85a4d2cb..1167427a04 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 @@ -46,11 +46,16 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } is PermalinkData.RoomLink -> { - val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) - if (permalinkData.isRoomAlias) { - MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + // Exclude event link (used in reply event) + 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 { - MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + null } } is PermalinkData.GroupLink -> { From 3ee5a7f54d48af4a23d5b7fedd8b69e67b8086b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:55:08 +0100 Subject: [PATCH 17/20] Better code --- .../android/api/permalinks/PermalinkParser.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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) } From 54f2ac0d8cea552e71b4691a0089c37e9772d9b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:59:41 +0100 Subject: [PATCH 18/20] Better comment --- .../main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1167427a04..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 @@ -46,7 +46,6 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } is PermalinkData.RoomLink -> { - // Exclude event link (used in reply event) if (permalinkData.eventId == null) { val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) if (permalinkData.isRoomAlias) { @@ -55,6 +54,7 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, 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 } } From 0ccb975d43dd4bab43140d86d329f4385bc976e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 11:04:06 +0100 Subject: [PATCH 19/20] Disable MatrixLinkify --- .../matrix/android/api/permalinks/MatrixLinkify.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 } } From 3cc65b1e71e7a84e569b6978395f5ad43a4cf444 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 11:05:54 +0100 Subject: [PATCH 20/20] ktlint --- ...ocompleteRoomController.kt => AutocompleteGroupController.kt} | 0 .../features/home/room/detail/composer/TextComposerViewModel.kt | 1 - 2 files changed, 1 deletion(-) rename vector/src/main/java/im/vector/riotx/features/autocomplete/group/{AutocompleteRoomController.kt => AutocompleteGroupController.kt} (100%) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt similarity index 100% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt 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 538d13bb4e..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 @@ -79,7 +79,6 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: usersQueryObservable.accept(query) } - private fun handleQueryRooms(action: TextComposerAction.QueryRooms) { val query = Option.fromNullable(action.query) roomsQueryObservable.accept(query)