From d73a1135ae8afbe1246a52bd6bd991698f83f35e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:01:07 +0100 Subject: [PATCH 01/12] Extract AutoComplete feature from RoomDetailFragment --- .../home/room/detail/AutoCompleter.kt | 241 ++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 192 +------------- 2 files changed, 247 insertions(+), 186 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt new file mode 100644 index 0000000000..5e8a87ba51 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +import android.graphics.drawable.ColorDrawable +import android.text.Editable +import android.text.Spannable +import android.widget.EditText +import androidx.fragment.app.Fragment +import com.otaliastudios.autocomplete.Autocomplete +import com.otaliastudios.autocomplete.AutocompleteCallback +import com.otaliastudios.autocomplete.CharPolicy +import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.util.toRoomAliasMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter +import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter +import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter +import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter +import im.vector.riotx.features.command.Command +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState +import im.vector.riotx.features.html.PillImageSpan +import im.vector.riotx.features.themes.ThemeUtils +import javax.inject.Inject + +class AutoCompleter @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val commandAutocompletePolicy: CommandAutocompletePolicy, + private val autocompleteCommandPresenter: AutocompleteCommandPresenter, + private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteRoomPresenter: AutocompleteRoomPresenter, + private val autocompleteGroupPresenter: AutocompleteGroupPresenter +) { + private lateinit var fragment: Fragment + + fun enterSpecialMode() { + commandAutocompletePolicy.enabled = false + } + + fun exitSpecialMode() { + commandAutocompletePolicy.enabled = true + } + + private val glideRequests by lazy { + GlideApp.with(fragment) + } + + fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { + this.fragment = fragment + + val elevation = 6f + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + Autocomplete.on(editText) + .with(commandAutocompletePolicy) + .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() + + autocompleteRoomPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('#', true)) + .with(autocompleteRoomPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { + // Detect last '#' and remove it + var startIndex = editable.lastIndexOf("#") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toRoomAliasMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteGroupPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('+', true)) + .with(autocompleteGroupPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { + // Detect last '+' and remove it + var startIndex = editable.lastIndexOf("+") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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 matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + fun render(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) + } + + interface AutoCompleterListener : + AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 334445870c..4ae79e8215 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -20,12 +20,10 @@ import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.DialogInterface import android.content.Intent -import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcelable -import android.text.Editable import android.text.Spannable import android.view.* import android.widget.TextView @@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText -import com.otaliastudios.autocomplete.Autocomplete -import com.otaliastudios.autocomplete.AutocompleteCallback -import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent -import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem -import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -84,11 +75,6 @@ import im.vector.riotx.core.utils.* import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment -import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter -import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy -import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter -import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter -import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId @@ -117,7 +103,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.SharedData -import im.vector.riotx.features.themes.ThemeUtils import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize @@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, - private val commandAutocompletePolicy: CommandAutocompletePolicy, - private val autocompleteCommandPresenter: AutocompleteCommandPresenter, - private val autocompleteUserPresenter: AutocompleteUserPresenter, - private val autocompleteRoomPresenter: AutocompleteRoomPresenter, - private val autocompleteGroupPresenter: AutocompleteGroupPresenter, + private val autoCompleter: AutoCompleter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor( ) : VectorBaseFragment(), TimelineEventController.Callback, - AutocompleteUserPresenter.Callback, - AutocompleteRoomPresenter.Callback, - AutocompleteGroupPresenter.Callback, + AutoCompleter.AutoCompleterListener, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -397,7 +376,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderRegularMode(text: String) { - commandAutocompletePolicy.enabled = true + autoCompleter.exitSpecialMode() composerLayout.collapse() updateComposerText(text) @@ -408,7 +387,7 @@ class RoomDetailFragment @Inject constructor( @DrawableRes iconRes: Int, @StringRes descriptionRes: Int, defaultContent: String) { - commandAutocompletePolicy.enabled = false + autoCompleter.enterSpecialMode() // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { text = event.getDisambiguatedDisplayName() @@ -580,164 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - Autocomplete.on(composerLayout.composerEditText) - .with(commandAutocompletePolicy) - .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() - - autocompleteRoomPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('#', true)) - .with(autocompleteRoomPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { - // Detect last '#' and remove it - var startIndex = editable.lastIndexOf("#") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toRoomAliasMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteGroupPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('+', true)) - .with(autocompleteGroupPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { - // Detect last '+' and remove it - var startIndex = editable.lastIndexOf("+") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteUserPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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 matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() + autoCompleter.setup(this, composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { @@ -834,9 +656,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderTextComposerState(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + autoCompleter.render(state) } private fun renderTombstoneEventHandling(async: Async) { From c4fe0bdb7f74695c501d7895c3db199ea892fac5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:08:28 +0100 Subject: [PATCH 02/12] Split into small methods --- .../home/room/detail/AutoCompleter.kt | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 5e8a87ba51..7edba64a05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.widget.EditText @@ -68,12 +69,19 @@ class AutoCompleter @Inject constructor( fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { this.fragment = fragment - val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + + setupCommands(backgroundDrawable, editText) + setupUsers(backgroundDrawable, editText, listener) + setupRooms(backgroundDrawable, editText, listener) + setupGroups(backgroundDrawable, editText, listener) + } + + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { @@ -88,12 +96,62 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) { + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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 matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) { autocompleteRoomPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('#', true)) .with(autocompleteRoomPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { @@ -134,12 +192,14 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) { autocompleteGroupPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('+', true)) .with(autocompleteGroupPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { @@ -180,52 +240,6 @@ class AutoCompleter @Inject constructor( } }) .build() - - autocompleteUserPresenter.callback = listener - Autocomplete.on(editText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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 matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() } fun render(state: TextComposerViewState) { @@ -238,4 +252,8 @@ class AutoCompleter @Inject constructor( AutocompleteUserPresenter.Callback, AutocompleteRoomPresenter.Callback, AutocompleteGroupPresenter.Callback + + companion object { + private const val ELEVATION = 6f + } } From d88e5d8af8b7afea2c5dfc6c33068d5355d3fabb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:14:26 +0100 Subject: [PATCH 03/12] DRY --- .../home/room/detail/AutoCompleter.kt | 133 +++++------------- 1 file changed, 39 insertions(+), 94 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 7edba64a05..ef7715f91a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -28,6 +28,7 @@ import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R @@ -77,6 +78,12 @@ class AutoCompleter @Inject constructor( setupGroups(backgroundDrawable, editText, listener) } + fun render(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) + } + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) @@ -107,36 +114,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { 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 matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "@", item.toMatrixItem()) return true } @@ -155,36 +133,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { - // Detect last '#' and remove it - var startIndex = editable.lastIndexOf("#") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toRoomAliasMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem()) return true } @@ -203,36 +152,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { - // Detect last '+' and remove it - var startIndex = editable.lastIndexOf("+") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "+", item.toMatrixItem()) return true } @@ -242,10 +162,35 @@ class AutoCompleter @Inject constructor( .build() } - fun render(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) { + // Detect last firstChar and remove it + var startIndex = editable.lastIndexOf(firstChar) + 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 = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } interface AutoCompleterListener : From 8597c2b9a2d99d5edcbf3fa2dd9ef89ba30f955e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:23:30 +0100 Subject: [PATCH 04/12] Improve API --- .../riotx/features/home/room/detail/AutoCompleter.kt | 12 ++++++------ .../features/home/room/detail/RoomDetailFragment.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ef7715f91a..ba643e2d7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -53,7 +53,7 @@ class AutoCompleter @Inject constructor( private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter ) { - private lateinit var fragment: Fragment + private lateinit var editText: EditText fun enterSpecialMode() { commandAutocompletePolicy.enabled = false @@ -64,13 +64,13 @@ class AutoCompleter @Inject constructor( } private val glideRequests by lazy { - GlideApp.with(fragment) + GlideApp.with(editText) } - fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { - this.fragment = fragment + fun setup(editText: EditText, listener: AutoCompleterListener) { + this.editText = editText - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) setupCommands(backgroundDrawable, editText) setupUsers(backgroundDrawable, editText, listener) @@ -185,7 +185,7 @@ class AutoCompleter @Inject constructor( val span = PillImageSpan( glideRequests, avatarRenderer, - fragment.requireContext(), + editText.context, matrixItem ) span.bind(editText) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4ae79e8215..4414c48205 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -559,7 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - autoCompleter.setup(this, composerLayout.composerEditText, this) + autoCompleter.setup(composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { From 8b4c51139df1406d15fa2e6e76eca58eae7e4020 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:00:02 +0100 Subject: [PATCH 05/12] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 71 +++++++++++++++++++ .../emoji/AutocompleteEmojiItem.kt | 52 ++++++++++++++ .../emoji/AutocompleteEmojiPresenter.kt | 54 ++++++++++++++ .../home/room/detail/AutoCompleter.kt | 27 ++++++- .../reactions/EmojiSearchResultViewModel.kt | 16 +---- .../reactions/data/EmojiDataSource.kt | 21 ++++++ 6 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt new file mode 100644 index 0000000000..e0d7bdd888 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import android.graphics.Typeface +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.reactions.ReactionClickListener +import im.vector.riotx.features.reactions.data.EmojiItem +import im.vector.riotx.features.reactions.emojiSearchResultItem +import javax.inject.Inject + +class AutocompleteEmojiController @Inject constructor( + private val fontProvider: EmojiCompatFontProvider +) : TypedEpoxyController>() { + + var emojiTypeface: Typeface? = fontProvider.typeface + + private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener { + override fun compatibilityFontUpdate(typeface: Typeface?) { + emojiTypeface = typeface + } + } + + init { + fontProvider.addListener(fontProviderListener) + } + + var listener: AutocompleteClickListener? = null + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { emojiItem -> + emojiSearchResultItem { + id(emojiItem.name) + emojiItem(emojiItem) + emojiTypeFace(emojiTypeface) + //currentQuery(data.query) + onClickListener(object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } + ) + } + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + fontProvider.removeListener(fontProviderListener) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt new file mode 100644 index 0000000000..e5c53315ab --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.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.riotx.features.autocomplete.emoji + +import android.view.View +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +//@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +//abstract class AutocompleteEmojiItem : VectorEpoxyModel() { +// +// @EpoxyAttribute +// var name: CharSequence? = null +// @EpoxyAttribute +// var parameters: CharSequence? = null +// @EpoxyAttribute +// var description: CharSequence? = null +// @EpoxyAttribute +// var clickListener: View.OnClickListener? = null +// +// override fun bind(holder: Holder) { +// holder.view.setOnClickListener(clickListener) +// +// 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) +// } +//} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt new file mode 100644 index 0000000000..731b48af86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.reactions.data.EmojiDataSource +import javax.inject.Inject + +class AutocompleteEmojiPresenter @Inject constructor(context: Context, + private val emojiDataSource: EmojiDataSource, + private val controller: AutocompleteEmojiController) : + RecyclerViewPresenter(context), AutocompleteClickListener { + + init { + controller.listener = this + } + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null + return controller.adapter + } + + override fun onItemClick(t: String) { + dispatchClick(t) + } + + override fun onQuery(query: CharSequence?) { + val data = if (query.isNullOrBlank()) { + // Return common emojis + emojiDataSource.getQuickReactions() + } else { + emojiDataSource.filterWith(query.toString()) + } + controller.setData(data) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ba643e2d7d..9fe2249450 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.widget.EditText -import androidx.fragment.app.Fragment import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy @@ -35,6 +34,7 @@ import im.vector.riotx.R import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -51,7 +51,8 @@ class AutoCompleter @Inject constructor( private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, - private val autocompleteGroupPresenter: AutocompleteGroupPresenter + private val autocompleteGroupPresenter: AutocompleteGroupPresenter, + private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { private lateinit var editText: EditText @@ -76,6 +77,7 @@ class AutoCompleter @Inject constructor( setupUsers(backgroundDrawable, editText, listener) setupRooms(backgroundDrawable, editText, listener) setupGroups(backgroundDrawable, editText, listener) + setupEmojis(backgroundDrawable, editText) } fun render(state: TextComposerViewState) { @@ -162,6 +164,27 @@ class AutoCompleter @Inject constructor( .build() } + private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { + Autocomplete.on(editText) + .with(CharPolicy(':', true)) + .with(autocompleteEmojiPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: String): Boolean { + editable.clear() + editable + .append(item) + .append(" ") + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) { // Detect last firstChar and remove it var startIndex = editable.lastIndexOf(firstChar) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index 01debac5ed..aa5e79ed29 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -56,26 +56,12 @@ class EmojiSearchResultViewModel @AssistedInject constructor( } private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { - val words = action.queryString.split("\\s".toRegex()) setState { copy( query = action.queryString, // First add emojis with name matching query, sorted by name // Then emojis with keyword matching any of the word in the query, sorted by name - results = dataSource.rawData.emojis - .values - .filter { emojiItem -> - emojiItem.name.contains(action.queryString, true) - } - .sortedBy { it.name } - + dataSource.rawData.emojis - .values - .filter { emojiItem -> - words.fold(true, { prev, word -> - prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } - }) - } - .sortedBy { it.name } + results = dataSource.filterWith(action.queryString) ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index a326828112..873ab90254 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -33,4 +33,25 @@ class EmojiDataSource @Inject constructor( .fromJson(input.bufferedReader().use { it.readText() }) } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + + fun filterWith(query: String): List { + val words = query.split("\\s".toRegex()) + + return rawData.emojis.values + .filter { emojiItem -> + emojiItem.name.contains(query, true) + } + .sortedBy { it.name } + + rawData.emojis.values + .filter { emojiItem -> + words.fold(true, { prev, word -> + prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } + }) + } + .sortedBy { it.name } + } + + fun getQuickReactions(): List { + return listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€").mapNotNull { rawData.emojis[it] } + } } From 9e73e95f55f1206bb2387831e740bab37635dff3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:36:54 +0100 Subject: [PATCH 06/12] Ensure there is never twice the same emoji --- .../vector/riotx/features/reactions/data/EmojiDataSource.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 873ab90254..8abea21667 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,7 +37,7 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) - return rawData.emojis.values + return (rawData.emojis.values .filter { emojiItem -> emojiItem.name.contains(query, true) } @@ -48,7 +48,8 @@ class EmojiDataSource @Inject constructor( prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } }) } - .sortedBy { it.name } + .sortedBy { it.name }) + .distinct() } fun getQuickReactions(): List { From 5fa2acf60b197a945e68f370c1e2d9585d63826d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:46:55 +0100 Subject: [PATCH 07/12] Completion on emoji --- .../riotx/features/reactions/data/EmojiDataSource.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8abea21667..b8ab24da4e 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -53,6 +53,15 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€").mapNotNull { rawData.emojis[it] } + return listOf( + "+1", // πŸ‘ + "-1", // πŸ‘Ž + "grinning", // πŸ˜„ + "tada", // πŸŽ‰ + "confused", // πŸ˜• + "heart", // ❀️ + "rocket", // πŸš€ + "eyes" // πŸ‘€ + ).mapNotNull { rawData.emojis[it] } } } From c8e67f8ab4d6781a0e7833c71b04b3491e55a9cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:06:28 +0100 Subject: [PATCH 08/12] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 15 +++-- .../emoji/AutocompleteEmojiItem.kt | 60 ++++++++++--------- .../home/room/detail/AutoCompleter.kt | 18 ++++-- .../res/layout/item_autocomplete_emoji.xml | 56 +++++++++++++++++ 4 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 vector/src/main/res/layout/item_autocomplete_emoji.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index e0d7bdd888..bef43d8b14 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -23,7 +23,6 @@ import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.reactions.ReactionClickListener import im.vector.riotx.features.reactions.data.EmojiItem -import im.vector.riotx.features.reactions.emojiSearchResultItem import javax.inject.Inject class AutocompleteEmojiController @Inject constructor( @@ -49,16 +48,16 @@ class AutocompleteEmojiController @Inject constructor( return } data.forEach { emojiItem -> - emojiSearchResultItem { + autocompleteEmojiItem { id(emojiItem.name) emojiItem(emojiItem) emojiTypeFace(emojiTypeface) - //currentQuery(data.query) - onClickListener(object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } + onClickListener( + object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt index e5c53315ab..36759f9271 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -16,37 +16,43 @@ package im.vector.riotx.features.autocomplete.emoji -import android.view.View +import android.graphics.Typeface import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.reactions.ReactionClickListener +import im.vector.riotx.features.reactions.data.EmojiItem -//@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) -//abstract class AutocompleteEmojiItem : VectorEpoxyModel() { -// -// @EpoxyAttribute -// var name: CharSequence? = null -// @EpoxyAttribute -// var parameters: CharSequence? = null -// @EpoxyAttribute -// var description: CharSequence? = null -// @EpoxyAttribute -// var clickListener: View.OnClickListener? = null -// -// override fun bind(holder: Holder) { -// holder.view.setOnClickListener(clickListener) -// -// 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) -// } -//} +@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +abstract class AutocompleteEmojiItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var emojiItem: EmojiItem + + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + + @EpoxyAttribute + var onClickListener: ReactionClickListener? = null + + override fun bind(holder: Holder) { + holder.emojiText.text = emojiItem.emoji + holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT + holder.emojiNameText.text = emojiItem.name + holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString()) + + holder.view.setOnClickListener { + onClickListener?.onReactionSelected(emojiItem.emoji) + } + } + + class Holder : VectorEpoxyHolder() { + val emojiText by bind(R.id.itemAutocompleteEmoji) + val emojiNameText by bind(R.id.itemAutocompleteEmojiName) + val emojiKeywordText by bind(R.id.itemAutocompleteEmojiSubname) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 9fe2249450..78ddf80a84 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -172,10 +172,20 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: String): Boolean { - editable.clear() - editable - .append(item) - .append(" ") + // 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 + editable.replace(startIndex, endIndex, item) return true } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000000..650a405f34 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file From 0e5fcd071cffd4621101f33947b09fd92ea491c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:21:56 +0100 Subject: [PATCH 09/12] Completion on emoji: display the first 50 results --- .../emoji/AutocompleteEmojiController.kt | 36 ++++++++++++------- .../emoji/AutocompleteMoreResultItem.kt | 28 +++++++++++++++ .../home/room/detail/AutoCompleter.kt | 2 +- .../layout/item_autocomplete_more_result.xml | 9 +++++ vector/src/main/res/values/strings_riotX.xml | 2 ++ 5 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt create mode 100644 vector/src/main/res/layout/item_autocomplete_more_result.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index bef43d8b14..010b362b68 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -47,18 +47,26 @@ class AutocompleteEmojiController @Inject constructor( if (data.isNullOrEmpty()) { return } - data.forEach { emojiItem -> - autocompleteEmojiItem { - id(emojiItem.name) - emojiItem(emojiItem) - emojiTypeFace(emojiTypeface) - onClickListener( - object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } - ) + data + .take(MAX) + .forEach { emojiItem -> + autocompleteEmojiItem { + id(emojiItem.name) + emojiItem(emojiItem) + emojiTypeFace(emojiTypeface) + onClickListener( + object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } + ) + } + } + + if (data.size > MAX) { + autocompleteMoreResultItem { + id("more_result") } } } @@ -67,4 +75,8 @@ class AutocompleteEmojiController @Inject constructor( super.onDetachedFromRecyclerView(recyclerView) fontProvider.removeListener(fontProviderListener) } + + companion object { + const val MAX = 50 + } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt new file mode 100644 index 0000000000..844cc96035 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.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.riotx.features.autocomplete.emoji + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result) +abstract class AutocompleteMoreResultItem : VectorEpoxyModel() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 78ddf80a84..609e7e2183 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -166,7 +166,7 @@ class AutoCompleter @Inject constructor( private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) - .with(CharPolicy(':', true)) + .with(CharPolicy(':', false)) .with(autocompleteEmojiPresenter) .with(ELEVATION) .with(backgroundDrawable) diff --git a/vector/src/main/res/layout/item_autocomplete_more_result.xml b/vector/src/main/res/layout/item_autocomplete_more_result.xml new file mode 100644 index 0000000000..d04f515ed0 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_more_result.xml @@ -0,0 +1,9 @@ + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1502740112..317511b921 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,4 +18,6 @@ Current device Other devices + Limited results, please type more letters… + From 92d7ebe94f6be8f0fcb25eba4182da1aefaaa05c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:22:43 +0100 Subject: [PATCH 10/12] Update changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 560350dddf..b11be910db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Improvements πŸ™Œ: - Introduce developer mode in the settings (#796) - Improve devices list screen - Add settings for rageshake sensibility + - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) Other changes: - From 9ecceafb960d2155f423c0a40099b8bb0d37aa64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 21:56:35 +0100 Subject: [PATCH 11/12] Move comment --- .../riotx/features/reactions/EmojiSearchResultViewModel.kt | 2 -- .../im/vector/riotx/features/reactions/data/EmojiDataSource.kt | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index aa5e79ed29..8aa03d9b22 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -59,8 +59,6 @@ class EmojiSearchResultViewModel @AssistedInject constructor( setState { copy( query = action.queryString, - // First add emojis with name matching query, sorted by name - // Then emojis with keyword matching any of the word in the query, sorted by name results = dataSource.filterWith(action.queryString) ) } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index b8ab24da4e..8a279a7d4d 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,11 +37,13 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) + // First add emojis with name matching query, sorted by name return (rawData.emojis.values .filter { emojiItem -> emojiItem.name.contains(query, true) } .sortedBy { it.name } + + // Then emojis with keyword matching any of the word in the query, sorted by name rawData.emojis.values .filter { emojiItem -> words.fold(true, { prev, word -> @@ -49,6 +51,7 @@ class EmojiDataSource @Inject constructor( }) } .sortedBy { it.name }) + // and ensure they will not be present twice .distinct() } From 448552d287e79f1d4035ef918668c504a9d9838f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 10:42:01 +0100 Subject: [PATCH 12/12] Move list of Quick Emoji to Emoji Data Source --- .../action/MessageActionsViewModel.kt | 6 ++-- .../reactions/data/EmojiDataSource.kt | 31 +++++++++++++------ .../res/layout/item_autocomplete_emoji.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index d08a891081..aad73e12f4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.reactions.data.EmojiDataSource import java.text.SimpleDateFormat import java.util.* @@ -101,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } companion object : MvRxViewModelFactory { - - val quickEmojis = listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€") - @JvmStatic override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() @@ -161,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted RxRoom(room) .liveAnnotationSummary(eventId) .map { annotations -> - quickEmojis.map { emoji -> + EmojiDataSource.quickEmojis.map { emoji -> ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8a279a7d4d..9317c645c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -34,6 +34,8 @@ class EmojiDataSource @Inject constructor( } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + private val quickReactions = mutableListOf() + fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) @@ -56,15 +58,24 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf( - "+1", // πŸ‘ - "-1", // πŸ‘Ž - "grinning", // πŸ˜„ - "tada", // πŸŽ‰ - "confused", // πŸ˜• - "heart", // ❀️ - "rocket", // πŸš€ - "eyes" // πŸ‘€ - ).mapNotNull { rawData.emojis[it] } + if (quickReactions.isEmpty()) { + listOf( + "+1", // πŸ‘ + "-1", // πŸ‘Ž + "grinning", // πŸ˜„ + "tada", // πŸŽ‰ + "confused", // πŸ˜• + "heart", // ❀️ + "rocket", // πŸš€ + "eyes" // πŸ‘€ + ) + .mapNotNullTo(quickReactions) { rawData.emojis[it] } + } + + return quickReactions + } + + companion object { + val quickEmojis = listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€") } } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml index 650a405f34..c34ab0d452 100644 --- a/vector/src/main/res/layout/item_autocomplete_emoji.xml +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 317511b921..3e8485ebcc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,6 +18,6 @@ Current device Other devices - Limited results, please type more letters… + Showing only the first results, type more letters…