forked from GitHub-Mirror/riotX-android
Refactoring / revert reaction module to package in main module
This commit is contained in:
@ -39,6 +39,10 @@
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity android:name=".features.media.VideoMediaViewerActivity" />
|
||||
|
||||
<activity
|
||||
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
|
||||
android:label="@string/title_activity_emoji_reaction_picker" />
|
||||
|
||||
<service
|
||||
android:name=".core.services.CallService"
|
||||
android:exported="false" />
|
||||
|
@ -199,7 +199,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
* MENU MANAGEMENT
|
||||
* ========================================================================================== */
|
||||
|
||||
final override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val menuRes = getMenuRes()
|
||||
|
||||
if (menuRes != -1) {
|
||||
|
@ -52,7 +52,7 @@ import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
|
||||
class EmojiChooserFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = EmojiChooserFragment()
|
||||
}
|
||||
|
||||
private lateinit var viewModel: EmojiChooserViewModel
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.emoji_chooser_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = activity?.run {
|
||||
ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
viewModel.initWithContect(context!!)
|
||||
(view as? RecyclerView)?.let {
|
||||
it.adapter = viewModel.adapter
|
||||
it.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// val ds = EmojiDataSource(this.context!!)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class EmojiChooserViewModel : ViewModel() {
|
||||
|
||||
var adapter: EmojiRecyclerAdapter? = null
|
||||
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
|
||||
|
||||
val currentSection: MutableLiveData<Int> = MutableLiveData()
|
||||
|
||||
fun initWithContect(context: Context) {
|
||||
//TODO load async
|
||||
val emojiDataSource = EmojiDataSource(context)
|
||||
emojiSourceLiveData.value = emojiDataSource
|
||||
adapter = EmojiRecyclerAdapter(emojiDataSource)
|
||||
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
|
||||
override fun firstVisibleSectionChange(section: Int) {
|
||||
currentSection.value = section
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToSection(sectionIndex: Int) {
|
||||
adapter?.scrollToSection(sectionIndex)
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.riotredesign.R
|
||||
import java.io.InputStreamReader
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
|
||||
|
||||
|
||||
class EmojiDataSource(val context: Context) {
|
||||
|
||||
var rawData: EmojiData? = null
|
||||
|
||||
init {
|
||||
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
|
||||
val moshi = Moshi.Builder().build()
|
||||
val jsonAdapter = moshi.adapter(EmojiData::class.java)
|
||||
val inputAsString = input.bufferedReader().use { it.readText() }
|
||||
this.rawData = jsonAdapter.fromJson(inputAsString)
|
||||
// this.rawData = mb.fr(InputStreamReader(it), EmojiData::class.java)
|
||||
}
|
||||
}
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EmojiData(val categories: List<EmojiCategory>,
|
||||
val emojis: Map<String, EmojiItem>,
|
||||
val aliases: Map<String, String>)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EmojiCategory(val id: String, val name: String, val emojis: List<String>)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EmojiItem(
|
||||
@Json(name = "a") val name: String,
|
||||
@Json(name = "b") val unicode: String,
|
||||
@Json(name = "j") val keywords: List<String>?,
|
||||
val k: List<String>?) {
|
||||
|
||||
var _emojiText: String? = null
|
||||
|
||||
fun emojiString() : String {
|
||||
if (_emojiText == null) {
|
||||
val utf8Text = unicode.split("-").joinToString("") { "\\u${it}" }//"\u0048\u0065\u006C\u006C\u006F World"
|
||||
_emojiText = fromUnicode(utf8Text)
|
||||
}
|
||||
return _emojiText!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromUnicode(unicode: String): String {
|
||||
val str = unicode.replace("\\", "")
|
||||
val arr = str.split("u".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val text = StringBuffer()
|
||||
for (i in 1 until arr.size) {
|
||||
val hexVal = Integer.parseInt(arr[i], 16)
|
||||
text.append(Character.toChars(hexVal))
|
||||
}
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// name: 'a',
|
||||
// unified: 'b',
|
||||
// non_qualified: 'c',
|
||||
// has_img_apple: 'd',
|
||||
// has_img_google: 'e',
|
||||
// has_img_twitter: 'f',
|
||||
// has_img_emojione: 'g',
|
||||
// has_img_facebook: 'h',
|
||||
// has_img_messenger: 'i',
|
||||
// keywords: 'j',
|
||||
// sheet: 'k',
|
||||
// emoticons: 'l',
|
||||
// text: 'm',
|
||||
// short_names: 'n',
|
||||
// added_in: 'o',
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
/**
|
||||
* We want to use a custom view for rendering an emoji.
|
||||
* With generic textview, the performance in the recycler view are very bad
|
||||
*/
|
||||
class EmojiDrawView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
var mLayout: StaticLayout? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// var _mySpacing = 0f
|
||||
|
||||
var emoji: String? = null
|
||||
// set(value) {
|
||||
// if (value != null) {
|
||||
// EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
|
||||
// mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||
// if (value != field) invalidate()
|
||||
// EmojiRecyclerAdapter.endTraceSession()
|
||||
// } else {
|
||||
// mLayout = null
|
||||
//// if (value != field) invalidate()
|
||||
// }
|
||||
// field = value
|
||||
// }
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw")
|
||||
super.onDraw(canvas)
|
||||
canvas?.save()
|
||||
val space = abs((width - emojiSize) / 2f)
|
||||
if (mLayout == null) {
|
||||
// canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
|
||||
} else {
|
||||
canvas?.translate(space, space)
|
||||
mLayout!!.draw(canvas)
|
||||
}
|
||||
canvas?.restore()
|
||||
EmojiRecyclerAdapter.endTraceSession()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val tPaint = TextPaint()
|
||||
|
||||
var emojiSize = 40
|
||||
|
||||
fun configureTextPaint(context: Context, typeface: Typeface?) {
|
||||
tPaint.isAntiAlias = true;
|
||||
tPaint.textSize = 24 * context.resources.displayMetrics.density
|
||||
tPaint.color = Color.LTGRAY
|
||||
typeface?.let {
|
||||
tPaint.typeface = it
|
||||
}
|
||||
|
||||
emojiSize = tPaint.measureText("😅").toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.provider.FontRequest
|
||||
import androidx.core.provider.FontsContractCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Loading indicator while getting emoji data source?
|
||||
* TODO: migrate to maverick
|
||||
* TODO: Finish Refactor to vector base activity
|
||||
* TODO: Move font request to app
|
||||
*/
|
||||
class EmojiReactionPickerActivity : VectorBaseActivity() {
|
||||
|
||||
private lateinit var tabLayout: TabLayout
|
||||
|
||||
lateinit var viewModel: EmojiChooserViewModel
|
||||
|
||||
private var mHandler: Handler? = null
|
||||
|
||||
override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker
|
||||
|
||||
override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker
|
||||
|
||||
private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener<TabLayout.Tab> {
|
||||
override fun onTabReselected(p0: TabLayout.Tab) {
|
||||
}
|
||||
|
||||
override fun onTabUnselected(p0: TabLayout.Tab) {
|
||||
}
|
||||
|
||||
override fun onTabSelected(p0: TabLayout.Tab) {
|
||||
viewModel.scrollToSection(p0.position)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getFontThreadHandler(): Handler {
|
||||
if (mHandler == null) {
|
||||
val handlerThread = HandlerThread("fonts")
|
||||
handlerThread.start()
|
||||
mHandler = Handler(handlerThread.looper)
|
||||
}
|
||||
return mHandler!!
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
|
||||
configureToolbar()
|
||||
|
||||
requestEmojivUnicode10CompatibleFont()
|
||||
|
||||
tabLayout = findViewById(R.id.tabs)
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
||||
|
||||
viewModel.emojiSourceLiveData.observe(this, Observer {
|
||||
it.rawData?.categories?.let { categories ->
|
||||
for (category in categories) {
|
||||
val s = category.emojis[0]
|
||||
tabLayout.addTab(tabLayout.newTab().setText(it.rawData!!.emojis[s]!!.emojiString()))
|
||||
}
|
||||
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
viewModel.currentSection.observe(this, Observer { section ->
|
||||
section?.let {
|
||||
tabLayout.removeOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
tabLayout.getTabAt(it)?.select()
|
||||
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun requestEmojivUnicode10CompatibleFont() {
|
||||
val fontRequest = FontRequest(
|
||||
"com.google.android.gms.fonts",
|
||||
"com.google.android.gms",
|
||||
"Noto Color Emoji Compat",
|
||||
R.array.com_google_android_gms_fonts_certs
|
||||
)
|
||||
|
||||
EmojiDrawView.configureTextPaint(this, null)
|
||||
val callback = object : FontsContractCompat.FontRequestCallback() {
|
||||
|
||||
override fun onTypefaceRetrieved(typeface: Typeface) {
|
||||
EmojiDrawView.configureTextPaint(this@EmojiReactionPickerActivity, typeface)
|
||||
}
|
||||
|
||||
override fun onTypefaceRequestFailed(reason: Int) {
|
||||
Timber.e("Failed to load Emoji Compatible font, reason:$reason")
|
||||
}
|
||||
}
|
||||
|
||||
FontsContractCompat.requestFont(this, fontRequest, callback, getFontThreadHandler())
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater: MenuInflater = menuInflater
|
||||
inflater.inflate(getMenuRes(), menu)
|
||||
|
||||
val searchItem = menu.findItem(R.id.search)
|
||||
(searchItem.actionView as? SearchView)?.let {
|
||||
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
||||
it.isIconified = false
|
||||
it.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
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
||||
// when back, clear all search
|
||||
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = 0
|
||||
it.setQuery("", true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
//TODO move to ThemeUtils when core module is created
|
||||
private fun getActionBarSize(): Int {
|
||||
return try {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(R.attr.actionBarSize, typedValue, true)
|
||||
TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
|
||||
} catch (e: Exception) {
|
||||
//Timber.e(e, "Unable to get color")
|
||||
TypedValue.complexToDimensionPixelSize(56, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun intent(context: Context): Intent {
|
||||
val intent = Intent(context, EmojiReactionPickerActivity::class.java)
|
||||
// intent.putExtra(EXTRA_MATRIX_ID, matrixID)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Trace
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import im.vector.riotredesign.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Configure Span using available width and emoji size
|
||||
* TODO: Search
|
||||
* TODO: Performances
|
||||
* TODO: Scroll to section - Find a way to snap section to the top
|
||||
*/
|
||||
class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
var mRecyclerView: RecyclerView? = null
|
||||
|
||||
|
||||
var currentFirstVisibleSection = 0
|
||||
|
||||
enum class ScrollState {
|
||||
IDLE,
|
||||
DRAGGING,
|
||||
SETTLING,
|
||||
UNKNWON
|
||||
}
|
||||
|
||||
private var scrollState = ScrollState.UNKNWON
|
||||
private var isFastScroll = false
|
||||
|
||||
val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>()
|
||||
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
this.mRecyclerView = recyclerView
|
||||
|
||||
val gridLayoutManager = GridLayoutManager(recyclerView.context, 8)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (isSection(position)) gridLayoutManager.spanCount else 1
|
||||
}
|
||||
}.apply {
|
||||
isSpanIndexCacheEnabled = true
|
||||
}
|
||||
recyclerView.layoutManager = gridLayoutManager
|
||||
|
||||
recyclerView.itemAnimator = DefaultItemAnimator().apply {
|
||||
supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
recyclerView.setHasFixedSize(true)
|
||||
//Default is 5 but we have lots of views for emojis
|
||||
recyclerView.recycledViewPool
|
||||
.setMaxRecycledViews(R.layout.grid_item_emoji, 300)
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
this.mRecyclerView = null
|
||||
recyclerView.removeOnScrollListener(scrollListener)
|
||||
staticLayoutCache.clear()
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
fun scrollToSection(section: Int) {
|
||||
if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) {
|
||||
//ignore
|
||||
return
|
||||
}
|
||||
//mRecyclerView?.smoothScrollToPosition(getSectionOffset(section) - 1)
|
||||
//TODO Snap section header to top
|
||||
mRecyclerView?.scrollToPosition(getSectionOffset(section) - 1)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
beginTraceSession("MyAdapter.onCreateViewHolder")
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val itemView = inflater.inflate(viewType, parent, false)
|
||||
val viewHolder = when (viewType) {
|
||||
R.layout.grid_section_header -> SectionViewHolder(itemView)
|
||||
else -> EmojiViewHolder(itemView)
|
||||
}
|
||||
endTraceSession()
|
||||
return viewHolder
|
||||
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
beginTraceSession("MyAdapter.getItemViewType")
|
||||
if (isSection(position)) {
|
||||
return R.layout.grid_section_header
|
||||
}
|
||||
endTraceSession()
|
||||
return R.layout.grid_item_emoji
|
||||
}
|
||||
|
||||
private fun isSection(position: Int): Boolean {
|
||||
dataSource?.rawData?.categories?.let { categories ->
|
||||
var sectionOffset = 1
|
||||
var lastItemInSection = 0
|
||||
for (category in categories) {
|
||||
lastItemInSection = sectionOffset + category.emojis.size - 1
|
||||
if (position == sectionOffset - 1) return true
|
||||
sectionOffset = lastItemInSection + 2
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getSectionForAbsoluteIndex(position: Int): Int {
|
||||
var sectionOffset = 1
|
||||
var lastItemInSection = 0
|
||||
var index = 0
|
||||
dataSource?.rawData?.categories?.let {
|
||||
for (category in it) {
|
||||
lastItemInSection = sectionOffset + category.emojis.size - 1
|
||||
if (position <= lastItemInSection) return index
|
||||
sectionOffset = lastItemInSection + 2
|
||||
index++
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun getSectionOffset(section: Int): Int {
|
||||
//Todo cache this for fast access
|
||||
var sectionOffset = 1
|
||||
var lastItemInSection = 0
|
||||
dataSource?.rawData?.categories?.let {
|
||||
for ((index, category) in it.withIndex()) {
|
||||
lastItemInSection = sectionOffset + category.emojis.size - 1
|
||||
if (section == index) return sectionOffset
|
||||
sectionOffset = lastItemInSection + 2
|
||||
}
|
||||
}
|
||||
return sectionOffset
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
beginTraceSession("MyAdapter.onBindViewHolder")
|
||||
dataSource?.rawData?.categories?.let { categories ->
|
||||
val sectionNumber = getSectionForAbsoluteIndex(position)
|
||||
if (isSection(position)) {
|
||||
holder.bind(categories[sectionNumber].name)
|
||||
} else {
|
||||
val sectionMojis = categories[sectionNumber].emojis
|
||||
val sectionOffset = getSectionOffset(sectionNumber)
|
||||
val emoji = sectionMojis[position - sectionOffset]
|
||||
val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString()
|
||||
(holder as EmojiViewHolder).data = item
|
||||
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
|
||||
// Log.i("PERF","Bind with draw at position:$position")
|
||||
holder.bind(item)
|
||||
} else {
|
||||
// Log.i("PERF","Bind without draw at position:$position")
|
||||
toUpdateWhenNotBusy.add(item to holder)
|
||||
holder.bind(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
endTraceSession()
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
if (holder is EmojiViewHolder) {
|
||||
holder.data = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
toUpdateWhenNotBusy.removeIf { it.second == holder }
|
||||
} else {
|
||||
val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder }
|
||||
if (index != -1) {
|
||||
toUpdateWhenNotBusy.removeAt(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
dataSource?.rawData?.categories?.let {
|
||||
var count = /*number of sections*/ it.size
|
||||
for (ad in it) {
|
||||
count += ad.emojis.size
|
||||
}
|
||||
return count
|
||||
} ?: kotlin.run { return 0 }
|
||||
}
|
||||
|
||||
|
||||
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
abstract fun bind(s: String?)
|
||||
}
|
||||
|
||||
|
||||
class EmojiViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||
|
||||
var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text)
|
||||
val placeHolder: View = itemView.findViewById(R.id.grid_item_place_holder)
|
||||
|
||||
var data: String? = null
|
||||
|
||||
override fun bind(s: String?) {
|
||||
emojiView.emoji = s
|
||||
if (s != null) {
|
||||
emojiView.mLayout = getStaticLayoutForEmoji(s)
|
||||
placeHolder.visibility = View.GONE
|
||||
// emojiView.visibility = View.VISIBLE
|
||||
} else {
|
||||
emojiView.mLayout = null
|
||||
placeHolder.visibility = View.VISIBLE
|
||||
// emojiView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SectionViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||
|
||||
var textView: TextView = itemView.findViewById(R.id.section_header_textview)
|
||||
|
||||
override fun bind(s: String?) {
|
||||
textView.text = s
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun endTraceSession() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
Trace.endSection()
|
||||
}
|
||||
}
|
||||
|
||||
fun beginTraceSession(sectionName: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
Trace.beginSection(sectionName)
|
||||
}
|
||||
}
|
||||
|
||||
val staticLayoutCache = HashMap<String, StaticLayout>()
|
||||
|
||||
fun getStaticLayoutForEmoji(emoji: String): StaticLayout {
|
||||
var cachedLayout = staticLayoutCache[emoji]
|
||||
if (cachedLayout == null) {
|
||||
cachedLayout = StaticLayout(emoji, EmojiDrawView.tPaint, EmojiDrawView.emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||
staticLayoutCache[emoji] = cachedLayout!!
|
||||
}
|
||||
return cachedLayout!!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun firstVisibleSectionChange(section: Int)
|
||||
}
|
||||
|
||||
//privates
|
||||
|
||||
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
scrollState = when (newState) {
|
||||
RecyclerView.SCROLL_STATE_IDLE -> ScrollState.IDLE
|
||||
RecyclerView.SCROLL_STATE_SETTLING -> ScrollState.SETTLING
|
||||
RecyclerView.SCROLL_STATE_DRAGGING -> ScrollState.DRAGGING
|
||||
else -> ScrollState.UNKNWON
|
||||
}
|
||||
|
||||
//TODO better
|
||||
if (scrollState == ScrollState.IDLE) {
|
||||
//
|
||||
val toUpdate = toUpdateWhenNotBusy.clone() as ArrayList<Pair<String, EmojiViewHolder>>
|
||||
toUpdateWhenNotBusy.clear()
|
||||
toUpdate.chunked(8).forEach {
|
||||
recyclerView.post {
|
||||
val transition = AutoTransition().apply {
|
||||
duration = 150
|
||||
}
|
||||
for (pair in it) {
|
||||
val holder = pair.second
|
||||
if (pair.first == holder.data) {
|
||||
TransitionManager.beginDelayedTransition(holder.itemView as FrameLayout, transition)
|
||||
val data = holder.data
|
||||
holder.bind(data)
|
||||
}
|
||||
}
|
||||
toUpdateWhenNotBusy.clear()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
//Log.i("SCROLL SPEED","scroll speed $dy")
|
||||
isFastScroll = abs(dy) > 50
|
||||
val visible = (recyclerView.layoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition()
|
||||
GlobalScope.launch {
|
||||
val section = getSectionForAbsoluteIndex(visible)
|
||||
if (section != currentFirstVisibleSection) {
|
||||
currentFirstVisibleSection = section
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
interactionListener?.firstVisibleSectionChange(currentFirstVisibleSection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
vector/src/main/res/drawable/circle.xml
Normal file
6
vector/src/main/res/drawable/circle.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/pale_grey" />
|
||||
</shape>
|
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/fragment"
|
||||
android:name="im.vector.riotredesign.features.reactions.EmojiChooserFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:layout="@layout/emoji_chooser_fragment" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:minHeight="0dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
14
vector/src/main/res/layout/emoji_chooser_fragment.xml
Normal file
14
vector/src/main/res/layout/emoji_chooser_fragment.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/emoji_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
tools:context="im.vector.riotredesign.features.reactions.EmojiChooserFragment"
|
||||
tools:itemCount="100"
|
||||
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
tools:listitem="@layout/grid_item_emoji"
|
||||
tools:spanCount="10">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
31
vector/src/main/res/layout/grid_item_emoji.xml
Normal file
31
vector/src/main/res/layout/grid_item_emoji.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
tools:showIn="@layout/activity_emoji_reaction_picker">
|
||||
|
||||
<View
|
||||
android:id="@+id/grid_item_place_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:background="@drawable/circle" />
|
||||
|
||||
<im.vector.riotredesign.features.reactions.EmojiDrawView
|
||||
android:id="@+id/grid_item_emoji_text"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center" />
|
||||
<!--<TextView-->
|
||||
<!--android:layout_gravity="center"-->
|
||||
<!--android:id="@+id/grid_item_emoji_text"-->
|
||||
<!--android:layout_width="wrap_content"-->
|
||||
<!--android:layout_height="wrap_content"-->
|
||||
<!--tools:text="😀"-->
|
||||
<!--android:textSize="24sp"-->
|
||||
<!--/>-->
|
||||
</FrameLayout>
|
18
vector/src/main/res/layout/grid_section_header.xml
Normal file
18
vector/src/main/res/layout/grid_section_header.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/section_header_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Smiley & Peolple" />
|
||||
|
||||
</LinearLayout>
|
10
vector/src/main/res/menu/menu_emoji_reaction_picker.xml
Normal file
10
vector/src/main/res/menu/menu_emoji_reaction_picker.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_search_white"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="android.widget.SearchView"
|
||||
app:showAsAction="collapseActionView|ifRoom" />
|
||||
</menu>
|
1
vector/src/main/res/raw/emoji_picker_datasource.json
Normal file
1
vector/src/main/res/raw/emoji_picker_datasource.json
Normal file
File diff suppressed because one or more lines are too long
17
vector/src/main/res/values/font_certs.xml
Normal file
17
vector/src/main/res/values/font_certs.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<array name="com_google_android_gms_fonts_certs">
|
||||
<item>@array/com_google_android_gms_fonts_certs_dev</item>
|
||||
<item>@array/com_google_android_gms_fonts_certs_prod</item>
|
||||
</array>
|
||||
<string-array name="com_google_android_gms_fonts_certs_dev">
|
||||
<item>
|
||||
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
|
||||
</item>
|
||||
</string-array>
|
||||
<string-array name="com_google_android_gms_fonts_certs_prod">
|
||||
<item>
|
||||
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
|
||||
</item>
|
||||
</string-array>
|
||||
</resources>
|
@ -1402,6 +1402,8 @@ Why choose Riot.im?
|
||||
<string name="autodiscover_well_known_autofill_dialog_message">Riot detected a custom server configuration for your userId domain \"%s\":\n%s</string>
|
||||
<string name="autodiscover_well_known_autofill_confirm">Use Config</string>
|
||||
|
||||
|
||||
<string name="title_activity_emoji_reaction_picker">Reactions</string>
|
||||
<string name="reactions_agree">Agree</string>
|
||||
<string name="reactions_like">Like</string>
|
||||
<string name="message_add_reaction">Add Reaction</string>
|
||||
|
Reference in New Issue
Block a user