Merge pull request #5237 from vector-im/feature/mna/5123-room-tag-suggestion
#5123: @room tag suggestion
This commit is contained in:
commit
d1d26a98af
|
@ -0,0 +1 @@
|
||||||
|
Add completion for @room to notify everyone in a room
|
|
@ -59,6 +59,10 @@
|
||||||
<item name="android:fontFamily">sans-serif-medium</item>
|
<item name="android:fontFamily">sans-serif-medium</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Vector.Body.OnError">
|
||||||
|
<item name="android:textColor">?colorOnError</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
|
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
|
||||||
<item name="fontFamily">sans-serif</item>
|
<item name="fontFamily">sans-serif</item>
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
<item name="android:fontFamily">sans-serif</item>
|
||||||
|
|
|
@ -50,6 +50,9 @@ interface PushRuleService {
|
||||||
|
|
||||||
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
||||||
|
|
||||||
|
fun resolveSenderNotificationPermissionCondition(event: Event,
|
||||||
|
condition: SenderNotificationPermissionCondition): Boolean
|
||||||
|
|
||||||
interface PushRuleListener {
|
interface PushRuleListener {
|
||||||
fun onEvents(pushEvents: PushEvents)
|
fun onEvents(pushEvents: PushEvents)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,19 @@ sealed class MatrixItem(
|
||||||
data class UserItem(override val id: String,
|
data class UserItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: 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 {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
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)
|
* Return the prefix as defined in the matrix spec (and not extracted from the id)
|
||||||
*/
|
*/
|
||||||
fun getIdPrefix() = when (this) {
|
private fun getIdPrefix() = when (this) {
|
||||||
is UserItem -> '@'
|
is UserItem -> '@'
|
||||||
is EventItem -> '$'
|
is EventItem -> '$'
|
||||||
is SpaceItem,
|
is SpaceItem,
|
||||||
is RoomItem -> '!'
|
is RoomItem,
|
||||||
|
is EveryoneInRoomItem -> '!'
|
||||||
is RoomAliasItem -> '#'
|
is RoomAliasItem -> '#'
|
||||||
is GroupItem -> '+'
|
is GroupItem -> '+'
|
||||||
}
|
}
|
||||||
|
|
||||||
fun firstLetterOfDisplayName(): String {
|
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)
|
return (displayName?.takeIf { it.isNotBlank() } ?: id)
|
||||||
.let { dn ->
|
.let { dn ->
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
|
@ -151,7 +169,8 @@ sealed class MatrixItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.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
|
// If no name is available, use room alias as Riot-Web does
|
||||||
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
|
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.pushrules.Action
|
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.PushEvents
|
||||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
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.getActions
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
||||||
|
@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||||
private val removePushRuleTask: RemovePushRuleTask,
|
private val removePushRuleTask: RemovePushRuleTask,
|
||||||
private val pushRuleFinder: PushRuleFinder,
|
private val pushRuleFinder: PushRuleFinder,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val conditionResolver: ConditionResolver,
|
||||||
@SessionDatabase private val monarchy: Monarchy
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
) : PushRuleService {
|
) : PushRuleService {
|
||||||
|
|
||||||
|
@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||||
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun resolveSenderNotificationPermissionCondition(event: Event, condition: SenderNotificationPermissionCondition): Boolean {
|
||||||
|
return conditionResolver.resolveSenderNotificationPermissionCondition(event, condition)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getKeywords(): LiveData<Set<String>> {
|
override fun getKeywords(): LiveData<Set<String>> {
|
||||||
// Keywords are all content rules that don't start with '.'
|
// Keywords are all content rules that don't start with '.'
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
|
|
@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
|
||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
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 org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
|
||||||
val pills = spannableString
|
val pills = spannableString
|
||||||
?.getSpans(0, text.length, MatrixItemSpan::class.java)
|
?.getSpans(0, text.length, MatrixItemSpan::class.java)
|
||||||
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
|
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
|
||||||
|
// we use the raw text for @room notification instead of a link
|
||||||
|
?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
|
||||||
?.toMutableList()
|
?.toMutableList()
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
|
@ -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<AutocompleteHeaderItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute var title: String? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.titleView.text = title
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val titleView by bind<TextView>(R.id.headerItemAutocompleteTitle)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,31 +16,81 @@
|
||||||
|
|
||||||
package im.vector.app.features.autocomplete.member
|
package im.vector.app.features.autocomplete.member
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
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.autocomplete.autocompleteMatrixItem
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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 org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AutocompleteMemberController @Inject constructor() : TypedEpoxyController<List<RoomMemberSummary>>() {
|
class AutocompleteMemberController @Inject constructor(private val context: Context) :
|
||||||
|
TypedEpoxyController<List<AutocompleteMemberItem>>() {
|
||||||
|
|
||||||
var listener: AutocompleteClickListener<RoomMemberSummary>? = null
|
/* ==========================================================================================
|
||||||
|
* Fields
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
var listener: AutocompleteClickListener<AutocompleteMemberItem>? = null
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Dependencies
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
override fun buildModels(data: List<RoomMemberSummary>?) {
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun buildModels(data: List<AutocompleteMemberItem>?) {
|
||||||
if (data.isNullOrEmpty()) {
|
if (data.isNullOrEmpty()) {
|
||||||
return
|
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
|
val host = this
|
||||||
data.forEach { user ->
|
|
||||||
autocompleteMatrixItem {
|
autocompleteMatrixItem {
|
||||||
|
roomMember.roomMemberSummary.let { user ->
|
||||||
id(user.userId)
|
id(user.userId)
|
||||||
matrixItem(user.toMatrixItem())
|
matrixItem(user.toMatrixItem())
|
||||||
avatarRenderer(host.avatarRenderer)
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -21,26 +21,44 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
||||||
import im.vector.app.features.autocomplete.RecyclerViewPresenter
|
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.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.members.roomMemberQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
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.session.room.model.RoomMemberSummary
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
||||||
@Assisted val roomId: String,
|
@Assisted val roomId: String,
|
||||||
session: Session,
|
private val session: Session,
|
||||||
private val controller: AutocompleteMemberController
|
private val controller: AutocompleteMemberController
|
||||||
) : RecyclerViewPresenter<RoomMemberSummary>(context), AutocompleteClickListener<RoomMemberSummary> {
|
) : RecyclerViewPresenter<AutocompleteMemberItem>(context), AutocompleteClickListener<AutocompleteMemberItem> {
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Fields
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
private val room by lazy { session.getRoom(roomId)!! }
|
private val room by lazy { session.getRoom(roomId)!! }
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Init
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
init {
|
init {
|
||||||
controller.listener = this
|
controller.listener = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Public api
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
controller.listener = null
|
controller.listener = null
|
||||||
}
|
}
|
||||||
|
@ -50,16 +68,48 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
||||||
fun create(roomId: String): AutocompleteMemberPresenter
|
fun create(roomId: String): AutocompleteMemberPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
||||||
return controller.adapter
|
return controller.adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(t: RoomMemberSummary) {
|
override fun onItemClick(t: AutocompleteMemberItem) {
|
||||||
dispatchClick(t)
|
dispatchClick(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQuery(query: CharSequence?) {
|
override fun onQuery(query: CharSequence?) {
|
||||||
val queryParams = roomMemberQueryParams {
|
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<AutocompleteMemberItem>().apply {
|
||||||
|
if (members.isNotEmpty()) {
|
||||||
|
if (canAddHeaders) {
|
||||||
|
add(membersHeader)
|
||||||
|
}
|
||||||
|
addAll(members)
|
||||||
|
}
|
||||||
|
everyone?.let {
|
||||||
|
val everyoneHeader = createEveryoneHeader()
|
||||||
|
add(everyoneHeader)
|
||||||
|
add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.setData(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
|
||||||
displayName = if (query.isNullOrBlank()) {
|
displayName = if (query.isNullOrBlank()) {
|
||||||
QueryStringValue.IsNotEmpty
|
QueryStringValue.IsNotEmpty
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,11 +118,50 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
||||||
memberships = listOf(Membership.JOIN)
|
memberships = listOf(Membership.JOIN)
|
||||||
excludeSelf = true
|
excludeSelf = true
|
||||||
}
|
}
|
||||||
val members = room.getRoomMembers(queryParams)
|
|
||||||
|
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()
|
.asSequence()
|
||||||
.sortedBy { it.displayName }
|
.sortedBy { it.displayName }
|
||||||
.disambiguate()
|
.disambiguate()
|
||||||
controller.setData(members.toList())
|
.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.command.CommandAutocompletePolicy
|
||||||
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
|
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
|
||||||
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter
|
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.member.AutocompleteMemberPresenter
|
||||||
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
|
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
|
||||||
import im.vector.app.features.command.Command
|
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.html.PillImageSpan
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
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.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
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.toMatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
|
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
|
||||||
|
|
||||||
|
@ -106,7 +107,7 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
Autocomplete.on<Command>(editText)
|
Autocomplete.on<Command>(editText)
|
||||||
.with(commandAutocompletePolicy)
|
.with(commandAutocompletePolicy)
|
||||||
.with(autocompleteCommandPresenter)
|
.with(autocompleteCommandPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<Command> {
|
.with(object : AutocompleteCallback<Command> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
||||||
|
@ -125,15 +126,24 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
|
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
|
||||||
Autocomplete.on<RoomMemberSummary>(editText)
|
Autocomplete.on<AutocompleteMemberItem>(editText)
|
||||||
.with(CharPolicy('@', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
|
||||||
.with(autocompleteMemberPresenter)
|
.with(autocompleteMemberPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<RoomMemberSummary> {
|
.with(object : AutocompleteCallback<AutocompleteMemberItem> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
|
||||||
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
|
return when (item) {
|
||||||
return true
|
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) {
|
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||||
|
@ -144,13 +154,13 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
Autocomplete.on<RoomSummary>(editText)
|
Autocomplete.on<RoomSummary>(editText)
|
||||||
.with(CharPolicy('#', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
|
||||||
.with(autocompleteRoomPresenter)
|
.with(autocompleteRoomPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<RoomSummary> {
|
.with(object : AutocompleteCallback<RoomSummary> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
||||||
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_ROOMS, item.toRoomAliasMatrixItem())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,13 +172,13 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
Autocomplete.on<GroupSummary>(editText)
|
Autocomplete.on<GroupSummary>(editText)
|
||||||
.with(CharPolicy('+', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
|
||||||
.with(autocompleteGroupPresenter)
|
.with(autocompleteGroupPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<GroupSummary> {
|
.with(object : AutocompleteCallback<GroupSummary> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
||||||
insertMatrixItem(editText, editable, "+", item.toMatrixItem())
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_GROUPS, item.toMatrixItem())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,9 +190,9 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
|
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
|
||||||
Autocomplete.on<String>(editText)
|
Autocomplete.on<String>(editText)
|
||||||
.with(CharPolicy(':', false))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
|
||||||
.with(autocompleteEmojiPresenter)
|
.with(autocompleteEmojiPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<String> {
|
.with(object : AutocompleteCallback<String> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
|
||||||
|
@ -210,7 +220,7 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
.build()
|
.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
|
// Detect last firstChar and remove it
|
||||||
var startIndex = editable.lastIndexOf(firstChar)
|
var startIndex = editable.lastIndexOf(firstChar)
|
||||||
if (startIndex == -1) {
|
if (startIndex == -1) {
|
||||||
|
@ -228,7 +238,7 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
|
|
||||||
// Adding trailing space " " or ": " if the user started mention someone
|
// Adding trailing space " " or ": " if the user started mention someone
|
||||||
val displayNameSuffix =
|
val displayNameSuffix =
|
||||||
if (firstChar == "@" && startIndex == 0) {
|
if (matrixItem is MatrixItem.UserItem) {
|
||||||
": "
|
": "
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
|
@ -249,6 +259,10 @@ class AutoCompleter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 = ':'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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.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.createLinkMovementMethod
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
|
@ -112,6 +113,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
private val htmlRenderer: Lazy<EventHtmlRenderer>,
|
private val htmlRenderer: Lazy<EventHtmlRenderer>,
|
||||||
private val htmlCompressor: VectorHtmlCompressor,
|
private val htmlCompressor: VectorHtmlCompressor,
|
||||||
|
private val textRendererFactory: EventTextRenderer.Factory,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val imageContentRenderer: ImageContentRenderer,
|
private val imageContentRenderer: ImageContentRenderer,
|
||||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||||
|
@ -138,6 +140,10 @@ class MessageItemFactory @Inject constructor(
|
||||||
pillsPostProcessorFactory.create(roomId)
|
pillsPostProcessorFactory.create(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val textRenderer by lazy {
|
||||||
|
textRendererFactory.create(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||||
val event = params.event
|
val event = params.event
|
||||||
val highlight = params.isHighlighted
|
val highlight = params.isHighlighted
|
||||||
|
@ -549,8 +555,9 @@ class MessageItemFactory @Inject constructor(
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
val renderedBody = textRenderer.render(body)
|
||||||
val linkifiedBody = body.linkify(callback)
|
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
|
||||||
|
val linkifiedBody = renderedBody.linkify(callback)
|
||||||
|
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(
|
.message(
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
package im.vector.app.features.html
|
package im.vector.app.features.html
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.drawable.Drawable
|
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.core.glide.GlideRequests
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.session.room.send.MatrixItemSpan
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
||||||
setChipMinHeightResource(R.dimen.pill_min_height)
|
setChipMinHeightResource(R.dimen.pill_min_height)
|
||||||
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
||||||
chipIcon = icon
|
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)
|
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,55 +38,85 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI
|
||||||
private val sessionHolder: ActiveSessionHolder) :
|
private val sessionHolder: ActiveSessionHolder) :
|
||||||
EventHtmlRenderer.PostProcessor {
|
EventHtmlRenderer.PostProcessor {
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Public api
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(roomId: String?): PillsPostProcessor
|
fun create(roomId: String?): PillsPostProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
override fun afterRender(renderedText: Spannable) {
|
override fun afterRender(renderedText: Spannable) {
|
||||||
addPillSpans(renderedText, roomId)
|
addPillSpans(renderedText, roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
|
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.
|
// We let markdown handle links and then we add PillImageSpan if needed.
|
||||||
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
||||||
linkSpans.forEach { linkSpan ->
|
linkSpans.forEach { linkSpan ->
|
||||||
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
||||||
val startSpan = renderedText.getSpanStart(linkSpan)
|
val startSpan = renderedText.getSpanStart(linkSpan)
|
||||||
val endSpan = renderedText.getSpanEnd(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? {
|
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
|
||||||
val permalinkData = PermalinkParser.parse(url)
|
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
|
||||||
val matrixItem = when (permalinkData) {
|
is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
||||||
|
is PermalinkData.GroupLink -> permalinkData.toMatrixItem()
|
||||||
|
else -> null
|
||||||
|
} ?: return null
|
||||||
|
return createPillImageSpan(matrixItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
|
||||||
if (roomId == null) {
|
if (roomId == null) {
|
||||||
sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
|
sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
|
||||||
} else {
|
} else {
|
||||||
sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
|
sessionHolder.getSafeActiveSession()?.getRoomMember(userId, roomId)?.toMatrixItem()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
is PermalinkData.RoomLink -> {
|
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
|
||||||
if (permalinkData.eventId == null) {
|
if (eventId == null) {
|
||||||
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
|
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
|
||||||
if (permalinkData.isRoomAlias) {
|
when {
|
||||||
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||||
} else {
|
else -> MatrixItem.RoomItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||||
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
is PermalinkData.GroupLink -> {
|
private fun PermalinkData.GroupLink.toMatrixItem(): MatrixItem? {
|
||||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(groupId)
|
||||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
return MatrixItem.GroupItem(groupId, group?.displayName, group?.avatarUrl)
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
} ?: return null
|
|
||||||
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headerItemAutocompleteTitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
|
tools:text="Users" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -3782,4 +3782,7 @@
|
||||||
<string name="message_reaction_show_less">Show less</string>
|
<string name="message_reaction_show_less">Show less</string>
|
||||||
<string name="message_reaction_show_more">"%1$d more"</string>
|
<string name="message_reaction_show_more">"%1$d more"</string>
|
||||||
|
|
||||||
|
<string name="room_message_notify_everyone">Notify the whole room</string>
|
||||||
|
<string name="room_message_autocomplete_users">Users</string>
|
||||||
|
<string name="room_message_autocomplete_notification">Room notification</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue