From 56563412aa6df54dd214859af78ff67b4ffc1b5e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Apr 2019 15:51:35 +0200 Subject: [PATCH 01/10] Autocomplete : start integrating commands. Still need to work on it --- vector/build.gradle | 2 + .../autocomplete/EpoxyViewPresenter.kt | 99 +++++++++++++++++++ .../command/AutocompleteCommandController.kt | 37 +++++++ .../command/AutocompleteCommandItem.kt | 48 +++++++++ .../command/AutocompleteCommandPresenter.kt | 41 ++++++++ .../features/autocomplete/command/Command.kt | 36 +++++++ .../riotredesign/features/home/HomeModule.kt | 7 +- .../home/room/detail/RoomDetailFragment.kt | 20 +++- .../res/layout/item_command_autocomplete.xml | 46 +++++++++ 9 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt create mode 100644 vector/src/main/res/layout/item_command_autocomplete.xml diff --git a/vector/build.gradle b/vector/build.gradle index 8bb46c068d..bb99d9aca0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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' diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt new file mode 100644 index 0000000000..f7c64c0acb --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt @@ -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(context: Context) : AutocompletePresenter(context) { + + private var recyclerView: EpoxyRecyclerView? = null + private var clicks: AutocompletePresenter.ClickProvider? = null + private var observer: Observer? = null + + override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) { + 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() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt new file mode 100644 index 0000000000..dbd6a14449 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt @@ -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>() { + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { + autocompleteCommandItem { + id(it.command) + name(it.command) + parameters(it.parameters) + description(stringProvider.getString(it.description)) + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt new file mode 100644 index 0000000000..fa1788ad55 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt @@ -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() { + + @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(R.id.commandName) + val parametersView by bind(R.id.commandParameter) + val descriptionView by bind(R.id.commandDescription) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt new file mode 100644 index 0000000000..d0b10eb63c --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt new file mode 100644 index 0000000000..b3d95fd948 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt @@ -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", "", R.string.command_description_emote), + BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), + UNBAN_USER("/unban", "", R.string.command_description_unban_user), + SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), + RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), + INVITE("/invite", "", R.string.command_description_invite_user), + JOIN_ROOM("/join", "", R.string.command_description_join_room), + PART("/part", "", R.string.command_description_part_room), + TOPIC("/topic", "", R.string.command_description_topic), + KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), + CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), + MARKDOWN("/markdown", "", R.string.command_description_markdown), + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token); +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index a013a0dcae..0367934542 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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) + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 98e3164f32..5493079913 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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(composerEditText) + .with(CharPolicy('/', false)) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .build() + sendButton.setOnClickListener { val textMessage = composerEditText.text.toString() if (textMessage.isNotBlank()) { diff --git a/vector/src/main/res/layout/item_command_autocomplete.xml b/vector/src/main/res/layout/item_command_autocomplete.xml new file mode 100644 index 0000000000..3800443bf6 --- /dev/null +++ b/vector/src/main/res/layout/item_command_autocomplete.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file From aec7b733454a2a669571437e6ce3f5497663ee33 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Apr 2019 15:53:02 +0200 Subject: [PATCH 02/10] Introduce room member service --- .../matrix/android/api/session/room/Room.kt | 9 +-- .../room/members/RoomMembersService.kt | 52 ++++++++++++++++ .../internal/session/room/DefaultRoom.kt | 16 ++--- .../internal/session/room/RoomFactory.kt | 8 +-- .../internal/session/room/RoomModule.kt | 2 +- .../room/members/DefaultRoomMembersService.kt | 61 +++++++++++++++++++ .../session/room/members/RoomMembers.kt | 16 ++++- 7 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 20ec0d534a..f119df9412 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.session.room.members.RoomMembersService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService @@ -26,7 +27,7 @@ import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, SendService, ReadService { +interface Room : TimelineService, SendService, ReadService, RoomMembersService { /** * The roomId of this room @@ -39,10 +40,4 @@ interface Room : TimelineService, SendService, ReadService { */ val roomSummary: LiveData - /** - * This methods load all room members if it was done yet. - * @return a [Cancelable] - */ - fun loadRoomMembersIfNeeded(): Cancelable - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt new file mode 100644 index 0000000000..e589de5cfc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt @@ -0,0 +1,52 @@ +/* + * + * * 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.matrix.android.api.session.room.members + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to retrieve room members of a room. It's implemented at the room level. + */ +interface RoomMembersService { + + /** + * This methods load all room members if it was done yet. + * @return a [Cancelable] + */ + fun loadRoomMembersIfNeeded(): Cancelable + + /** + * Return the roomMember with userId or null. + * @param userId the userId param to look for + * + * @return the roomMember with userId or null + */ + fun getRoomMember(userId: String): RoomMember? + + /** + * Return all the roomMembers of the room + * + * @return a [LiveData] of roomMember list. + */ + fun getRoomMembersLive(): LiveData> + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index ae6255a123..952a1ecac7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -20,35 +20,31 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.members.RoomMembersService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.timeline.TimelineService -import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith internal class DefaultRoom( override val roomId: String, - private val loadRoomMembersTask: LoadRoomMembersTask, private val monarchy: Monarchy, private val timelineService: TimelineService, private val sendService: SendService, private val readService: ReadService, - private val taskExecutor: TaskExecutor + private val roomMembersService: RoomMembersService ) : Room, TimelineService by timelineService, SendService by sendService, - ReadService by readService { + ReadService by readService, + RoomMembersService by roomMembersService { override val roomSummary: LiveData by lazy { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> @@ -59,8 +55,4 @@ internal class DefaultRoom( } } - override fun loadRoomMembersIfNeeded(): Cancelable { - val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) - return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index b30ba3eebd..1d91121e45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -17,8 +17,8 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.session.room.read.DefaultReadService @@ -33,7 +33,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val monarchy: Monarchy, - private val credentials: Credentials, private val paginationTask: PaginationTask, private val contextOfEventTask: GetContextOfEventTask, private val setReadMarkersTask: SetReadMarkersTask, @@ -45,15 +44,16 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) + val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, taskExecutor) val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor) + return DefaultRoom( roomId, - loadRoomMembersTask, monarchy, timelineService, sendService, readService, - taskExecutor + roomMembersService ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 34515d30da..f75302ffdf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -63,7 +63,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt new file mode 100644 index 0000000000..a7b944970a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt @@ -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.matrix.android.internal.session.room.members + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.members.RoomMembersService +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.fetchCopied + +internal class DefaultRoomMembersService(private val roomId: String, + private val monarchy: Monarchy, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val taskExecutor: TaskExecutor +) : RoomMembersService { + + override fun loadRoomMembersIfNeeded(): Cancelable { + val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) + return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) + } + + override fun getRoomMember(userId: String): RoomMember? { + val eventEntity = monarchy.fetchCopied { + RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() + } + return eventEntity?.asDomain()?.content.toModel() + } + + override fun getRoomMembersLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { + RoomMembers(it, roomId).queryRoomMembersEvent() + }, + { + it.asDomain().content.toModel()!! + } + ) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 1bbb792db8..6c6d78ed2e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import io.realm.Realm +import io.realm.RealmQuery import io.realm.Sort internal class RoomMembers(private val realm: Realm, @@ -47,10 +48,21 @@ internal class RoomMembers(private val realm: Realm, } } - fun getLoaded(): Map { + fun queryRoomMembersEvent(): RealmQuery { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX) + .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) + .distinct(EventEntityFields.STATE_KEY) + .isNotNull(EventEntityFields.CONTENT) + } + + fun queryRoomMemberEvent(userId: String): RealmQuery { + return queryRoomMembersEvent() + .equalTo(EventEntityFields.STATE_KEY, userId) + } + + fun getLoaded(): Map { + return queryRoomMembersEvent() .findAll() .map { it.asDomain() } .associateBy { it.stateKey!! } From 6d3028c2d792e5fb8d036abe0b5b586c93e0d6fc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Apr 2019 16:18:36 +0200 Subject: [PATCH 03/10] Autocomplete : start fetching users. Still need to adjust UI and manage selection. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 4 + .../room/members/RoomMembersService.kt | 4 +- .../room/members/DefaultRoomMembersService.kt | 4 +- .../command/AutocompleteCommandController.kt | 1 + .../command/AutocompleteCommandItem.kt | 2 +- .../command/AutocompleteCommandPresenter.kt | 1 + .../user/AutocompleteUserController.kt | 36 +++++++ .../autocomplete/user/AutocompleteUserItem.kt | 46 +++++++++ .../user/AutocompleteUserPresenter.kt | 50 ++++++++++ .../{autocomplete => }/command/Command.kt | 2 +- .../features/command/CommandParser.kt | 20 ++++ .../riotredesign/features/home/HomeModule.kt | 8 ++ .../home/room/detail/RoomDetailFragment.kt | 32 ++++++- .../detail/composer/TextComposerActions.kt | 21 +++++ .../detail/composer/TextComposerViewModel.kt | 94 +++++++++++++++++++ .../detail/composer/TextComposerViewState.kt | 32 +++++++ ...lete.xml => item_autocomplete_command.xml} | 0 .../res/layout/item_autocomplete_user.xml | 28 ++++++ 18 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt rename vector/src/main/java/im/vector/riotredesign/features/{autocomplete => }/command/Command.kt (96%) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt rename vector/src/main/res/layout/{item_command_autocomplete.xml => item_autocomplete_command.xml} (100%) create mode 100644 vector/src/main/res/layout/item_autocomplete_user.xml diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 2fbed52bee..c9dbbdd068 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -26,6 +26,10 @@ class RxRoom(private val room: Room) { return room.roomSummary.asObservable() } + fun liveRoomMemberIds(): Observable> { + return room.getRoomMemberIdsLive().asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt index e589de5cfc..33cbfa5a8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt @@ -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> + fun getRoomMemberIdsLive(): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt index a7b944970a..3a1fba7c7c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt @@ -48,13 +48,13 @@ internal class DefaultRoomMembersService(private val roomId: String, return eventEntity?.asDomain()?.content.toModel() } - override fun getRoomMembersLive(): LiveData> { + override fun getRoomMemberIdsLive(): LiveData> { return monarchy.findAllMappedWithChanges( { RoomMembers(it, roomId).queryRoomMembersEvent() }, { - it.asDomain().content.toModel()!! + it.stateKey!! } ) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt index dbd6a14449..82d180f4ce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt @@ -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>() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt index fa1788ad55..e0620e3d23 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt @@ -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() { @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt index d0b10eb63c..64b2db9b41 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt new file mode 100644 index 0000000000..31d6908541 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt @@ -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>() { + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { + autocompleteUserItem { + id(it.userId) + name(it.displayName) + avatarUrl(it.avatarUrl) + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt new file mode 100644 index 0000000000..6ab9ead512 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt @@ -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() { + + @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(R.id.userAutocompleteName) + val avatarImageView by bind(R.id.userAutocompleteAvatar) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt new file mode 100644 index 0000000000..f0b8956121 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -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(context) { + + var callback: Callback? = null + + override fun providesController(): EpoxyController { + return controller + } + + override fun onQuery(query: CharSequence?) { + callback?.onQueryUsers(query) + } + + fun render(users: Async>) { + if (users is Success) { + controller.setData(users()) + } + } + + interface Callback { + fun onQueryUsers(query: CharSequence?) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt similarity index 96% rename from vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt rename to vector/src/main/java/im/vector/riotredesign/features/command/Command.kt index b3d95fd948..e495552605 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/Command.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt new file mode 100644 index 0000000000..ea508e4236 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt @@ -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 { +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 0367934542..9866605305 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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) + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 5493079913..e7294daafe 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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(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)) + } + } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt new file mode 100644 index 0000000000..cb4e06c6f7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt @@ -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() +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt new file mode 100644 index 0000000000..41c09a191b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt @@ -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(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + private val roomId = initialState.roomId + + private val usersQueryObservable = BehaviorRelay.create>() + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? { + val currentSession = viewModelContext.activity.get() + 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, Option, List>( + 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 + ) + } + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt new file mode 100644 index 0000000000..e9317d71e8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt @@ -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> = Uninitialized +) : MvRxState { + + constructor(args: RoomDetailArgs) : this(roomId = args.roomId) + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/item_command_autocomplete.xml b/vector/src/main/res/layout/item_autocomplete_command.xml similarity index 100% rename from vector/src/main/res/layout/item_command_autocomplete.xml rename to vector/src/main/res/layout/item_autocomplete_command.xml diff --git a/vector/src/main/res/layout/item_autocomplete_user.xml b/vector/src/main/res/layout/item_autocomplete_user.xml new file mode 100644 index 0000000000..873e9c6a75 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_user.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file From c64d6b6b2800f44a2f405511a4c9bf27745ea622 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 8 Apr 2019 18:31:24 +0200 Subject: [PATCH 04/10] Autocomplete : handle click and better detection for / commands --- .../core/epoxy/VectorEpoxyHolder.kt | 2 +- .../riotredesign/core/listener/Listener.kt | 25 +++++++++++ ...enter.kt => EpoxyAutocompletePresenter.kt} | 7 ++- .../command/AutocompleteCommandController.kt | 16 ++++--- .../command/AutocompleteCommandItem.kt | 5 +++ .../command/AutocompleteCommandPresenter.kt | 10 +++-- .../autocomplete/command/CommandPolicy.kt | 44 +++++++++++++++++++ .../user/AutocompleteUserController.kt | 16 ++++--- .../autocomplete/user/AutocompleteUserItem.kt | 5 +++ .../user/AutocompleteUserPresenter.kt | 9 +++- .../home/room/detail/RoomDetailFragment.kt | 28 +++++++++++- 11 files changed, 149 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt rename vector/src/main/java/im/vector/riotredesign/features/autocomplete/{EpoxyViewPresenter.kt => EpoxyAutocompletePresenter.kt} (92%) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt diff --git a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt index f715106a3b..7c6f200bd2 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt @@ -27,7 +27,7 @@ import kotlin.reflect.KProperty * See [SampleKotlinModelWithHolder] for a usage example. */ abstract class VectorEpoxyHolder : EpoxyHolder() { - private lateinit var view: View + lateinit var view: View override fun bindView(itemView: View) { view = itemView diff --git a/vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt b/vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt new file mode 100644 index 0000000000..8746883cd9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt @@ -0,0 +1,25 @@ +/* + * 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.core.listener + +/** + * Simple generic listener interface + */ +interface Listener { + + fun onEvent(t: T) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt similarity index 92% rename from vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt rename to vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt index f7c64c0acb..cf5818d1a1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyViewPresenter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -24,8 +24,9 @@ import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyRecyclerView import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompletePresenter +import im.vector.riotredesign.core.listener.Listener -abstract class EpoxyViewPresenter(context: Context) : AutocompletePresenter(context) { +abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), Listener { private var recyclerView: EpoxyRecyclerView? = null private var clicks: AutocompletePresenter.ClickProvider? = null @@ -73,6 +74,10 @@ abstract class EpoxyViewPresenter(context: Context) : AutocompletePresenter>() { + var listener: Listener? = null + override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return } - data.forEach { + data.forEach { command -> autocompleteCommandItem { - id(it.command) - name(it.command) - parameters(it.parameters) - description(stringProvider.getString(it.description)) + id(command.command) + name(command.command) + parameters(command.parameters) + description(stringProvider.getString(command.description)) + clickListener { _ -> + listener?.onEvent(command) + } } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt index e0620e3d23..cd6a0ff06e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.autocomplete.command +import android.view.View import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -32,8 +33,12 @@ abstract class AutocompleteCommandItem : VectorEpoxyModel(context) { + private val controller: AutocompleteCommandController) : + EpoxyAutocompletePresenter(context) { + + init { + controller.listener = this + } override fun providesController(): EpoxyController { return controller diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt new file mode 100644 index 0000000000..6cf6dfa3d0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt @@ -0,0 +1,44 @@ +/* + * 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.text.Spannable +import com.otaliastudios.autocomplete.AutocompletePolicy + +class CommandPolicy : AutocompletePolicy { + override fun getQuery(text: Spannable): CharSequence { + if (text.length > 0) { + return text.substring(1, text.length) + } + + // Should not happen + return "" + } + + override fun onDismiss(text: Spannable?) { + } + + // Only if text which starts with '/' and without space + override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean { + return text?.startsWith("/") == true + && !text.contains(" ") + } + + override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean { + return !shouldShowPopup(text, cursorPos) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt index 31d6908541..6c9965c56c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt @@ -18,18 +18,24 @@ package im.vector.riotredesign.features.autocomplete.user import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotredesign.core.listener.Listener -class AutocompleteUserController() : TypedEpoxyController>() { +class AutocompleteUserController : TypedEpoxyController>() { + + var listener: Listener? = null override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return } - data.forEach { + data.forEach { user -> autocompleteUserItem { - id(it.userId) - name(it.displayName) - avatarUrl(it.avatarUrl) + id(user.userId) + name(user.displayName) + avatarUrl(user.avatarUrl) + clickListener { _ -> + listener?.onEvent(user) + } } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt index 6ab9ead512..6678bff543 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.autocomplete.user +import android.view.View import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -32,8 +33,12 @@ abstract class AutocompleteUserItem : VectorEpoxyModel(context) { +) : EpoxyAutocompletePresenter(context), Listener { var callback: Callback? = null + init { + controller.listener = this + } + override fun providesController(): EpoxyController { return controller } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index e7294daafe..d7873e1198 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -20,12 +20,14 @@ import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.Parcelable +import android.text.Editable import android.view.View 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.AutocompleteCallback 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 @@ -34,6 +36,7 @@ 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.CommandPolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.home.AvatarRenderer @@ -133,10 +136,22 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac val elevation = 6f val backgroundDrawable = ColorDrawable(Color.WHITE) Autocomplete.on(composerEditText) - .with(CharPolicy('/', false)) + .with(CommandPolicy()) .with(autocompleteCommandPresenter) .with(elevation) .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable?, item: Command?): Boolean { + editable?.clear() + editable + ?.append(item?.command) + ?.append(" ") + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) .build() autocompleteUserPresenter.callback = this @@ -145,6 +160,17 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac .with(autocompleteUserPresenter) .with(elevation) .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable?, item: User?): Boolean { + // TODO + editable?.append(item?.displayName) + ?.append(" ") + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) .build() sendButton.setOnClickListener { From 3f1cc466edce088c7f785b7a971af40d4a1822c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 09:58:07 +0200 Subject: [PATCH 05/10] Autocomplete : handle click --- .../core/epoxy/VectorEpoxyModel.kt | 3 + .../EpoxyAutocompletePresenter.kt | 13 +---- ...Policy.kt => CommandAutocompletePolicy.kt} | 2 +- .../home/room/detail/RoomDetailFragment.kt | 55 +++++++++++++++---- .../features/html/PillImageSpan.kt | 1 - 5 files changed, 48 insertions(+), 26 deletions(-) rename vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/{CommandPolicy.kt => CommandAutocompletePolicy.kt} (96%) diff --git a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt index bb7bb10c3f..16c2401dd9 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt @@ -19,6 +19,9 @@ package im.vector.riotredesign.core.epoxy import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.VisibilityState +/** + * EpoxyModelWithHolder which can listen to visibility state change + */ abstract class VectorEpoxyModel : EpoxyModelWithHolder() { private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt index cf5818d1a1..552b0eb6df 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -22,7 +22,6 @@ 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 import im.vector.riotredesign.core.listener.Listener @@ -60,25 +59,15 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre } 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() } override fun onEvent(t: T) { - dispatchClick(t) + clicks?.click(t) } - private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { override fun onChanged() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt similarity index 96% rename from vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt rename to vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt index 6cf6dfa3d0..74ee50aa06 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandPolicy.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -19,7 +19,7 @@ package im.vector.riotredesign.features.autocomplete.command import android.text.Spannable import com.otaliastudios.autocomplete.AutocompletePolicy -class CommandPolicy : AutocompletePolicy { +class CommandAutocompletePolicy : AutocompletePolicy { override fun getQuery(text: Spannable): CharSequence { if (text.length > 0) { return text.substring(1, text.length) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index d7873e1198..82c2d7d55f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.Parcelable import android.text.Editable +import android.text.Spannable import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy +import im.vector.matrix.android.api.session.Session 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.glide.GlideApp 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.CommandPolicy +import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.home.AvatarRenderer @@ -47,6 +50,7 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerVie 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.html.PillImageSpan import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaViewerActivity import kotlinx.android.parcel.Parcelize @@ -75,6 +79,12 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac } } + private val session by inject() + // TODO Inject? + private val glideRequests by lazy { + GlideApp.with(this) + } + private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val timelineEventController: TimelineEventController by inject { parametersOf(this) } @@ -136,16 +146,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac val elevation = 6f val backgroundDrawable = ColorDrawable(Color.WHITE) Autocomplete.on(composerEditText) - .with(CommandPolicy()) + .with(CommandAutocompletePolicy()) .with(autocompleteCommandPresenter) .with(elevation) .with(backgroundDrawable) .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable?, item: Command?): Boolean { - editable?.clear() + override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { + editable.clear() editable - ?.append(item?.command) - ?.append(" ") + .append(item.command) + .append(" ") return true } @@ -156,15 +166,37 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac autocompleteUserPresenter.callback = this Autocomplete.on(composerEditText) - .with(CharPolicy('@', false)) + .with(CharPolicy('@', true)) .with(autocompleteUserPresenter) .with(elevation) .with(backgroundDrawable) .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable?, item: User?): Boolean { - // TODO - editable?.append(item?.displayName) - ?.append(" ") + override fun onPopupItemClicked(editable: Editable, item: User): 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 displayName = item.displayName ?: item.userId + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val user = session.getUser(item.userId) + // FIXME avatar is not displayed + val span = PillImageSpan(glideRequests, context!!, item.userId, user) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return true } @@ -224,5 +256,4 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) } - } diff --git a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index a51894946f..5ab1853109 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -36,7 +36,6 @@ import java.lang.ref.WeakReference * This span is able to replace a text by a [ChipDrawable] * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. */ - class PillImageSpan(private val glideRequests: GlideRequests, private val context: Context, private val userId: String, From fab1d249f4f228bf2c05ce7e2d6da0d3e64488b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 10:03:06 +0200 Subject: [PATCH 06/10] Autocomplete : better code --- .../autocomplete/AutocompleteClickListener.kt} | 6 +++--- .../features/autocomplete/EpoxyAutocompletePresenter.kt | 5 ++--- .../autocomplete/command/AutocompleteCommandController.kt | 6 +++--- .../autocomplete/user/AutocompleteUserController.kt | 6 +++--- .../features/autocomplete/user/AutocompleteUserPresenter.kt | 3 +-- 5 files changed, 12 insertions(+), 14 deletions(-) rename vector/src/main/java/im/vector/riotredesign/{core/listener/Listener.kt => features/autocomplete/AutocompleteClickListener.kt} (84%) diff --git a/vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt similarity index 84% rename from vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt rename to vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt index 8746883cd9..aa2226d5b2 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/listener/Listener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package im.vector.riotredesign.core.listener +package im.vector.riotredesign.features.autocomplete /** * Simple generic listener interface */ -interface Listener { +interface AutocompleteClickListener { - fun onEvent(t: T) + fun onItemClick(t: T) } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt index 552b0eb6df..ebdee91b16 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -23,9 +23,8 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyRecyclerView import com.otaliastudios.autocomplete.AutocompletePresenter -import im.vector.riotredesign.core.listener.Listener -abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), Listener { +abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { private var recyclerView: EpoxyRecyclerView? = null private var clicks: AutocompletePresenter.ClickProvider? = null @@ -64,7 +63,7 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre observer?.onChanged() } - override fun onEvent(t: T) { + override fun onItemClick(t: T) { clicks?.click(t) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt index 1dbbcaad31..7356364c6c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt @@ -17,13 +17,13 @@ package im.vector.riotredesign.features.autocomplete.command import com.airbnb.epoxy.TypedEpoxyController -import im.vector.riotredesign.core.listener.Listener import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener import im.vector.riotredesign.features.command.Command class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController>() { - var listener: Listener? = null + var listener: AutocompleteClickListener? = null override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { @@ -36,7 +36,7 @@ class AutocompleteCommandController(private val stringProvider: StringProvider) parameters(command.parameters) description(stringProvider.getString(command.description)) clickListener { _ -> - listener?.onEvent(command) + listener?.onItemClick(command) } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt index 6c9965c56c..bec9adb022 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt @@ -18,11 +18,11 @@ package im.vector.riotredesign.features.autocomplete.user import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.user.model.User -import im.vector.riotredesign.core.listener.Listener +import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener class AutocompleteUserController : TypedEpoxyController>() { - var listener: Listener? = null + var listener: AutocompleteClickListener? = null override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { @@ -34,7 +34,7 @@ class AutocompleteUserController : TypedEpoxyController>() { name(user.displayName) avatarUrl(user.avatarUrl) clickListener { _ -> - listener?.onEvent(user) + listener?.onItemClick(user) } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt index 916b0f97ed..be4ec7e702 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -21,12 +21,11 @@ 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.core.listener.Listener import im.vector.riotredesign.features.autocomplete.EpoxyAutocompletePresenter class AutocompleteUserPresenter(context: Context, private val controller: AutocompleteUserController -) : EpoxyAutocompletePresenter(context), Listener { +) : EpoxyAutocompletePresenter(context) { var callback: Callback? = null From eae8f993e642226bc4f7c9c565d41c368e7d0751 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 13:36:33 +0200 Subject: [PATCH 07/10] SlashCommand: implement parser --- .../riotredesign/features/command/Command.kt | 5 + .../features/command/CommandParser.kt | 171 ++++++++++++++++++ .../features/command/ParsedCommand.kt | 48 +++++ .../home/room/detail/RoomDetailFragment.kt | 34 +++- .../home/room/detail/RoomDetailViewModel.kt | 50 ++++- .../home/room/detail/SendMessageResult.kt | 28 +++ 6 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt index e495552605..a41a3afd42 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt @@ -19,6 +19,11 @@ package im.vector.riotredesign.features.command import androidx.annotation.StringRes import im.vector.riotredesign.R +/** + * Defines the command line operations + * the user can write theses messages to perform some actions + * the list will be displayed in this order + */ enum class Command(val command: String, val parameters: String, @StringRes val description: Int) { EMOTE("/me", "", R.string.command_description_emote), BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt index ea508e4236..8a301b66bc 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt @@ -16,5 +16,176 @@ package im.vector.riotredesign.features.command +import timber.log.Timber + object CommandParser { + + /** + * Convert the text message into a Slash command. + * + * @param textMessage the text message + * @return a parsed slash command (ok or error) + */ + fun parseSplashCommand(textMessage: String): ParsedCommand { + // check if it has the Slash marker + if (!textMessage.startsWith("/")) { + return ParsedCommand.ErrorNotACommand + } else { + Timber.d("parseSplashCommand") + + // "/" only + if (textMessage.length == 1) { + return ParsedCommand.ErrorEmptySlashCommand + } + + // Exclude "//" + if ("/" == textMessage.substring(1, 2)) { + return ParsedCommand.ErrorNotACommand + } + + var messageParts: List? = null + + try { + messageParts = textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } + } catch (e: Exception) { + Timber.e(e, "## manageSplashCommand() : split failed " + e.message) + } + + // test if the string cut fails + if (messageParts.isNullOrEmpty()) { + return ParsedCommand.ErrorEmptySlashCommand + } + + val slashCommand = messageParts[0] + + when (slashCommand) { + Command.CHANGE_DISPLAY_NAME.command -> { + val newDisplayname = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() + + return if (newDisplayname.isNotEmpty()) { + ParsedCommand.ChangeDisplayName(newDisplayname) + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) + } + } + Command.TOPIC.command -> { + val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() + + return if (newTopic.isNotEmpty()) { + ParsedCommand.ChangeTopic(newTopic) + } else { + ParsedCommand.ErrorSyntax(Command.TOPIC) + } + } + Command.EMOTE.command -> { + val message = textMessage.substring(Command.EMOTE.command.length).trim() + + return ParsedCommand.SendEmote(message) + } + Command.JOIN_ROOM.command -> { + val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim() + + return if (roomAlias.isNotEmpty()) { + ParsedCommand.JoinRoom(roomAlias) + } else { + ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) + } + } + Command.PART.command -> { + val roomAlias = textMessage.substring(Command.PART.command.length).trim() + + return if (roomAlias.isNotEmpty()) { + ParsedCommand.PartRoom(roomAlias) + } else { + ParsedCommand.ErrorSyntax(Command.PART) + } + } + Command.INVITE.command -> { + return if (messageParts.size == 2) { + ParsedCommand.Invite(messageParts[1]) + } else { + ParsedCommand.ErrorSyntax(Command.INVITE) + } + } + Command.KICK_USER.command -> { + return if (messageParts.size >= 2) { + val user = messageParts[1] + val reason = textMessage.substring(Command.KICK_USER.command.length + + 1 + + user.length).trim() + + ParsedCommand.KickUser(user, reason) + } else { + ParsedCommand.ErrorSyntax(Command.KICK_USER) + } + } + Command.BAN_USER.command -> { + return if (messageParts.size >= 2) { + val user = messageParts[1] + val reason = textMessage.substring(Command.BAN_USER.command.length + + 1 + + user.length).trim() + + ParsedCommand.BanUser(user, reason) + } else { + ParsedCommand.ErrorSyntax(Command.BAN_USER) + } + } + Command.UNBAN_USER.command -> { + return if (messageParts.size == 2) { + ParsedCommand.UnbanUser(messageParts[1]) + } else { + ParsedCommand.ErrorSyntax(Command.UNBAN_USER) + } + } + Command.SET_USER_POWER_LEVEL.command -> { + return if (messageParts.size == 3) { + val userID = messageParts[1] + val powerLevelsAsString = messageParts[2] + + try { + val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString) + + ParsedCommand.SetUserPowerLevel(userID, powerLevelsAsInt) + } catch (e: Exception) { + ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) + } + } else { + ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) + } + } + Command.RESET_USER_POWER_LEVEL.command -> { + return if (messageParts.size == 2) { + val userId = messageParts[1] + + ParsedCommand.SetUserPowerLevel(userId, 0) + } else { + ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) + } + } + Command.MARKDOWN.command -> { + return if (messageParts.size == 2) { + when { + "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) + "off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false) + else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN) + } + } else { + ParsedCommand.ErrorSyntax(Command.MARKDOWN) + } + } + Command.CLEAR_SCALAR_TOKEN.command -> { + return if (messageParts.size == 1) { + ParsedCommand.ClearSclalarToken + } else { + ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) + } + } + else -> { + // Unknown command + return ParsedCommand.ErrorUnknownSlashCommand(slashCommand) + } + } + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt new file mode 100644 index 0000000000..b0bb3ee377 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt @@ -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.command + +/** + * Represent a parsed command + */ +sealed class ParsedCommand { + // This is not a Slash command + object ErrorNotACommand : ParsedCommand() + + object ErrorEmptySlashCommand : ParsedCommand() + + // Unknown/Unsupported slash command + class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand() + + // A slash command is detected, but there is an error + class ErrorSyntax(val command: Command) : ParsedCommand() + + // Valid commands: + + class SendEmote(val message: String) : ParsedCommand() + class BanUser(val userId: String, val reason: String) : ParsedCommand() + class UnbanUser(val userId: String) : ParsedCommand() + class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() + class Invite(val userId: String) : ParsedCommand() + class JoinRoom(val roomAlias: String) : ParsedCommand() + class PartRoom(val roomAlias: String) : ParsedCommand() + class ChangeTopic(val topic: String) : ParsedCommand() + class KickUser(val userId: String, val reason: String) : ParsedCommand() + class ChangeDisplayName(val displayName: String) : ParsedCommand() + class SetMarkdown(val enable: Boolean) : ParsedCommand() + object ClearSclalarToken : ParsedCommand() +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 82c2d7d55f..13ab8eec53 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -23,6 +23,7 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker @@ -35,9 +36,11 @@ 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.extensions.observeEvent import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.utils.toast import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter @@ -80,7 +83,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac } private val session by inject() - // TODO Inject? private val glideRequests by lazy { GlideApp.with(this) } @@ -104,6 +106,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac setupComposer() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } + roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } } override fun onResume() { @@ -192,8 +195,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac // Add the span val user = session.getUser(item.userId) - // FIXME avatar is not displayed val span = PillImageSpan(glideRequests, context!!, item.userId, user) + span.bind(composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -209,7 +212,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac val textMessage = composerEditText.text.toString() if (textMessage.isNotBlank()) { roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage)) - composerEditText.text = null } } } @@ -236,6 +238,32 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac autocompleteUserPresenter.render(state.asyncUsers) } + private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { + when (sendMessageResult) { + is SendMessageResult.MessageSent, is SendMessageResult.SlashCommandHandled -> { + // Clear composer + composerEditText.text = null + } + is SendMessageResult.SlashCommandError -> { + displayError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) + } + is SendMessageResult.SlashCommandUnknown -> { + displayError(getString(R.string.unrecognized_command, sendMessageResult.command)) + } + is SendMessageResult.SlashCommandNotImplemented -> { + activity!!.toast(R.string.not_implemented) + } + } + } + + private fun displayError(message: String) { + AlertDialog.Builder(activity!!) + .setTitle(R.string.command_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index eeef99c0ad..ee9a591ecc 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -16,6 +16,8 @@ package im.vector.riotredesign.features.home.room.detail +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay @@ -24,6 +26,9 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.VectorViewModel +import im.vector.riotredesign.core.utils.LiveEvent +import im.vector.riotredesign.features.command.CommandParser +import im.vector.riotredesign.features.command.ParsedCommand import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import io.reactivex.rxkotlin.subscribeBy @@ -63,17 +68,54 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.LoadMore -> handleLoadMore(action) } } + private val _sendMessageResultLiveData = MutableLiveData>() + val sendMessageResultLiveData: LiveData> + get() = _sendMessageResultLiveData + // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { - room.sendTextMessage(action.text, callback = object : MatrixCallback {}) + // Handle slash command + val slashCommandResult = CommandParser.parseSplashCommand(action.text) + + when (slashCommandResult) { + is ParsedCommand.ErrorNotACommand -> { + // Send the text message to the room + room.sendTextMessage(action.text, callback = object : MatrixCallback {}) + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + is ParsedCommand.ErrorSyntax -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) + } + is ParsedCommand.ErrorEmptySlashCommand -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) + } + is ParsedCommand.ErrorUnknownSlashCommand -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) + } + else -> { + handleValidSlashCommand(slashCommandResult) + } + } + } + + private fun handleValidSlashCommand(parsedCommand: ParsedCommand) { + when (parsedCommand) { + is ParsedCommand.Invite -> { + //room.invite(parsedCommand.userId) + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + } + else -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + } } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt new file mode 100644 index 0000000000..8570e4fc22 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt @@ -0,0 +1,28 @@ +/* + * 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 + +import im.vector.riotredesign.features.command.Command + +sealed class SendMessageResult { + object MessageSent : SendMessageResult() + class SlashCommandError(val command: Command) : SendMessageResult() + class SlashCommandUnknown(val command: String) : SendMessageResult() + object SlashCommandHandled : SendMessageResult() + // TODO Remove + object SlashCommandNotImplemented : SendMessageResult() +} \ No newline at end of file From 81ddb8c5fb02c5f8707273e5b97c6e39baa1ff09 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 14:35:18 +0200 Subject: [PATCH 08/10] SlashCommand: handle user invitation --- .../matrix/android/api/session/room/Room.kt | 1 - .../room/members/RoomMembersService.kt | 5 +++ .../android/internal/session/room/RoomAPI.kt | 10 +++++ .../internal/session/room/RoomFactory.kt | 4 +- .../internal/session/room/RoomModule.kt | 8 +++- .../session/room/invite/InviteBody.kt | 25 ++++++++++++ .../session/room/invite/InviteTask.kt | 40 +++++++++++++++++++ .../room/members/DefaultRoomMembersService.kt | 10 +++++ .../home/room/detail/RoomDetailFragment.kt | 18 ++++++--- .../home/room/detail/RoomDetailViewModel.kt | 25 +++++++----- .../home/room/detail/SendMessageResult.kt | 2 + 11 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index f119df9412..cdc971fd2d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.timeline.TimelineService -import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to interact within a room. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt index 33cbfa5a8d..930afd7abf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.members import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable @@ -48,5 +49,9 @@ interface RoomMembersService { */ fun getRoomMemberIdsLive(): LiveData> + /** + * Invite a user in the room + */ + fun invite(userId: String, callback: MatrixCallback) } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index bc44300f4d..1b89662daa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.room.invite.InviteBody import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse @@ -120,5 +121,14 @@ internal interface RoomAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map): Call + /** + * Invite a user to the given room. + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-rooms-roomid-invite + * + * @param roomId the room id + * @param body a object that just contains a user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") + fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 1d91121e45..720d3804a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor @@ -32,6 +33,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFact import im.vector.matrix.android.internal.task.TaskExecutor internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, private val monarchy: Monarchy, private val paginationTask: PaginationTask, private val contextOfEventTask: GetContextOfEventTask, @@ -44,7 +46,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) - val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, taskExecutor) + val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, inviteTask, taskExecutor) val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor) return DefaultRoom( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index f75302ffdf..ccdfe9ad99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask +import im.vector.matrix.android.internal.session.room.invite.DefaultInviteTask +import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask @@ -63,12 +65,16 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { DefaultCreateRoomTask(get(), get()) as CreateRoomTask } + scope(DefaultSession.SCOPE) { + DefaultInviteTask(get()) as InviteTask + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt new file mode 100644 index 0000000000..652f8d63fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt @@ -0,0 +1,25 @@ +/* + * 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.matrix.android.internal.session.room.invite + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InviteBody( + @Json(name = "user_id") val userId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt new file mode 100644 index 0000000000..1086c6b5d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt @@ -0,0 +1,40 @@ +/* + * 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.matrix.android.internal.session.room.invite + +import arrow.core.Try +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task + + +internal interface InviteTask : Task { + data class Params( + val roomId: String, + val userId: String + ) +} + +internal class DefaultInviteTask(private val roomAPI: RoomAPI) : InviteTask { + + override fun execute(params: InviteTask.Params): Try { + return executeRequest { + val body = InviteBody(params.userId) + apiCall = roomAPI.invite(params.roomId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt index 3a1fba7c7c..a007a1eb63 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt @@ -20,12 +20,14 @@ package im.vector.matrix.android.internal.session.room.members import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.members.RoomMembersService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied @@ -33,6 +35,7 @@ import im.vector.matrix.android.internal.util.fetchCopied internal class DefaultRoomMembersService(private val roomId: String, private val monarchy: Monarchy, private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, private val taskExecutor: TaskExecutor ) : RoomMembersService { @@ -58,4 +61,11 @@ internal class DefaultRoomMembersService(private val roomId: String, } ) } + + override fun invite(userId: String, callback: MatrixCallback) { + val params = InviteTask.Params(roomId, userId) + inviteTask.configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 13ab8eec53..a5b6738411 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -40,7 +40,6 @@ import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.VectorBaseFragment -import im.vector.riotredesign.core.utils.toast import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter @@ -240,23 +239,30 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { - is SendMessageResult.MessageSent, is SendMessageResult.SlashCommandHandled -> { + is SendMessageResult.MessageSent, + is SendMessageResult.SlashCommandHandled -> { // Clear composer composerEditText.text = null } is SendMessageResult.SlashCommandError -> { - displayError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) + displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } is SendMessageResult.SlashCommandUnknown -> { - displayError(getString(R.string.unrecognized_command, sendMessageResult.command)) + displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) + } + is SendMessageResult.SlashCommandResultOk -> { + // Ignore + } + is SendMessageResult.SlashCommandResultError -> { + displayCommandError(sendMessageResult.throwable.localizedMessage) } is SendMessageResult.SlashCommandNotImplemented -> { - activity!!.toast(R.string.not_implemented) + displayCommandError(getString(R.string.not_implemented)) } } } - private fun displayError(message: String) { + private fun displayCommandError(message: String) { AlertDialog.Builder(activity!!) .setTitle(R.string.command_error) .setMessage(message) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index ee9a591ecc..cb56cb6bbf 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -100,17 +100,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is ParsedCommand.ErrorUnknownSlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) } - else -> { - handleValidSlashCommand(slashCommandResult) - } - } - } - - private fun handleValidSlashCommand(parsedCommand: ParsedCommand) { - when (parsedCommand) { is ParsedCommand.Invite -> { - //room.invite(parsedCommand.userId) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + handleInviteSlashCommand(slashCommandResult) } else -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) @@ -118,6 +109,20 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } + private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + + room.invite(invite.userId, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) + } + + override fun onFailure(failure: Throwable) { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) + } + }) + } + private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { displayedEventsObservable.accept(action) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt index 8570e4fc22..189ad90d88 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt @@ -23,6 +23,8 @@ sealed class SendMessageResult { class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() object SlashCommandHandled : SendMessageResult() + object SlashCommandResultOk : SendMessageResult() + class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() // TODO Remove object SlashCommandNotImplemented : SendMessageResult() } \ No newline at end of file From 3b52fa4be831d3c7dc890fcd9ca55378e81cb2ea Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 14:52:31 +0200 Subject: [PATCH 09/10] Command parser: check userId format --- .../features/command/CommandParser.kt | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt index 8a301b66bc..1de4ac4206 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.command +import im.vector.matrix.android.api.MatrixPatterns import timber.log.Timber object CommandParser { @@ -60,10 +61,10 @@ object CommandParser { when (slashCommand) { Command.CHANGE_DISPLAY_NAME.command -> { - val newDisplayname = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() + val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() - return if (newDisplayname.isNotEmpty()) { - ParsedCommand.ChangeDisplayName(newDisplayname) + return if (newDisplayName.isNotEmpty()) { + ParsedCommand.ChangeDisplayName(newDisplayName) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) } @@ -102,52 +103,76 @@ object CommandParser { } Command.INVITE.command -> { return if (messageParts.size == 2) { - ParsedCommand.Invite(messageParts[1]) + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.Invite(userId) + } else { + ParsedCommand.ErrorSyntax(Command.INVITE) + } } else { ParsedCommand.ErrorSyntax(Command.INVITE) } } Command.KICK_USER.command -> { return if (messageParts.size >= 2) { - val user = messageParts[1] - val reason = textMessage.substring(Command.KICK_USER.command.length - + 1 - + user.length).trim() + val userId = messageParts[1] + if (MatrixPatterns.isUserId(userId)) { + val reason = textMessage.substring(Command.KICK_USER.command.length + + 1 + + userId.length).trim() - ParsedCommand.KickUser(user, reason) + ParsedCommand.KickUser(userId, reason) + } else { + ParsedCommand.ErrorSyntax(Command.KICK_USER) + } } else { ParsedCommand.ErrorSyntax(Command.KICK_USER) } } Command.BAN_USER.command -> { return if (messageParts.size >= 2) { - val user = messageParts[1] - val reason = textMessage.substring(Command.BAN_USER.command.length - + 1 - + user.length).trim() + val userId = messageParts[1] + if (MatrixPatterns.isUserId(userId)) { + val reason = textMessage.substring(Command.BAN_USER.command.length + + 1 + + userId.length).trim() - ParsedCommand.BanUser(user, reason) + ParsedCommand.BanUser(userId, reason) + } else { + ParsedCommand.ErrorSyntax(Command.BAN_USER) + } } else { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } Command.UNBAN_USER.command -> { return if (messageParts.size == 2) { - ParsedCommand.UnbanUser(messageParts[1]) + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.UnbanUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.UNBAN_USER) + } } else { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } Command.SET_USER_POWER_LEVEL.command -> { return if (messageParts.size == 3) { - val userID = messageParts[1] - val powerLevelsAsString = messageParts[2] + val userId = messageParts[1] + if (MatrixPatterns.isUserId(userId)) { + val powerLevelsAsString = messageParts[2] - try { - val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString) + try { + val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString) - ParsedCommand.SetUserPowerLevel(userID, powerLevelsAsInt) - } catch (e: Exception) { + ParsedCommand.SetUserPowerLevel(userId, powerLevelsAsInt) + } catch (e: Exception) { + ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) + } + } else { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } else { @@ -158,7 +183,11 @@ object CommandParser { return if (messageParts.size == 2) { val userId = messageParts[1] - ParsedCommand.SetUserPowerLevel(userId, 0) + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.SetUserPowerLevel(userId, 0) + } else { + ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) + } } else { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } From 63964ac10161d629fcf3e7d95ba1b65bf5147f51 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 9 Apr 2019 15:21:17 +0200 Subject: [PATCH 10/10] SlashCommand: add TODO --- .../features/command/CommandParser.kt | 2 +- .../features/command/ParsedCommand.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 43 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt index 1de4ac4206..f442d1d709 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt @@ -205,7 +205,7 @@ object CommandParser { } Command.CLEAR_SCALAR_TOKEN.command -> { return if (messageParts.size == 1) { - ParsedCommand.ClearSclalarToken + ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt index b0bb3ee377..350423b1d3 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt @@ -44,5 +44,5 @@ sealed class ParsedCommand { class KickUser(val userId: String, val reason: String) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() - object ClearSclalarToken : ParsedCommand() + object ClearScalarToken : ParsedCommand() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index cb56cb6bbf..895c665912 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -103,7 +103,48 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } - else -> { + is ParsedCommand.SetUserPowerLevel -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.ClearScalarToken -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.SetMarkdown -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.UnbanUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.BanUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.KickUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.JoinRoom -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.PartRoom -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.SendEmote -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.ChangeTopic -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.ChangeDisplayName -> { + // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } }