Merge branch 'develop' into feature/room_list_actions

This commit is contained in:
ganfra 2019-11-04 17:17:43 +01:00
commit 945e5d5a74
25 changed files with 536 additions and 29 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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
}
}

View File

@ -37,5 +37,6 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token);
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler);
}

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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<RoomDetailViewState>(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})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
popDraft()

View File

@ -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<MessageTextItem.Holder>() {
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 {

View File

@ -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))
}
}

View File

@ -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?
}
}
}

View File

@ -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
}
}
}

View File

@ -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!!)
}
}

View File

@ -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

View File

@ -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<Toolbar>(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<Toolbar>(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"

View File

@ -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<EmojiSearchResultViewState>() {
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)
}
}

View File

@ -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)
}
}

View File

@ -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<EmojiSearchResultItem.Holder>() {
@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<TextView>(R.id.item_emoji_tv)
val emojiNameText by bind<TextView>(R.id.item_emoji_name)
val emojiKeywordText by bind<TextView>(R.id.item_emoji_keyword)
}
}

View File

@ -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<EmojiDataSource.EmojiItem> = emptyList()
) : MvRxState
class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState)
: VectorViewModel<EmojiSearchResultViewState>(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<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
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)
}
}
}

View File

@ -14,6 +14,14 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:layout="@layout/emoji_chooser_fragment" />
<fragment
android:id="@+id/searchFragment"
android:name="im.vector.riotx.features.reactions.EmojiSearchResultFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:visibility="invisible" />
<com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:minHeight="44dp">
<!-- size in dp, because we do not want the display to be impacted by font size setting -->
<TextView
android:id="@+id/item_emoji_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:textSize="25sp"
tools:ignore="SpUsage"
android:textColor="?android:textColorPrimary"
tools:text="@sample/reactions.json/data/reaction" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:id="@+id/item_emoji_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textStyle="bold"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
tools:text="Smiley Face" />
<TextView
android:id="@+id/item_emoji_keyword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="14sp"
android:maxLines="2"
android:textColor="?android:textColorPrimary"
tools:text="Smile, foo, bar" />
</LinearLayout>
</LinearLayout>

View File

@ -4,6 +4,7 @@
<item
android:id="@+id/search"
android:icon="@drawable/ic_search_white"
app:iconTint="?riotx_text_primary"
android:title="@string/search"
app:actionViewClass="android.widget.SearchView"
app:showAsAction="collapseActionView|ifRoom" />

View File

@ -34,6 +34,7 @@
<attr name="vctr_unread_marker_line_color" format="color" />
<attr name="vctr_markdown_block_background_color" format="color" />
<attr name="vctr_room_activity_divider_color" format="color" />
<attr name="vctr_spoiler_background_color" format="color" />
<!-- tab bar colors -->
<attr name="vctr_tab_bar_inverted_background_color" format="color" />

View File

@ -1168,6 +1168,8 @@
<string name="command_description_nick">Changes your display nickname</string>
<string name="command_description_markdown">On/Off markdown</string>
<string name="command_description_clear_scalar_token">To fix Matrix Apps management</string>
<string name="command_description_spoiler">Sends the given message as a spoiler</string>
<string name="spoiler">Spoiler</string>
<string name="markdown_has_been_enabled">Markdown has been enabled.</string>
<string name="markdown_has_been_disabled">Markdown has been disabled.</string>
@ -1528,6 +1530,7 @@ Why choose Riot.im?
<string name="message_add_reaction">Add Reaction</string>
<string name="message_view_reaction">View Reactions</string>
<string name="reactions">Reactions</string>
<string name="reaction_search_type_hint">Type keywords to find a reaction.</string>
<string name="event_redacted_by_user_reason">Event deleted by user</string>
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>

View File

@ -101,6 +101,7 @@
<item name="vctr_search_mode_room_name_text_color">#CCC3C3C3</item>
<item name="vctr_unread_marker_line_color">@color/accent_color_dark</item>
<item name="vctr_markdown_block_background_color">@android:color/black</item>
<item name="vctr_spoiler_background_color">#FFFFFFFF</item>
<item name="vctr_room_activity_divider_color">#565656</item>
<!-- tab bar colors -->

View File

@ -101,6 +101,7 @@
<item name="vctr_search_mode_room_name_text_color">#333C3C3C</item>
<item name="vctr_unread_marker_line_color">@color/accent_color_light</item>
<item name="vctr_markdown_block_background_color">#FFEEEEEE</item>
<item name="vctr_spoiler_background_color">#FF000000</item>
<item name="vctr_room_activity_divider_color">#FFF2F2F2</item>
<!-- tab bar colors -->