Autocomplete : start integrating commands. Still need to work on it

This commit is contained in:
Benoit Marty 2019-04-08 15:51:35 +02:00
parent a9b8c57464
commit 56563412aa
9 changed files with 333 additions and 3 deletions

View File

@ -171,6 +171,8 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"

implementation 'com.otaliastudios:autocomplete:1.1.0'

// Butterknife
implementation 'com.jakewharton:butterknife:10.1.0'
kapt 'com.jakewharton:butterknife-compiler:10.1.0'

View File

@ -0,0 +1,99 @@
/*
* 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

import android.content.Context
import android.database.DataSetObserver
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.AutocompletePresenter

abstract class EpoxyViewPresenter<T>(context: Context) : AutocompletePresenter<T>(context) {

private var recyclerView: EpoxyRecyclerView? = null
private var clicks: AutocompletePresenter.ClickProvider<T>? = null
private var observer: Observer? = null

override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider<T>) {
this.clicks = provider
}

override fun registerDataSetObserver(observer: DataSetObserver) {
this.observer = Observer(observer)
}

override fun getView(): ViewGroup? {
recyclerView = EpoxyRecyclerView(context).apply {
setController(providesController())
observer?.let {
adapter?.registerAdapterDataObserver(it)
}
itemAnimator = null
}
return recyclerView
}

override fun onViewShown() {}


override fun onViewHidden() {
recyclerView = null
observer = null
}

abstract fun providesController(): EpoxyController
/**
* Dispatch click event to [AutocompleteCallback].
* Should be called when items are clicked.
*
* @param item the clicked item.
*/
protected fun dispatchClick(item: T) {
clicks?.click(item)
}

protected fun dispatchLayoutChange() {
observer?.onChanged()
}


private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {

override fun onChanged() {
root.onChanged()
}

override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
root.onChanged()
}

override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
root.onChanged()
}

override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
root.onChanged()
}

override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
root.onChanged()
}
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.command

import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotredesign.core.resources.StringProvider

class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() {

override fun buildModels(data: List<Command>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach {
autocompleteCommandItem {
id(it.command)
name(it.command)
parameters(it.parameters)
description(stringProvider.getString(it.description))
}
}
}
}

View File

@ -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.riotredesign.features.autocomplete.command

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

@EpoxyModelClass(layout = R.layout.item_command_autocomplete)
abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandItem.Holder>() {

@EpoxyAttribute
var name: CharSequence? = null
@EpoxyAttribute
var parameters: CharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null

override fun bind(holder: Holder) {
holder.nameView.text = name
holder.parametersView.text = parameters
holder.descriptionView.text = description
}

class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.commandName)
val parametersView by bind<TextView>(R.id.commandParameter)
val descriptionView by bind<TextView>(R.id.commandDescription)
}

}

View File

@ -0,0 +1,41 @@
/*
* 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.command

import android.content.Context
import com.airbnb.epoxy.EpoxyController
import im.vector.riotredesign.features.autocomplete.EpoxyViewPresenter

class AutocompleteCommandPresenter(context: Context,
private val controller: AutocompleteCommandController
) : EpoxyViewPresenter<Command>(context) {

override fun providesController(): EpoxyController {
return controller
}

override fun onQuery(query: CharSequence?) {
val data = Command.values().filter {
if (query.isNullOrEmpty()) {
true
} else {
it.command.startsWith(query, 1, true)
}
}
controller.setData(data)
}
}

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.command

import androidx.annotation.StringRes
import im.vector.riotredesign.R

enum class Command(val command: String, val parameters: String, @StringRes val description: Int) {
EMOTE("/me", "<message>", R.string.command_description_emote),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user),
UNBAN_USER("/unban", "<user-id>", R.string.command_description_unban_user),
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user),
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user),
INVITE("/invite", "<user-id>", R.string.command_description_invite_user),
JOIN_ROOM("/join", "<room-alias>", R.string.command_description_join_room),
PART("/part", "<room-alias>", R.string.command_description_part_room),
TOPIC("/topic", "<topic>", R.string.command_description_topic),
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token);
}

View File

@ -18,6 +18,8 @@ package im.vector.riotredesign.features.home

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.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
@ -75,6 +77,9 @@ class HomeModule {
GroupSummaryController()
}


scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
val commandController = AutocompleteCommandController(get())
AutocompleteCommandPresenter(fragment.requireContext(), commandController)
}
}
}

View File

@ -16,6 +16,8 @@

package im.vector.riotredesign.features.home.room.detail

import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.os.Parcelable
import android.view.View
@ -23,11 +25,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
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.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.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
@ -63,6 +69,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac

private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
private val homePermalinkHandler: HomePermalinkHandler by inject()

private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@ -74,7 +81,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupRecyclerView()
setupToolbar()
setupSendButton()
setupComposer()
roomDetailViewModel.subscribe { renderState(it) }
}

@ -114,7 +121,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
timelineEventController.callback = this
}

private fun setupSendButton() {
private fun setupComposer() {
val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText)
.with(CharPolicy('/', false))
.with(autocompleteCommandPresenter)
.with(elevation)
.with(backgroundDrawable)
.build()

sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout 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:padding="6dp">

<TextView
android:id="@+id/commandName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:textSize="12sp"
android:textStyle="bold"
tools:text="/invite" />

<TextView
android:id="@+id/commandParameter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:layout_toEndOf="@+id/commandName"
android:layout_toRightOf="@+id/commandName"
android:maxLines="1"
android:textSize="12sp"
android:textStyle="italic"
tools:text="&lt;user-id&gt;" />

<TextView
android:id="@+id/commandDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/commandName"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/command_description_invite_user" />

</RelativeLayout>