forked from GitHub-Mirror/riotX-android
Autocomplete : start integrating commands. Still need to work on it
This commit is contained in:
parent
a9b8c57464
commit
56563412aa
@ -171,6 +171,8 @@ dependencies {
|
|||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
implementation "ru.noties.markwon:html:$markwon_version"
|
implementation "ru.noties.markwon:html:$markwon_version"
|
||||||
|
|
||||||
|
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
||||||
|
|
||||||
// Butterknife
|
// Butterknife
|
||||||
implementation 'com.jakewharton:butterknife:10.1.0'
|
implementation 'com.jakewharton:butterknife:10.1.0'
|
||||||
kapt 'com.jakewharton:butterknife-compiler:10.1.0'
|
kapt 'com.jakewharton:butterknife-compiler:10.1.0'
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -18,6 +18,8 @@ package im.vector.riotredesign.features.home
|
|||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
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.group.GroupSummaryController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||||
@ -75,6 +77,9 @@ class HomeModule {
|
|||||||
GroupSummaryController()
|
GroupSummaryController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
||||||
|
val commandController = AutocompleteCommandController(get())
|
||||||
|
AutocompleteCommandPresenter(fragment.requireContext(), commandController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -23,11 +25,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
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.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
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.AvatarRenderer
|
||||||
import im.vector.riotredesign.features.home.HomeModule
|
import im.vector.riotredesign.features.home.HomeModule
|
||||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||||
@ -63,6 +69,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
|
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||||
|
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
||||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
@ -74,7 +81,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupSendButton()
|
setupComposer()
|
||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +121,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
timelineEventController.callback = this
|
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 {
|
sendButton.setOnClickListener {
|
||||||
val textMessage = composerEditText.text.toString()
|
val textMessage = composerEditText.text.toString()
|
||||||
if (textMessage.isNotBlank()) {
|
if (textMessage.isNotBlank()) {
|
||||||
|
46
vector/src/main/res/layout/item_command_autocomplete.xml
Normal file
46
vector/src/main/res/layout/item_command_autocomplete.xml
Normal 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="<user-id>" />
|
||||||
|
|
||||||
|
<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>
|
Loading…
Reference in New Issue
Block a user