diff --git a/changelog.d/5123.feature b/changelog.d/5123.feature
new file mode 100644
index 0000000000..cb1a7adf08
--- /dev/null
+++ b/changelog.d/5123.feature
@@ -0,0 +1 @@
+Add completion for @room to notify everyone in a room
diff --git a/library/ui-styles/src/main/res/values/text_appearances.xml b/library/ui-styles/src/main/res/values/text_appearances.xml
index 4ad3fd493e..8e30dd00d6 100644
--- a/library/ui-styles/src/main/res/values/text_appearances.xml
+++ b/library/ui-styles/src/main/res/values/text_appearances.xml
@@ -59,6 +59,10 @@
- sans-serif-medium
+
+
-
\ No newline at end of file
+
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index 88268f0f86..76885d8545 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -50,6 +50,9 @@ interface PushRuleService {
// fun fulfilledBingRule(event: Event, rules: List): PushRule?
+ fun resolveSenderNotificationPermissionCondition(event: Event,
+ condition: SenderNotificationPermissionCondition): Boolean
+
interface PushRuleListener {
fun onEvents(pushEvents: PushEvents)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 3396c4a6c9..302f7387fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -35,7 +35,19 @@ sealed class MatrixItem(
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
+ MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+
+ override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
+ }
+
+ data class EveryoneInRoomItem(override val id: String,
+ override val displayName: String = NOTIFY_EVERYONE,
+ override val avatarUrl: String? = null,
+ val roomDisplayName: String? = null) :
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -46,7 +58,7 @@ sealed class MatrixItem(
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -57,7 +69,7 @@ sealed class MatrixItem(
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -68,7 +80,7 @@ sealed class MatrixItem(
data class SpaceItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -79,7 +91,7 @@ sealed class MatrixItem(
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -90,7 +102,7 @@ sealed class MatrixItem(
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -109,16 +121,22 @@ sealed class MatrixItem(
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
- fun getIdPrefix() = when (this) {
- is UserItem -> '@'
- is EventItem -> '$'
+ private fun getIdPrefix() = when (this) {
+ is UserItem -> '@'
+ is EventItem -> '$'
is SpaceItem,
- is RoomItem -> '!'
- is RoomAliasItem -> '#'
- is GroupItem -> '+'
+ is RoomItem,
+ is EveryoneInRoomItem -> '!'
+ is RoomAliasItem -> '#'
+ is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
+ val displayName = when (this) {
+ // use the room display name for the notify everyone item
+ is EveryoneInRoomItem -> roomDisplayName
+ else -> displayName
+ }
return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn ->
var startIndex = 0
@@ -151,7 +169,8 @@ sealed class MatrixItem(
}
companion object {
- private const val ircPattern = " (IRC)"
+ private const val IRC_PATTERN = " (IRC)"
+ const val NOTIFY_EVERYONE = "@room"
}
}
@@ -171,6 +190,8 @@ fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
+fun RoomSummary.toEveryoneInRoomMatrixItem() = MatrixItem.EveryoneInRoomItem(id = roomId, avatarUrl = avatarUrl, roomDisplayName = displayName)
+
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 3e821b8956..cdc7350f8b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action
+import org.matrix.android.sdk.api.pushrules.ConditionResolver
import org.matrix.android.sdk.api.pushrules.PushEvents
import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleScope
import org.matrix.android.sdk.api.pushrules.RuleSetKey
+import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.pushrules.getActions
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
@@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
private val removePushRuleTask: RemovePushRuleTask,
private val pushRuleFinder: PushRuleFinder,
private val taskExecutor: TaskExecutor,
+ private val conditionResolver: ConditionResolver,
@SessionDatabase private val monarchy: Monarchy
) : PushRuleService {
@@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
}
+ override fun resolveSenderNotificationPermissionCondition(event: Event, condition: SenderNotificationPermissionCondition): Boolean {
+ return conditionResolver.resolveSenderNotificationPermissionCondition(event, condition)
+ }
+
override fun getKeywords(): LiveData> {
// Keywords are all content rules that don't start with '.'
val liveData = monarchy.findAllMappedWithChanges(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index 33cb0db243..ccbfbfcded 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
+import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
import java.util.Collections
import javax.inject.Inject
@@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
val pills = spannableString
?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
+ // we use the raw text for @room notification instead of a link
+ ?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
?: return null
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt
new file mode 100644
index 0000000000..f287104415
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.app.features.autocomplete
+
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+
+@EpoxyModelClass(layout = R.layout.item_autocomplete_header_item)
+abstract class AutocompleteHeaderItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute var title: String? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.titleView.text = title
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val titleView by bind(R.id.headerItemAutocompleteTitle)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
index 9b4bd78504..2034cee90a 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
@@ -16,31 +16,81 @@
package im.vector.app.features.autocomplete.member
+import android.content.Context
import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
+import im.vector.app.features.autocomplete.autocompleteHeaderItem
import im.vector.app.features.autocomplete.autocompleteMatrixItem
import im.vector.app.features.home.AvatarRenderer
-import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
-class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteMemberController @Inject constructor(private val context: Context) :
+ TypedEpoxyController>() {
- var listener: AutocompleteClickListener? = null
+ /* ==========================================================================================
+ * Fields
+ * ========================================================================================== */
+
+ var listener: AutocompleteClickListener? = null
+
+ /* ==========================================================================================
+ * Dependencies
+ * ========================================================================================== */
@Inject lateinit var avatarRenderer: AvatarRenderer
- override fun buildModels(data: List?) {
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
+ override fun buildModels(data: List?) {
if (data.isNullOrEmpty()) {
return
}
+ data.forEach { item ->
+ when (item) {
+ is AutocompleteMemberItem.Header -> buildHeaderItem(item)
+ is AutocompleteMemberItem.RoomMember -> buildRoomMemberItem(item)
+ is AutocompleteMemberItem.Everyone -> buildEveryoneItem(item)
+ }
+ }
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun buildHeaderItem(header: AutocompleteMemberItem.Header) {
+ autocompleteHeaderItem {
+ id(header.id)
+ title(header.title)
+ }
+ }
+
+ private fun buildRoomMemberItem(roomMember: AutocompleteMemberItem.RoomMember) {
val host = this
- data.forEach { user ->
- autocompleteMatrixItem {
+ autocompleteMatrixItem {
+ roomMember.roomMemberSummary.let { user ->
id(user.userId)
matrixItem(user.toMatrixItem())
avatarRenderer(host.avatarRenderer)
- clickListener { host.listener?.onItemClick(user) }
+ clickListener { host.listener?.onItemClick(roomMember) }
+ }
+ }
+ }
+
+ private fun buildEveryoneItem(everyone: AutocompleteMemberItem.Everyone) {
+ val host = this
+ autocompleteMatrixItem {
+ everyone.roomSummary.let { room ->
+ id(room.roomId)
+ matrixItem(room.toEveryoneInRoomMatrixItem())
+ subName(host.context.getString(R.string.room_message_notify_everyone))
+ avatarRenderer(host.avatarRenderer)
+ clickListener { host.listener?.onItemClick(everyone) }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt
new file mode 100644
index 0000000000..77c5069938
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 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.app.features.autocomplete.member
+
+import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+sealed class AutocompleteMemberItem {
+ data class Header(val id: String, val title: String) : AutocompleteMemberItem()
+ data class RoomMember(val roomMemberSummary: RoomMemberSummary) : AutocompleteMemberItem()
+ data class Everyone(val roomSummary: RoomSummary) : AutocompleteMemberItem()
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
index 4976cb39b9..ce3b9c6a7e 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
@@ -21,26 +21,44 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter
+import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.util.MatrixItem
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String,
- session: Session,
+ private val session: Session,
private val controller: AutocompleteMemberController
-) : RecyclerViewPresenter(context), AutocompleteClickListener {
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+ /* ==========================================================================================
+ * Fields
+ * ========================================================================================== */
private val room by lazy { session.getRoom(roomId)!! }
+ /* ==========================================================================================
+ * Init
+ * ========================================================================================== */
+
init {
controller.listener = this
}
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
+
fun clear() {
controller.listener = null
}
@@ -50,29 +68,100 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
fun create(roomId: String): AutocompleteMemberPresenter
}
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller.adapter
}
- override fun onItemClick(t: RoomMemberSummary) {
+ override fun onItemClick(t: AutocompleteMemberItem) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
- val queryParams = roomMemberQueryParams {
- displayName = if (query.isNullOrBlank()) {
- QueryStringValue.IsNotEmpty
- } else {
- QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+ val queryParams = createQueryParams(query)
+ val membersHeader = createMembersHeader()
+ val members = createMemberItems(queryParams)
+ val everyone = createEveryoneItem(query)
+ // add headers only when user can notify everyone
+ val canAddHeaders = canNotifyEveryone()
+
+ val items = mutableListOf().apply {
+ if (members.isNotEmpty()) {
+ if (canAddHeaders) {
+ add(membersHeader)
+ }
+ addAll(members)
+ }
+ everyone?.let {
+ val everyoneHeader = createEveryoneHeader()
+ add(everyoneHeader)
+ add(it)
}
- memberships = listOf(Membership.JOIN)
- excludeSelf = true
}
- val members = room.getRoomMembers(queryParams)
- .asSequence()
- .sortedBy { it.displayName }
- .disambiguate()
- controller.setData(members.toList())
+
+ controller.setData(items)
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
+ displayName = if (query.isNullOrBlank()) {
+ QueryStringValue.IsNotEmpty
+ } else {
+ QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+ }
+ memberships = listOf(Membership.JOIN)
+ excludeSelf = true
+ }
+
+ private fun createMembersHeader() =
+ AutocompleteMemberItem.Header(
+ ID_HEADER_MEMBERS,
+ context.getString(R.string.room_message_autocomplete_users)
+ )
+
+ private fun createMemberItems(queryParams: RoomMemberQueryParams) =
+ room.getRoomMembers(queryParams)
+ .asSequence()
+ .sortedBy { it.displayName }
+ .disambiguate()
+ .map { AutocompleteMemberItem.RoomMember(it) }
+ .toList()
+
+ private fun createEveryoneHeader() =
+ AutocompleteMemberItem.Header(
+ ID_HEADER_EVERYONE,
+ context.getString(R.string.room_message_autocomplete_notification)
+ )
+
+ private fun createEveryoneItem(query: CharSequence?) =
+ room.roomSummary()
+ ?.takeIf { canNotifyEveryone() }
+ ?.takeIf { query.isNullOrBlank() || MatrixItem.NOTIFY_EVERYONE.startsWith("@$query") }
+ ?.let {
+ AutocompleteMemberItem.Everyone(it)
+ }
+
+ private fun canNotifyEveryone() = session.resolveSenderNotificationPermissionCondition(
+ Event(
+ senderId = session.myUserId,
+ roomId = roomId
+ ),
+ SenderNotificationPermissionCondition(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
+ )
+
+ /* ==========================================================================================
+ * Const
+ * ========================================================================================== */
+
+ companion object {
+ private const val ID_HEADER_MEMBERS = "ID_HEADER_MEMBERS"
+ private const val ID_HEADER_EVERYONE = "ID_HEADER_EVERYONE"
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
index 9f85d4015b..be5f9c0bb4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
@@ -33,6 +33,7 @@ import im.vector.app.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.app.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter
+import im.vector.app.features.autocomplete.member.AutocompleteMemberItem
import im.vector.app.features.autocomplete.member.AutocompleteMemberPresenter
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.app.features.command.Command
@@ -41,9 +42,9 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.group.model.GroupSummary
-import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
@@ -106,7 +107,7 @@ class AutoCompleter @AssistedInject constructor(
Autocomplete.on(editText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
@@ -125,15 +126,24 @@ class AutoCompleter @AssistedInject constructor(
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
- Autocomplete.on(editText)
- .with(CharPolicy('@', true))
+ Autocomplete.on(editText)
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
.with(autocompleteMemberPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
- .with(object : AutocompleteCallback {
- override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean {
- insertMatrixItem(editText, editable, "@", item.toMatrixItem())
- return true
+ .with(object : AutocompleteCallback {
+ override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
+ return when (item) {
+ is AutocompleteMemberItem.Header -> false // do nothing header is not clickable
+ is AutocompleteMemberItem.RoomMember -> {
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomMemberSummary.toMatrixItem())
+ true
+ }
+ is AutocompleteMemberItem.Everyone -> {
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomSummary.toEveryoneInRoomMatrixItem())
+ true
+ }
+ }
}
override fun onPopupVisibilityChanged(shown: Boolean) {
@@ -144,13 +154,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy('#', true))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
.with(autocompleteRoomPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
- insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_ROOMS, item.toRoomAliasMatrixItem())
return true
}
@@ -162,13 +172,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy('+', true))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
.with(autocompleteGroupPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
- insertMatrixItem(editText, editable, "+", item.toMatrixItem())
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_GROUPS, item.toMatrixItem())
return true
}
@@ -180,9 +190,9 @@ class AutoCompleter @AssistedInject constructor(
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy(':', false))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
.with(autocompleteEmojiPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
@@ -210,7 +220,7 @@ class AutoCompleter @AssistedInject constructor(
.build()
}
- private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
+ private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) {
// Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) {
@@ -228,7 +238,7 @@ class AutoCompleter @AssistedInject constructor(
// Adding trailing space " " or ": " if the user started mention someone
val displayNameSuffix =
- if (firstChar == "@" && startIndex == 0) {
+ if (matrixItem is MatrixItem.UserItem) {
": "
} else {
" "
@@ -249,6 +259,10 @@ class AutoCompleter @AssistedInject constructor(
}
companion object {
- private const val ELEVATION = 6f
+ private const val ELEVATION_DP = 6f
+ private const val TRIGGER_AUTO_COMPLETE_MEMBERS = '@'
+ private const val TRIGGER_AUTO_COMPLETE_ROOMS = '#'
+ private const val TRIGGER_AUTO_COMPLETE_GROUPS = '+'
+ private const val TRIGGER_AUTO_COMPLETE_EMOJIS = ':'
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 0c836748c8..aa1758dd6c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -61,6 +61,7 @@ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
+import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.EventHtmlRenderer
@@ -112,6 +113,7 @@ class MessageItemFactory @Inject constructor(
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy,
private val htmlCompressor: VectorHtmlCompressor,
+ private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
@@ -138,6 +140,10 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId)
}
+ private val textRenderer by lazy {
+ textRendererFactory.create(roomId)
+ }
+
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
@@ -549,8 +555,9 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
- val bindingOptions = spanUtils.getBindingOptions(body)
- val linkifiedBody = body.linkify(callback)
+ val renderedBody = textRenderer.render(body)
+ val bindingOptions = spanUtils.getBindingOptions(renderedBody)
+ val linkifiedBody = renderedBody.linkify(callback)
return MessageTextItem_()
.message(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt
new file mode 100644
index 0000000000..d50a6fb297
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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.app.features.home.room.detail.timeline.render
+
+import android.content.Context
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.html.PillImageSpan
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.util.MatrixItem
+
+class EventTextRenderer @AssistedInject constructor(@Assisted private val roomId: String?,
+ private val context: Context,
+ private val avatarRenderer: AvatarRenderer,
+ private val sessionHolder: ActiveSessionHolder) {
+
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
+
+ @AssistedFactory
+ interface Factory {
+ fun create(roomId: String?): EventTextRenderer
+ }
+
+ /**
+ * @param text the text you want to render
+ */
+ fun render(text: CharSequence): CharSequence {
+ return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) {
+ SpannableStringBuilder(text).apply {
+ addNotifyEveryoneSpans(this, roomId)
+ }
+ } else {
+ text
+ }
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
+ val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomId)
+ val matrixItem = MatrixItem.EveryoneInRoomItem(
+ id = roomId,
+ avatarUrl = room?.avatarUrl,
+ roomDisplayName = room?.displayName
+ )
+
+ // search for notify everyone text
+ var foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, 0)
+ while (foundIndex >= 0) {
+ val endSpan = foundIndex + MatrixItem.NOTIFY_EVERYONE.length
+ addPillSpan(text, createPillImageSpan(matrixItem), foundIndex, endSpan)
+ foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, endSpan)
+ }
+ }
+
+ private fun createPillImageSpan(matrixItem: MatrixItem) =
+ PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+
+ private fun addPillSpan(
+ renderedText: Spannable,
+ pillSpan: PillImageSpan,
+ startSpan: Int,
+ endSpan: Int
+ ) {
+ renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
index ff2e2a9cdb..ae285b074c 100644
--- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
+++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
@@ -19,6 +19,7 @@
package im.vector.app.features.html
import android.content.Context
+import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
@@ -32,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference
@@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = icon
+ if (matrixItem is MatrixItem.EveryoneInRoomItem) {
+ chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError))
+ // setTextColor API does not exist right now for ChipDrawable, use textAppearance
+ setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError)
+ }
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
index f8a2ee5137..506f5e773c 100644
--- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
+++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
@@ -36,57 +36,87 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) :
- EventHtmlRenderer.PostProcessor {
+ EventHtmlRenderer.PostProcessor {
+
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
@AssistedFactory
interface Factory {
fun create(roomId: String?): PillsPostProcessor
}
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId)
}
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
+ addLinkSpans(renderedText, roomId)
+ }
+
+ private fun addPillSpan(
+ renderedText: Spannable,
+ pillSpan: PillImageSpan,
+ startSpan: Int,
+ endSpan: Int
+ ) {
+ renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ private fun addLinkSpans(renderedText: Spannable, roomId: String?) {
// We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan)
- renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ addPillSpan(renderedText, pillSpan, startSpan, endSpan)
}
}
+ private fun createPillImageSpan(matrixItem: MatrixItem) =
+ PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
- val permalinkData = PermalinkParser.parse(url)
- val matrixItem = when (permalinkData) {
- is PermalinkData.UserLink -> {
- if (roomId == null) {
- sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
- } else {
- sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
- }
- }
- is PermalinkData.RoomLink -> {
- if (permalinkData.eventId == null) {
- val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
- if (permalinkData.isRoomAlias) {
- MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
- } else {
- MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
- }
- } else {
- // Exclude event link (used in reply events, we do not want to pill the "in reply to")
- null
- }
- }
- is PermalinkData.GroupLink -> {
- val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
- MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
- }
+ val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
+ is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
+ is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
+ is PermalinkData.GroupLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
- return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+ return createPillImageSpan(matrixItem)
+ }
+
+ private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
+ if (roomId == null) {
+ sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
+ } else {
+ sessionHolder.getSafeActiveSession()?.getRoomMember(userId, roomId)?.toMatrixItem()
+ }
+
+ private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
+ if (eventId == null) {
+ val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
+ when {
+ isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
+ else -> MatrixItem.RoomItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
+ }
+ } else {
+ // Exclude event link (used in reply events, we do not want to pill the "in reply to")
+ null
+ }
+
+ private fun PermalinkData.GroupLink.toMatrixItem(): MatrixItem? {
+ val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(groupId)
+ return MatrixItem.GroupItem(groupId, group?.displayName, group?.avatarUrl)
}
}
diff --git a/vector/src/main/res/layout/item_autocomplete_header_item.xml b/vector/src/main/res/layout/item_autocomplete_header_item.xml
new file mode 100644
index 0000000000..f842129e0c
--- /dev/null
+++ b/vector/src/main/res/layout/item_autocomplete_header_item.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index c155b6bb75..69a1c53d1c 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -3782,4 +3782,7 @@
Show less
"%1$d more"
+ Notify the whole room
+ Users
+ Room notification