Autocompletion: group (including pills for groups)
This commit is contained in:
parent
543c07fd69
commit
8dce98c538
|
@ -31,6 +31,13 @@ interface GroupService {
|
|||
*/
|
||||
fun getGroup(groupId: String): Group?
|
||||
|
||||
/**
|
||||
* Get a groupSummary from a groupId
|
||||
* @param groupId the groupId to look for.
|
||||
* @return the groupSummary with groupId or null
|
||||
*/
|
||||
fun getGroupSummary(groupId: String): GroupSummary?
|
||||
|
||||
/**
|
||||
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
|
||||
* @return the [LiveData] of [GroupSummary]
|
||||
|
|
|
@ -74,6 +74,9 @@ sealed class MatrixItem(
|
|||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
|
||||
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
|
||||
override fun getBestName() = id
|
||||
}
|
||||
|
||||
open fun getBestName(): String {
|
||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
|||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService {
|
||||
|
@ -33,6 +34,13 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon
|
|||
return null
|
||||
}
|
||||
|
||||
override fun getGroupSummary(groupId: String): GroupSummary? {
|
||||
return monarchy.fetchCopyMap(
|
||||
{ realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
|
||||
{ it, _ -> it.asDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) },
|
||||
|
|
|
@ -23,8 +23,6 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Utility class to detect special span in CharSequence and turn them into
|
||||
* formatted text to send them as a Matrix messages.
|
||||
*
|
||||
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
|
||||
*/
|
||||
internal class TextPillsUtils @Inject constructor(
|
||||
private val mentionLinkSpecComparator: MentionLinkSpecComparator
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.autocomplete.group
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.otaliastudios.autocomplete.RecyclerViewPresenter
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutocompleteGroupPresenter @Inject constructor(context: Context,
|
||||
private val controller: AutocompleteGroupController
|
||||
) : RecyclerViewPresenter<GroupSummary>(context), AutocompleteClickListener<GroupSummary> {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
controller.listener = this
|
||||
}
|
||||
|
||||
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
||||
// Also remove animation
|
||||
recyclerView?.itemAnimator = null
|
||||
return controller.adapter
|
||||
}
|
||||
|
||||
override fun onItemClick(t: GroupSummary) {
|
||||
dispatchClick(t)
|
||||
}
|
||||
|
||||
override fun onQuery(query: CharSequence?) {
|
||||
callback?.onQueryGroups(query)
|
||||
}
|
||||
|
||||
fun render(groups: Async<List<GroupSummary>>) {
|
||||
if (groups is Success) {
|
||||
controller.setData(groups())
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onQueryGroups(query: CharSequence?)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.autocomplete.group
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
|
||||
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutocompleteGroupController @Inject constructor() : TypedEpoxyController<List<GroupSummary>>() {
|
||||
|
||||
var listener: AutocompleteClickListener<GroupSummary>? = null
|
||||
|
||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
override fun buildModels(data: List<GroupSummary>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
data.forEach { groupSummary ->
|
||||
autocompleteMatrixItem {
|
||||
id(groupSummary.groupId)
|
||||
matrixItem(groupSummary.toMatrixItem())
|
||||
avatarRenderer(avatarRenderer)
|
||||
clickListener { _ ->
|
||||
listener?.onItemClick(groupSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
|||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
|
@ -85,6 +86,7 @@ import im.vector.riotx.features.attachments.AttachmentsHelper
|
|||
import im.vector.riotx.features.attachments.ContactAttachment
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
|
||||
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotx.features.command.Command
|
||||
|
@ -144,6 +146,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
|
||||
private val autocompleteUserPresenter: AutocompleteUserPresenter,
|
||||
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
|
||||
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
|
||||
private val permalinkHandler: PermalinkHandler,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||
|
@ -155,6 +158,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
TimelineEventController.Callback,
|
||||
AutocompleteUserPresenter.Callback,
|
||||
AutocompleteRoomPresenter.Callback,
|
||||
AutocompleteGroupPresenter.Callback,
|
||||
VectorInviteView.Callback,
|
||||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
|
@ -633,6 +637,52 @@ class RoomDetailFragment @Inject constructor(
|
|||
})
|
||||
.build()
|
||||
|
||||
autocompleteGroupPresenter.callback = this
|
||||
Autocomplete.on<GroupSummary>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('+', true))
|
||||
.with(autocompleteGroupPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<GroupSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
||||
// Detect last '+' and remove it
|
||||
var startIndex = editable.lastIndexOf("+")
|
||||
if (startIndex == -1) {
|
||||
startIndex = 0
|
||||
}
|
||||
|
||||
// Detect next word separator
|
||||
var endIndex = editable.indexOf(" ", startIndex)
|
||||
if (endIndex == -1) {
|
||||
endIndex = editable.length
|
||||
}
|
||||
|
||||
// Replace the word by its completion
|
||||
val matrixItem = item.toMatrixItem()
|
||||
val displayName = matrixItem.getBestName()
|
||||
|
||||
// with a trailing space
|
||||
editable.replace(startIndex, endIndex, "$displayName ")
|
||||
|
||||
// Add the span
|
||||
val span = PillImageSpan(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
requireContext(),
|
||||
matrixItem
|
||||
)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
autocompleteUserPresenter.callback = this
|
||||
Autocomplete.on<User>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('@', true))
|
||||
|
@ -776,6 +826,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private fun renderTextComposerState(state: TextComposerViewState) {
|
||||
autocompleteUserPresenter.render(state.asyncUsers)
|
||||
autocompleteRoomPresenter.render(state.asyncRooms)
|
||||
autocompleteGroupPresenter.render(state.asyncGroups)
|
||||
}
|
||||
|
||||
private fun renderTombstoneEventHandling(async: Async<String>) {
|
||||
|
@ -1114,6 +1165,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
textComposerViewModel.handle(TextComposerAction.QueryRooms(query))
|
||||
}
|
||||
|
||||
// AutocompleteGroupPresenter.Callback
|
||||
|
||||
override fun onQueryGroups(query: CharSequence?) {
|
||||
textComposerViewModel.handle(TextComposerAction.QueryGroups(query))
|
||||
}
|
||||
|
||||
private fun handleActions(action: EventSharedAction) {
|
||||
when (action) {
|
||||
is EventSharedAction.AddReaction -> {
|
||||
|
|
|
@ -21,4 +21,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
|
|||
sealed class TextComposerAction : VectorViewModelAction {
|
||||
data class QueryUsers(val query: CharSequence?) : TextComposerAction()
|
||||
data class QueryRooms(val query: CharSequence?) : TextComposerAction()
|
||||
data class QueryGroups(val query: CharSequence?) : TextComposerAction()
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay
|
|||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.rx.rx
|
||||
|
@ -43,6 +44,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
|
||||
private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
|
||||
private val roomsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
|
||||
private val groupsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
|
@ -61,12 +63,14 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
init {
|
||||
observeUsersQuery()
|
||||
observeRoomsQuery()
|
||||
observeGroupsQuery()
|
||||
}
|
||||
|
||||
override fun handle(action: TextComposerAction) {
|
||||
when (action) {
|
||||
is TextComposerAction.QueryUsers -> handleQueryUsers(action)
|
||||
is TextComposerAction.QueryRooms -> handleQueryRooms(action)
|
||||
is TextComposerAction.QueryUsers -> handleQueryUsers(action)
|
||||
is TextComposerAction.QueryRooms -> handleQueryRooms(action)
|
||||
is TextComposerAction.QueryGroups -> handleQueryGroups(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +85,11 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
roomsQueryObservable.accept(query)
|
||||
}
|
||||
|
||||
private fun handleQueryGroups(action: TextComposerAction.QueryGroups) {
|
||||
val query = Option.fromNullable(action.query)
|
||||
groupsQueryObservable.accept(query)
|
||||
}
|
||||
|
||||
private fun observeUsersQuery() {
|
||||
Observable.combineLatest<List<String>, Option<AutocompleteQuery>, List<User>>(
|
||||
room.rx().liveRoomMemberIds(),
|
||||
|
@ -124,4 +133,23 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeGroupsQuery() {
|
||||
Observable.combineLatest<List<GroupSummary>, Option<AutocompleteQuery>, List<GroupSummary>>(
|
||||
session.rx().liveGroupSummaries(),
|
||||
groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
BiFunction { groupSummaries, query ->
|
||||
val filter = query.orNull() ?: ""
|
||||
groupSummaries
|
||||
.filter {
|
||||
it.groupId.contains(filter, ignoreCase = true)
|
||||
}
|
||||
.sortedBy { it.displayName }
|
||||
}
|
||||
).execute { async ->
|
||||
copy(
|
||||
asyncGroups = async
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,15 @@ package im.vector.riotx.features.home.room.detail.composer
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
||||
|
||||
data class TextComposerViewState(val roomId: String,
|
||||
val asyncUsers: Async<List<User>> = Uninitialized,
|
||||
val asyncRooms: Async<List<RoomSummary>> = Uninitialized
|
||||
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
|
||||
val asyncGroups: Async<List<GroupSummary>> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
|
||||
|
|
|
@ -54,8 +54,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
|
|||
}
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
// TODO val group = sessionHolder.getSafeActiveSession()?.getGroup(permalinkData.groupId)
|
||||
MatrixItem.RoomItem(permalinkData.groupId /* TODO Group display name and avatar */)
|
||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue