Autocomplete : start fetching users. Still need to adjust UI and manage selection.

This commit is contained in:
Benoit Marty 2019-04-08 16:18:36 +02:00
parent aec7b73345
commit 6d3028c2d7
18 changed files with 376 additions and 9 deletions

View File

@ -26,6 +26,10 @@ class RxRoom(private val room: Room) {
return room.roomSummary.asObservable()
}
fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable()
}
}
fun Room.rx(): RxRoom {

View File

@ -42,11 +42,11 @@ interface RoomMembersService {
fun getRoomMember(userId: String): RoomMember?
/**
* Return all the roomMembers of the room
* Return all the roomMembers ids of the room
*
* @return a [LiveData] of roomMember list.
*/
fun getRoomMembersLive(): LiveData<List<RoomMember>>
fun getRoomMemberIdsLive(): LiveData<List<String>>
}

View File

@ -48,13 +48,13 @@ internal class DefaultRoomMembersService(private val roomId: String,
return eventEntity?.asDomain()?.content.toModel()
}
override fun getRoomMembersLive(): LiveData<List<RoomMember>> {
override fun getRoomMemberIdsLive(): LiveData<List<String>> {
return monarchy.findAllMappedWithChanges(
{
RoomMembers(it, roomId).queryRoomMembersEvent()
},
{
it.asDomain().content.toModel<RoomMember>()!!
it.stateKey!!
}
)
}

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.features.autocomplete.command
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.command.Command
class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() {

View File

@ -23,7 +23,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_command_autocomplete)
@EpoxyModelClass(layout = R.layout.item_autocomplete_command)
abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandItem.Holder>() {
@EpoxyAttribute

View File

@ -19,6 +19,7 @@ package im.vector.riotredesign.features.autocomplete.command
import android.content.Context
import com.airbnb.epoxy.EpoxyController
import im.vector.riotredesign.features.autocomplete.EpoxyViewPresenter
import im.vector.riotredesign.features.command.Command
class AutocompleteCommandPresenter(context: Context,
private val controller: AutocompleteCommandController

View File

@ -0,0 +1,36 @@
/*
* 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.riotredesign.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User
class AutocompleteUserController() : TypedEpoxyController<List<User>>() {
override fun buildModels(data: List<User>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach {
autocompleteUserItem {
id(it.userId)
name(it.displayName)
avatarUrl(it.avatarUrl)
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.riotredesign.features.autocomplete.user
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
@EpoxyAttribute
var name: String? = null
@EpoxyAttribute
var avatarUrl: String? = null
override fun bind(holder: Holder) {
holder.nameView.text = name
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.userAutocompleteName)
val avatarImageView by bind<ImageView>(R.id.userAutocompleteAvatar)
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.riotredesign.features.autocomplete.user
import android.content.Context
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.features.autocomplete.EpoxyViewPresenter
class AutocompleteUserPresenter(context: Context,
private val controller: AutocompleteUserController
) : EpoxyViewPresenter<User>(context) {
var callback: Callback? = null
override fun providesController(): EpoxyController {
return controller
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryUsers(query)
}
fun render(users: Async<List<User>>) {
if (users is Success) {
controller.setData(users())
}
}
interface Callback {
fun onQueryUsers(query: CharSequence?)
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotredesign.features.autocomplete.command
package im.vector.riotredesign.features.command
import androidx.annotation.StringRes
import im.vector.riotredesign.R

View File

@ -0,0 +1,20 @@
/*
* 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.riotredesign.features.command
object CommandParser {
}

View File

@ -20,6 +20,8 @@ import androidx.fragment.app.Fragment
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandController
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserController
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
@ -81,5 +83,11 @@ class HomeModule {
val commandController = AutocompleteCommandController(get())
AutocompleteCommandPresenter(fragment.requireContext(), commandController)
}
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
val userController = AutocompleteUserController()
AutocompleteUserPresenter(fragment.requireContext(), userController)
}
}
}

View File

@ -28,15 +28,20 @@ import com.airbnb.mvrx.fragmentViewModel
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.command.Command
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotredesign.features.media.MediaContentRenderer
@ -56,7 +61,7 @@ data class RoomDetailArgs(
) : Parcelable
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback {
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback {
companion object {
@ -68,8 +73,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
}
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
private val homePermalinkHandler: HomePermalinkHandler by inject()
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@ -83,6 +90,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
setupToolbar()
setupComposer()
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
}
override fun onResume() {
@ -131,6 +139,14 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
.with(backgroundDrawable)
.build()
autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerEditText)
.with(CharPolicy('@', false))
.with(autocompleteUserPresenter)
.with(elevation)
.with(backgroundDrawable)
.build()
sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
@ -158,7 +174,11 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
}
}
// TimelineEventController.Callback ************************************************************
private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers)
}
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) {
homePermalinkHandler.launch(url)
@ -173,4 +193,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
startActivity(intent)
}
// AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.riotredesign.features.home.room.detail.composer
sealed class TextComposerActions {
data class QueryUsers(val query: CharSequence?) : TextComposerActions()
}

View File

@ -0,0 +1,94 @@
/*
* 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.riotredesign.features.home.room.detail.composer
import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
typealias AutocompleteUserQuery = CharSequence
class TextComposerViewModel(initialState: TextComposerViewState,
private val session: Session
) : VectorViewModel<TextComposerViewState>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId
private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteUserQuery>>()
companion object : MvRxViewModelFactory<TextComposerViewModel, TextComposerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
return TextComposerViewModel(state, currentSession)
}
}
init {
observeUsersQuery()
}
fun process(action: TextComposerActions) {
when (action) {
is TextComposerActions.QueryUsers -> handleQueryUsers(action)
}
}
private fun handleQueryUsers(action: TextComposerActions.QueryUsers) {
val query = Option.fromNullable(action.query)
usersQueryObservable.accept(query)
}
private fun observeUsersQuery() {
Observable.combineLatest<List<String>, Option<AutocompleteUserQuery>, List<User>>(
room.rx().liveRoomMemberIds(),
usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomMembers, query ->
val users = roomMembers
.mapNotNull {
session.getUser(it)
}
val filter = query.orNull()
if (filter.isNullOrBlank()) {
users
} else {
users.filter {
it.displayName?.startsWith(prefix = filter, ignoreCase = true)
?: false
}
}
}
).execute { async ->
copy(
asyncUsers = async
)
}
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.riotredesign.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.user.model.User
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
data class TextComposerViewState(val roomId: String,
val asyncUsers: Async<List<User>> = Uninitialized
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
}

View File

@ -0,0 +1,28 @@
<?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="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/userAutocompleteAvatar"
android:layout_width="28dp"
android:layout_height="28dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/userAutocompleteName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:maxLines="1"
android:textSize="12sp"
android:textStyle="bold"
tools:text="name" />
</LinearLayout>