diff --git a/CHANGES.md b/CHANGES.md index c53dc349d2..46097b72ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,9 @@ Features ✨: - Handle long click on room in the room list (#395) Improvements 🙌: + - Search reaction by name or keyword in emoji picker - Handle code tags (#567) + - Support spoiler messages Other changes: - Markdown set to off by default (#412) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 87ed61c695..35e646b316 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -60,6 +60,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.reactions.EmojiReactionPickerActivity +import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity @@ -200,6 +201,8 @@ interface ScreenComponent { fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) + fun inject(emojiSearchResultFragment: EmojiSearchResultFragment) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt new file mode 100644 index 0000000000..3de555f66e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.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.core.utils + +import java.net.URL + +fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 2d4cecdf26..7d745b925b 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -37,5 +37,6 @@ enum class Command(val command: String, val parameters: String, @StringRes val d 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); + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), + SPOILER("/spoiler", "", R.string.command_description_spoiler); } diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 9cf6510fdb..a9c20a9ec5 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -56,11 +56,12 @@ object CommandParser { return ParsedCommand.ErrorEmptySlashCommand } - when (val slashCommand = messageParts.first()) { + + return when (val slashCommand = messageParts.first()) { Command.CHANGE_DISPLAY_NAME.command -> { val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() - return if (newDisplayName.isNotEmpty()) { + if (newDisplayName.isNotEmpty()) { ParsedCommand.ChangeDisplayName(newDisplayName) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) @@ -69,7 +70,7 @@ object CommandParser { Command.TOPIC.command -> { val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() - return if (newTopic.isNotEmpty()) { + if (newTopic.isNotEmpty()) { ParsedCommand.ChangeTopic(newTopic) } else { ParsedCommand.ErrorSyntax(Command.TOPIC) @@ -78,12 +79,12 @@ object CommandParser { Command.EMOTE.command -> { val message = textMessage.substring(Command.EMOTE.command.length).trim() - return ParsedCommand.SendEmote(message) + ParsedCommand.SendEmote(message) } Command.JOIN_ROOM.command -> { val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim() - return if (roomAlias.isNotEmpty()) { + if (roomAlias.isNotEmpty()) { ParsedCommand.JoinRoom(roomAlias) } else { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) @@ -92,14 +93,14 @@ object CommandParser { Command.PART.command -> { val roomAlias = textMessage.substring(Command.PART.command.length).trim() - return if (roomAlias.isNotEmpty()) { + if (roomAlias.isNotEmpty()) { ParsedCommand.PartRoom(roomAlias) } else { ParsedCommand.ErrorSyntax(Command.PART) } } Command.INVITE.command -> { - return if (messageParts.size == 2) { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -112,7 +113,7 @@ object CommandParser { } } Command.KICK_USER.command -> { - return if (messageParts.size >= 2) { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val reason = textMessage.substring(Command.KICK_USER.command.length @@ -128,7 +129,7 @@ object CommandParser { } } Command.BAN_USER.command -> { - return if (messageParts.size >= 2) { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val reason = textMessage.substring(Command.BAN_USER.command.length @@ -144,7 +145,7 @@ object CommandParser { } } Command.UNBAN_USER.command -> { - return if (messageParts.size == 2) { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -157,7 +158,7 @@ object CommandParser { } } Command.SET_USER_POWER_LEVEL.command -> { - return if (messageParts.size == 3) { + if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val powerLevelsAsString = messageParts[2] @@ -177,7 +178,7 @@ object CommandParser { } } Command.RESET_USER_POWER_LEVEL.command -> { - return if (messageParts.size == 2) { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -190,7 +191,7 @@ object CommandParser { } } Command.MARKDOWN.command -> { - return if (messageParts.size == 2) { + if (messageParts.size == 2) { when { "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) "off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false) @@ -201,15 +202,20 @@ object CommandParser { } } Command.CLEAR_SCALAR_TOKEN.command -> { - return if (messageParts.size == 1) { + if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } + Command.SPOILER.command -> { + val message = textMessage.substring(Command.SPOILER.command.length).trim() + + ParsedCommand.SendSpoiler(message) + } else -> { // Unknown command - return ParsedCommand.ErrorUnknownSlashCommand(slashCommand) + ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index f6bbed2889..02f5abe540 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -45,4 +45,5 @@ sealed class ParsedCommand { class ChangeDisplayName(val displayName: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() + class SendSpoiler(val message: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b1c6aa02fb..2436b7a8b4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -49,6 +49,7 @@ import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.subscribeLogError @@ -66,6 +67,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider, private val session: Session ) : VectorViewModel(initialState) { @@ -327,6 +329,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } + is ParsedCommand.SendSpoiler -> { + room.sendFormattedTextMessage( + "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", + "${slashCommandResult.message}" + ) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() + } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) popDraft() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index 564adc50f2..45a6e2e743 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -24,6 +24,7 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.utils.isValidUrl import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers @@ -49,12 +50,12 @@ abstract class MessageTextItem : AbsMessageItem() { private val mvmtMethod = BetterLinkMovementMethod.newInstance().also { it.setOnLinkClickListener { _, url -> // Return false to let android manage the click on the link, or true if the link is handled by the application - urlClickCallback?.onUrlClicked(url) == true + url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true } // We need also to fix the case when long click on link will trigger long click on cell it.setOnLinkLongClickListener { tv, url -> // Long clicks are handled by parent, return true to block android to do something with url - if (urlClickCallback?.onUrlLongClicked(url) == true) { + if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) true } else { diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index dc9e21e440..327677ade1 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -58,5 +58,7 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context .addHandler(FontTagHandler()) .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) .addHandler(MxReplyTagHandler()) + // FIXME (P3) SpanHandler is not recreated when theme is change and it depends on theme colors + .addHandler(SpanHandler(context)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt new file mode 100644 index 0000000000..dbc09cf0a9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt @@ -0,0 +1,49 @@ +/* + * 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.html + +import android.content.Context +import im.vector.riotx.R +import im.vector.riotx.features.themes.ThemeUtils +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler + +class SpanHandler(context: Context) : TagHandler() { + + override fun supportedTags() = listOf("span") + + private val spoilerBgColorHidden: Int = ThemeUtils.getColor(context, R.attr.vctr_spoiler_background_color) + private val spoilerBgColorRevealed: Int = ThemeUtils.getColor(context, R.attr.vctr_markdown_block_background_color) + + private val textColor: Int = ThemeUtils.getColor(context, R.attr.riotx_text_primary) + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + val mxSpoiler = tag.attributes()["data-mx-spoiler"] + if (mxSpoiler != null) { + SpannableBuilder.setSpans( + visitor.builder(), + SpoilerSpan(spoilerBgColorHidden, spoilerBgColorRevealed, textColor), + tag.start(), + tag.end() + ) + } else { + // default thing? + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.kt new file mode 100644 index 0000000000..d8236f0746 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.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.riotx.features.html + +import android.graphics.Color +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.view.View + +class SpoilerSpan(private val bgColorHidden: Int, + private val bgColorRevealed: Int, + private val textColor: Int) : ClickableSpan() { + + override fun onClick(widget: View) { + isHidden = !isHidden + widget.invalidate() + } + + private var isHidden = true + + override fun updateDrawState(tp: TextPaint) { + if (isHidden) { + tp.bgColor = bgColorHidden + tp.color = Color.TRANSPARENT + } else { + tp.bgColor = bgColorRevealed + tp.color = textColor + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt index 8aec8231db..77cad5643f 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt @@ -36,12 +36,10 @@ class EmojiChooserFragment : VectorBaseFragment() { viewModel = activity?.run { ViewModelProviders.of(this, viewModelFactory).get(EmojiChooserViewModel::class.java) } ?: throw Exception("Invalid Activity") - viewModel.initWithContect(context!!) + viewModel.initWithContext(context!!) (view as? RecyclerView)?.let { it.adapter = viewModel.adapter it.adapter?.notifyDataSetChanged() } - -// val ds = EmojiDataSource(this.context!!) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt index 16aecd0906..bbde2ac54c 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt @@ -39,7 +39,7 @@ class EmojiChooserViewModel @Inject constructor() : ViewModel() { } } - fun initWithContect(context: Context) { + fun initWithContext(context: Context) { // TODO load async val emojiDataSource = EmojiDataSource(context) emojiSourceLiveData.value = emojiDataSource diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt index 0a4e05a4c8..a442f5f77e 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt @@ -25,15 +25,22 @@ import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayout +import com.jakewharton.rxbinding3.widget.queryTextChanges import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* +import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject /** @@ -41,9 +48,9 @@ import javax.inject.Inject * TODO: Loading indicator while getting emoji data source? * TODO: migrate to MvRx * TODO: Finish Refactor to vector base activity - * TODO: Move font request to app */ -class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvider.FontProviderListener { +class EmojiReactionPickerActivity : VectorBaseActivity(), + EmojiCompatFontProvider.FontProviderListener { private lateinit var tabLayout: TabLayout @@ -57,6 +64,8 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider + val searchResultViewModel: EmojiSearchResultViewModel by viewModel() + private var tabLayoutSelectionListener = object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab) { } @@ -121,10 +130,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide finish() } } + + supportFragmentManager.findFragmentById(R.id.fragment)?.view?.isVisible = true + supportFragmentManager.findFragmentById(R.id.searchFragment)?.view?.isInvisible = true + tabLayout.isVisible = true } override fun compatibilityFontUpdate(typeface: Typeface?) { EmojiDrawView.configureTextPaint(this, typeface) + searchResultViewModel.dataSource } override fun onDestroy() { @@ -137,11 +151,11 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide inflater.inflate(getMenuRes(), menu) val searchItem = menu.findItem(R.id.search) - (searchItem.actionView as? SearchView)?.let { + (searchItem.actionView as? SearchView)?.let { searchView -> searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { - it.isIconified = false - it.requestFocusFromTouch() + searchView.isIconified = false + searchView.requestFocusFromTouch() // we want to force the tool bar as visible even if hidden with scroll flags findViewById(R.id.toolbar)?.minimumHeight = getActionBarSize() return true @@ -150,12 +164,20 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean { // when back, clear all search findViewById(R.id.toolbar)?.minimumHeight = 0 - it.setQuery("", true) + searchView.setQuery("", true) return true } }) - } + searchView.queryTextChanges() + .throttleWithTimeout(600, TimeUnit.MILLISECONDS) + .doOnError { err -> Timber.e(err) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { query -> + onQueryText(query.toString()) + } + .disposeOnDestroy() + } return true } @@ -171,6 +193,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide } } + private fun onQueryText(query: String) { + if (query.isEmpty()) { + supportFragmentManager.findFragmentById(R.id.fragment)?.view?.isVisible = true + supportFragmentManager.findFragmentById(R.id.searchFragment)?.view?.isInvisible = true + tabLayout.isVisible = true + } else { + tabLayout.isVisible = false + supportFragmentManager.findFragmentById(R.id.fragment)?.view?.isInvisible = true + supportFragmentManager.findFragmentById(R.id.searchFragment)?.view?.isVisible = true + searchResultViewModel.updateQuery(query) + } + } + companion object { const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt new file mode 100644 index 0000000000..3e8f1c9769 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt @@ -0,0 +1,80 @@ +/* + * 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.reactions + +import android.graphics.Typeface +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import javax.inject.Inject + +class EmojiSearchResultController @Inject constructor(val stringProvider: StringProvider, + 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: ReactionClickListener? = null + + override fun buildModels(data: EmojiSearchResultViewState?) { + val results = data?.results ?: return + + if (results.isEmpty()) { + if (data.query.isEmpty()) { + // display 'Type something to find' + genericFooterItem { + id("type.query.item") + text(stringProvider.getString(R.string.reaction_search_type_hint)) + } + } else { + // Display no search Results + genericFooterItem { + id("no.results.item") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } + } else { + // Build the search results + results.forEach { + emojiSearchResultItem { + id(it.name) + emojiItem(it) + emojiTypeFace(emojiTypeface) + currentQuery(data.query) + onClickListener(listener) + } + } + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + fontProvider.removeListener(fontProviderListener) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt new file mode 100644 index 0000000000..a4f443de1e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt @@ -0,0 +1,69 @@ +/* + * 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.reactions + +import android.os.Bundle +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.EpoxyRecyclerView +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject + +class EmojiSearchResultFragment : VectorBaseFragment() { + + override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy + + val viewModel: EmojiSearchResultViewModel by activityViewModel() + + var sharedViewModel: EmojiChooserViewModel? = null + + @Inject lateinit var epoxyController: EmojiSearchResultController + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + sharedViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(EmojiChooserViewModel::class.java) + + epoxyController.listener = object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + sharedViewModel?.selectedReaction = reaction + sharedViewModel?.navigateEvent?.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH) + } + } + + val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + val epoxyRecyclerView = view as? EpoxyRecyclerView ?: return + epoxyRecyclerView.layoutManager = lmgr + val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, lmgr.orientation) + epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + epoxyRecyclerView.setController(epoxyController) + } + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt new file mode 100644 index 0000000000..1b117035d9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt @@ -0,0 +1,58 @@ +/* + * 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.reactions + +import android.graphics.Typeface +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_emoji_result) +abstract class EmojiSearchResultItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + lateinit var emojiItem: EmojiDataSource.EmojiItem + + @EpoxyAttribute + var currentQuery: String? = null + + @EpoxyAttribute + var onClickListener: ReactionClickListener? = null + + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + + override fun bind(holder: Holder) { + super.bind(holder) + // TODO use query string to highlight the matched query in name and keywords? + holder.emojiText.text = emojiItem.emojiString() + holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT + holder.emojiNameText.text = emojiItem.name + holder.emojiKeywordText.text = emojiItem.keywords?.joinToString(", ") + holder.view.setOnClickListener { + onClickListener?.onReactionSelected(emojiItem.emojiString()) + } + } + + class Holder : VectorEpoxyHolder() { + val emojiText by bind(R.id.item_emoji_tv) + val emojiNameText by bind(R.id.item_emoji_name) + val emojiKeywordText by bind(R.id.item_emoji_keyword) + } +} 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 new file mode 100644 index 0000000000..d11c25a1e3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.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.reactions + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.riotx.core.platform.VectorViewModel + +data class EmojiSearchResultViewState( + val query: String = "", + val results: List = emptyList() +) : MvRxState + +class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState) + : VectorViewModel(initialState) { + + fun updateQuery(queryString: String) { + setState { + copy( + query = queryString, + results = dataSource.rawData?.emojis?.toList() + ?.map { it.second } + ?.filter { + it.name.contains(queryString, true) + || queryString.split("\\s".toRegex()).fold(true, { prev, q -> + prev && (it.keywords?.any { it.contains(q, true) } ?: false) + }) + } ?: emptyList() + ) + } + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { + // TODO get the data source from activity? share it with other fragment + return EmojiSearchResultViewModel(EmojiDataSource(viewModelContext.activity), state) + } + } +} diff --git a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml index ccaf149732..d56198d341 100644 --- a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml +++ b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml @@ -14,6 +14,14 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:layout="@layout/emoji_chooser_fragment" /> + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_emoji_reaction_picker.xml b/vector/src/main/res/menu/menu_emoji_reaction_picker.xml index 98242f57bb..87135d64ea 100644 --- a/vector/src/main/res/menu/menu_emoji_reaction_picker.xml +++ b/vector/src/main/res/menu/menu_emoji_reaction_picker.xml @@ -4,6 +4,7 @@ diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index e9a4296add..c30a1d99d9 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -34,6 +34,7 @@ + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 83ce65783f..be0d97c147 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1168,6 +1168,8 @@ Changes your display nickname On/Off markdown To fix Matrix Apps management + Sends the given message as a spoiler + Spoiler Markdown has been enabled. Markdown has been disabled. @@ -1528,6 +1530,7 @@ Why choose Riot.im? Add Reaction View Reactions Reactions + Type keywords to find a reaction. Event deleted by user Event moderated by room admin diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f09cb0c874..f61a89482a 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -101,6 +101,7 @@ #CCC3C3C3 @color/accent_color_dark @android:color/black + #FFFFFFFF #565656 diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 1da010b8ff..aa343a11fc 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -101,6 +101,7 @@ #333C3C3C @color/accent_color_light #FFEEEEEE + #FF000000 #FFF2F2F2