package jp.juggler.subwaytooter.dialog import android.app.Dialog import android.content.Context import android.graphics.drawable.PictureDrawable import android.text.Editable import android.text.TextWatcher import android.util.TypedValue import android.view.* import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.google.android.flexbox.* import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.databinding.EmojiPickerDialogBinding import jp.juggler.subwaytooter.emoji.* import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.span.NetworkEmojiSpan import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.emojiSizeMode import jp.juggler.subwaytooter.util.minHeightCompat import jp.juggler.subwaytooter.util.minWidthCompat import jp.juggler.subwaytooter.view.NetworkEmojiView import jp.juggler.util.* import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.data.* import jp.juggler.util.log.* import jp.juggler.util.ui.* import org.jetbrains.anko.image import org.jetbrains.anko.wrapContent import kotlin.math.abs import kotlin.math.sign private class EmojiPicker( private val activity: AppCompatActivity, private val accessInfo: SavedAccount?, private val closeOnSelected: Boolean, private val onPicked: suspend (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit, ) { companion object { private val log = LogCategory("EmojiPicker") private const val VT_CATEGORY = 0 private const val VT_CUSTOM_EMOJI = 1 private const val VT_TWEMOJI = 2 private const val VT_COMPAT_EMOJI = 3 private const val gridCols = 6 private fun EmojiCategory.getUnicodeEmojis() = emojiList.map { PickerItemUnicode(unicodeEmoji = it) } } private class SkinTone(val codeInt: Int) { val code = StringBuilder().apply { appendCodePoint(codeInt) }.toString() } private sealed interface PickerItem private class PickerItemUnicode(val unicodeEmoji: UnicodeEmoji) : PickerItem private class PickerItemCustom(val customEmoji: CustomEmoji) : PickerItem private open class PickerItemCategory( var name: String, val category: EmojiCategory, val original: PickerItemCategory? = null, var next: PickerItemCategory? = null, ) : PickerItem { val items = ArrayList() open fun createFiltered(keywordLower: String?) = PickerItemCategory( name = name, category = category, original = this, ).also { dst -> dst.items.addAll( if (keywordLower.isNullOrEmpty()) { items } else { items.filter { when (it) { is PickerItemCustom -> it.customEmoji.shortcode.contains(keywordLower) is PickerItemUnicode -> it.unicodeEmoji.namesLower.any { n -> n.contains(keywordLower) } else -> false } } } ) } } private class PickerItemCategoryRecent( name: String, val accessInfo: SavedAccount?, ) : PickerItemCategory(name, EmojiCategory.Recent) { private val recentsJsonList: List? get() = try { PrefS.spEmojiPickerRecent.value.decodeJsonArray().objectList() } catch (ex: Throwable) { log.w(ex, "can't load spEmojiPickerRecent") null } private fun JsonObject.parseRecent1( customEmojiMap: HashMap?, ): PickerItem? { val name = string("name") val instance = string("instance") try { name ?: error("missing emoji name") when (instance) { null -> EmojiMap.shortNameMap[name]?.let { return PickerItemUnicode(unicodeEmoji = it) } accessInfo?.apiHost?.ascii -> customEmojiMap?.get(name)?.let { return PickerItemCustom(customEmoji = it) } } } catch (ex: Throwable) { log.w(ex, "can't add emoji. $name, $instance") } return null } private fun List.loadRecents() { try { val customEmojiMap = accessInfo?.let { App1.custom_emoji_lister.getMapNonBlocking(it) } val newItems = mapNotNull { it.parseRecent1(customEmojiMap) } items.clear() items.addAll(newItems) } catch (ex: Throwable) { log.w(ex, "loadRecents failed.") } } // 最近使用した絵文字のPickerCategoryを作る fun load() = recentsJsonList?.loadRecents() fun update( targetName: String, targetInstance: String?, ) { // Recentをロード(他インスタンスの絵文字を含む) val list = recentsJsonList?.toMutableList() ?: ArrayList() // 選択された絵文字と同じ項目を除去 // 項目が増えすぎたら減らす // ユニコード絵文字256個、カスタム絵文字はインスタンス別256個まで var nCount = 0 val it = list.iterator() while (it.hasNext()) { val item = it.next() val itemName = item.string("name") val itemInstance = item.string("instance") if (itemInstance != targetInstance) continue if (itemName == targetName || ++nCount >= 256) { it.remove() } } // 先頭に項目を追加 list.add(0, JsonObject().apply { put("name", targetName) targetInstance?.let { put("instance", it) } }) // 保存する try { PrefS.spEmojiPickerRecent.value = list.toJsonArray().toString() } catch (ex: Throwable) { log.e(ex, "can't save spEmojiPickerRecent") } // カテゴリ内のPickerItemの更新 try { list.loadRecents() } catch (ex: Throwable) { log.e(ex, "loadRecents failed.") } } } private sealed class ViewHolderBase(view: View) : RecyclerView.ViewHolder(view) { abstract fun bind(item: PickerItem) } private var lastExpandCategory: PickerItemCategory? = null private var canCollapse = true private inner class VhCategory( view: LinearLayout = LinearLayout(activity), ) : ViewHolderBase(view) { var lastItem: PickerItemCategory? = null val ibExpand = AppCompatImageButton(activity).apply { layoutParams = LinearLayout.LayoutParams(gridSize, matchParent) background = ContextCompat.getDrawable( this@EmojiPicker.activity, R.drawable.btn_bg_transparent_round6dp ) setOnClickListener { val orig = lastItem?.original ?: return@setOnClickListener lastExpandCategory = if (lastExpandCategory != orig) orig else lastItem?.next // 再表示 showFiltered( lastSelectedCategory, lastSelectedKeyword, scrollCategoryTab = false, scrollToCategory = true, ) } } val tv = AppCompatTextView(activity).apply { layoutParams = LinearLayout.LayoutParams(0, wrapContent).apply { weight = 1f } minHeightCompat = (density * 48f + 0.5f).toInt() gravity = Gravity.START or Gravity.CENTER_VERTICAL includeFontPadding = false } init { view.layoutParams = RecyclerView.LayoutParams(headerWidth, wrapContent) view.setPadding(cellMargin, cellMargin, cellMargin, cellMargin) view.isBaselineAligned = false view.gravity = Gravity.START or Gravity.CENTER_VERTICAL view.addView(ibExpand) view.addView(tv) } override fun bind(item: PickerItem) { if (item is PickerItemCategory) { lastItem = item tv.text = item.name ibExpand.vg(canCollapse)?.let { val drawableId = when (lastExpandCategory == item.original) { true -> R.drawable.ic_arrow_drop_down else -> R.drawable.ic_arrow_drop_up } it.image = ContextCompat.getDrawable(activity, drawableId) } } } } private inner class VhCustomEmoji( val view: FrameLayout = FrameLayout(activity), ) : ViewHolderBase(view) { val niv = NetworkEmojiView( context = activity, sizeMode = accessInfo.emojiSizeMode(), maxEmojiWidth = NetworkEmojiSpan.maxEmojiWidth, ).apply { layoutParams = FrameLayout.LayoutParams(gridSize, gridSize) } init { view.setButtonBackground() view.setOnClickListener(pickerItemClickListener) view.layoutParams = RecyclerView.LayoutParams(wrapContent, wrapContent) view.setPadding(cellMargin, cellMargin, cellMargin, cellMargin) view.addView(niv) } override fun bind(item: PickerItem) { if (activity.isDestroyed) return if (item is PickerItemCustom) { view.setTag(R.id.btnAbout, item) niv.setEmoji( url = if (disableAnimation) { item.customEmoji.staticUrl } else { item.customEmoji.url }, initialAspect = item.customEmoji.aspect, defaultHeight = gridSize, ) } } } private inner class VhTwemoji( val view: FrameLayout = FrameLayout(activity), ) : ViewHolderBase(view) { val iv = AppCompatImageView(activity).apply { layoutParams = FrameLayout.LayoutParams(gridSize, gridSize) scaleType = ImageView.ScaleType.FIT_CENTER } init { view.setButtonBackground() view.setOnClickListener(pickerItemClickListener) view.layoutParams = RecyclerView.LayoutParams(wrapContent, wrapContent) view.setPadding(cellMargin, cellMargin, cellMargin, cellMargin) view.addView(iv) } override fun bind(item: PickerItem) { if (activity.isDestroyed) return if (item is PickerItemUnicode) { view.setTag(R.id.btnAbout, item) val emoji = applySkinTone(item.unicodeEmoji) if (emoji.isSvg) { Glide.with(activity) .`as`(PictureDrawable::class.java) .load("file:///android_asset/${emoji.assetsName}") .into(iv) } else { Glide.with(activity) .load(emoji.drawableId) .into(iv) } } } } private inner class VhAppCompatEmoji( val view: FrameLayout = FrameLayout(activity), ) : ViewHolderBase(view) { val tv = AppCompatTextView(activity).apply { layoutParams = FrameLayout.LayoutParams(gridSize, gridSize) gravity = Gravity.CENTER includeFontPadding = false setLineSpacing(0f, 0f) setTextSize(TypedValue.COMPLEX_UNIT_PX, gridSize.toFloat() * 0.7f) } init { view.setButtonBackground() view.setOnClickListener(pickerItemClickListener) view.layoutParams = RecyclerView.LayoutParams(wrapContent, wrapContent) view.addView(tv) } override fun bind(item: PickerItem) { if (activity.isDestroyed) return if (item is PickerItemUnicode) { view.setTag(R.id.btnAbout, item) val unicodeEmoji = applySkinTone(item.unicodeEmoji) tv.text = unicodeEmoji.unifiedCode } } } class FlexboxLayoutManagerWrapper(context: Context) : FlexboxLayoutManager(context) { override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams { return LayoutParams(lp) } } private inner class GridAdapter : RecyclerView.Adapter() { var list: List = emptyList() set(value) { field = value @Suppress("NotifyDataSetChanged") notifyDataSetChanged() } override fun getItemCount() = list.size override fun getItemViewType(position: Int) = when (list[position]) { is PickerItemCategory -> VT_CATEGORY is PickerItemCustom -> VT_CUSTOM_EMOJI is PickerItemUnicode -> when { useTwemoji -> VT_TWEMOJI else -> VT_COMPAT_EMOJI } } override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int) = when (viewType) { VT_CATEGORY -> VhCategory() VT_CUSTOM_EMOJI -> VhCustomEmoji() VT_TWEMOJI -> VhTwemoji() VT_COMPAT_EMOJI -> VhAppCompatEmoji() else -> error("unknown viewType=$viewType") } override fun onBindViewHolder(viewHolder: ViewHolderBase, position: Int) { viewHolder.bind(list[position]) } } private enum class FlickStatus { None, Start, Intercepted, } private lateinit var pickerCategries: List private val adapter = GridAdapter() private val views = EmojiPickerDialogBinding.inflate(activity.layoutInflater) private val ibSkinTone = listOf( Pair(R.id.btnSkinTone0, 0), Pair(R.id.btnSkinTone1, 0x1F3FB), Pair(R.id.btnSkinTone2, 0x1F3FC), Pair(R.id.btnSkinTone3, 0x1F3FD), Pair(R.id.btnSkinTone4, 0x1F3FE), Pair(R.id.btnSkinTone5, 0x1F3FF), ).map { (btnId, skinToneCode) -> views.root.findViewById(btnId).apply { tag = SkinTone(skinToneCode) setOnClickListener { selectedTone = (it.tag as SkinTone) showSkinTone() @Suppress("NotifyDataSetChanged") adapter.notifyDataSetChanged() } } } private val matchParent = RecyclerView.LayoutParams.MATCH_PARENT private val useTwemoji = PrefB.bpUseTwemoji.value private val disableAnimation = PrefB.bpDisableEmojiAnimation.value private var selectedTone: SkinTone = (ibSkinTone[0].tag as SkinTone) private lateinit var dialog: Dialog private var bInstanceHasCustomEmoji = false private var lastSelectedCategory: EmojiCategory? = null private var lastSelectedKeyword: String? = null private var recentCategory: PickerItemCategoryRecent? = null private val density = activity.resources.displayMetrics.density val cellMargin = (density * 1f + 0.5f).toInt() val gridSize = (density * 48f + 0.5f).toInt() val headerWidth = gridSize * 6 private val cancelY = 16f private val interceptX = 40f private var tracker: VelocityTracker? = null private var dragging = FlickStatus.None private var startX = 0f private var startY = 0f private val textWatcher = object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable?) { showFiltered(null, s?.toString()) } } private val pickerItemClickListener = View.OnClickListener { v -> val targetEmoji: EmojiBase val targetName: String val targetInstance: String? when (val item = v.getTag(R.id.btnAbout)) { is PickerItemUnicode -> { targetEmoji = applySkinTone(item.unicodeEmoji) targetName = targetEmoji.unifiedName targetInstance = null } is PickerItemCustom -> { targetEmoji = item.customEmoji targetName = item.customEmoji.shortcode targetInstance = accessInfo!!.apiHost.ascii } else -> return@OnClickListener } recentCategory?.update(targetName, targetInstance) if (closeOnSelected) { dialog.dismissSafe() } else if (lastSelectedCategory == null || lastSelectedCategory == EmojiCategory.Recent) { // 全カテゴリ表示や最近の表示は最近の絵文字の順序を変えるため更新してしまう showFiltered(lastSelectedCategory, lastSelectedKeyword) // XXX: タップ状態の表示が行えない… } else { // この場合はビューの更新は不要で、タップ状態の表示を行える } activity.launchAndShowError { onPicked(targetEmoji, bInstanceHasCustomEmoji) } } private suspend fun createCustomEmojiCategories(): List { accessInfo ?: error("missing accessInfo") val context = activity val srcList = App1.custom_emoji_lister.getList(accessInfo) val nameMap = HashMap() for (emoji in srcList) { if (!emoji.visibleInPicker) continue val categoryName = emoji.category ?: "" (nameMap[categoryName] ?: PickerItemCategory( name = categoryName, category = EmojiCategory.Custom, ).also { nameMap[categoryName] = it }) .items.add(PickerItemCustom(emoji)) } val otherCategory = nameMap[""] // カテゴリ名の頭に「カスタム」を追加 return nameMap.values.onEach { it.name = when (it) { otherCategory -> when (nameMap.size) { 0 -> context.getString(R.string.emoji_category_custom) else -> context.getString( R.string.emoji_picker_custom_of, context.getString(R.string.others) ) } else -> context.getString(R.string.emoji_picker_custom_of, it.name) } }.sortedWith { l, r -> if (l == otherCategory) 1 else if (r == otherCategory) -1 else l.name.compareTo(r.name) } } private suspend fun buildCategoryList() = buildList { // 最近使った絵文字 PickerItemCategoryRecent( name = activity.getString(R.string.emoji_category_recent), accessInfo = accessInfo, ).also { recentCategory = it it.load() add(it) } // カスタム絵文字 try { addAll(createCustomEmojiCategories()) } catch (ex: Throwable) { log.w(ex, "createCustomEmojiCategories failed.") } val categories = mutableListOf( EmojiCategory.People, EmojiCategory.ComplexTones, EmojiCategory.Nature, EmojiCategory.Foods, EmojiCategory.Activities, EmojiCategory.Places, EmojiCategory.Objects, EmojiCategory.Symbols, EmojiCategory.Flags, ) if (PrefB.bpEmojiPickerCategoryOther.value) { categories.add(EmojiCategory.Others) } for (category in categories) { val pc = PickerItemCategory( category = category, name = activity.getString(category.titleId), ) pc.items.addAll(category.getUnicodeEmojis()) add(pc) } } private fun applySkinTone(emojiArg: UnicodeEmoji): UnicodeEmoji { // トーン指定がないなら元のコード val selectedTone = selectedTone.takeIf { it.codeInt > 0 } ?: return emojiArg var emoji = emojiArg // Recentなどでは既にsuffixがついた名前が用意されている // suffixを除去する emoji.toneParent?.let { emoji = it } // 指定したトーンのサフィックスを追加して、絵文字が存在すればその名前にする emoji.toneChildren.find { it.first == selectedTone.code } ?.let { return it.second } // なければトーンなしの絵文字 return emoji } private fun showSkinTone() { ibSkinTone.forEach { when (selectedTone) { it.tag -> it.setImageResource(R.drawable.check_mark) else -> it.setImageDrawable(null) } } } private fun showFiltered( selectedCategory: EmojiCategory?, selectedKeyword: String?, scrollCategoryTab: Boolean = true, scrollToCategory: Boolean = false, ) { lastSelectedCategory = selectedCategory lastSelectedKeyword = selectedKeyword val keywordLower = selectedKeyword?.lowercase()?.trim() this.canCollapse = keywordLower.isNullOrEmpty() && (selectedCategory == null || selectedCategory == EmojiCategory.Custom) adapter.list = buildList { val filteredCategories = pickerCategries.filter { selectedCategory == null || it.category == selectedCategory }.mapNotNull { category -> category.createFiltered(keywordLower) .takeIf { it.items.isNotEmpty() } } for (i in filteredCategories.indices) { filteredCategories[i].next = filteredCategories.elementAtOrNull(i + 1)?.original ?: filteredCategories.elementAtOrNull(i - 1)?.original } if (lastExpandCategory == null || filteredCategories.none { it.original == lastExpandCategory } ) lastExpandCategory = filteredCategories.firstOrNull()?.original filteredCategories.forEach { if (selectedCategory == null || it.category == EmojiCategory.Custom) { // 見出し付き表示の場合は折りたたむ可能性がある add(it) if (!canCollapse || lastExpandCategory == it.original) addAll(it.items) } else { // カスタム以外のカテゴリが選択されている場合、(ヘッダがなく解除できないので) 折りたたみはできない addAll(it.items) } } } val targetCategory = lastExpandCategory if (scrollToCategory && targetCategory != null) { views.root.handler?.postDelayed({ adapter.list.indexOfFirst { (it as? PickerItemCategory)?.original == targetCategory } .takeIf { it != -1 } ?.let { views.rvGrid.smoothScrollToPosition(it) } }, 100L) } for (it in views.llCategories.children) { val backgroundId = when (it.tag) { selectedCategory -> R.drawable.bg_button_cw else -> R.drawable.btn_bg_transparent_round6dp } it.background = ContextCompat.getDrawable(it.context, backgroundId) if (it.tag == selectedCategory && scrollCategoryTab) { val oldScrollX = views.svCategories.scrollX val visibleWidth = views.svCategories.width log.i("left=${it.left},r=${it.right},s=$oldScrollX") when { oldScrollX > it.left -> views.svCategories.smoothScrollTo(it.left, 0) oldScrollX + visibleWidth < it.right -> views.svCategories.smoothScrollTo(it.right - visibleWidth, 0) } } } } private fun addCategoryButton(category: EmojiCategory) { val density = activity.resources.displayMetrics.density val wrapContent = FlexboxLayout.LayoutParams.WRAP_CONTENT val minWidth = (density * 48f + 0.5f).toInt() val padTb = (density * 4f + 0.5f).toInt() val padLr = (density * 6f + 0.5f).toInt() AppCompatButton(activity).apply { tag = category layoutParams = FlexboxLayout.LayoutParams(wrapContent, wrapContent) background = ContextCompat.getDrawable(context, R.drawable.btn_bg_transparent_round6dp) minWidthCompat = minWidth minHeightCompat = minWidth setPadding(padLr, padTb, padLr, padTb) text = activity?.getString(category.titleId) setOnClickListener { views.etFilter.removeTextChangedListener(textWatcher) views.etFilter.setText("") views.etFilter.addTextChangedListener(textWatcher) showFiltered(category, null) } }.let { views.llCategories.addView(it) } } private fun movePage(delta: Int) { val categories = buildList { addAll(pickerCategries.map { it.category }.distinct().sorted()) } val newIndex = when ( val oldIndex = categories.indexOfFirst { it == lastSelectedCategory } ) { -1 -> if (delta > 0) 0 else categories.size - 1 else -> (oldIndex + categories.size + delta) % categories.size } val newCategory = categories[newIndex] showFiltered(newCategory, null) } private fun View.setButtonBackground() { background = ContextCompat.getDrawable(context, R.drawable.btn_bg_transparent_round6dp) } private fun handleTouch(ev: MotionEvent, wasIntercept: Boolean) = try { when (ev.actionMasked) { MotionEvent.ACTION_CANCEL -> { log.i("ACTION_CANCEL wasIntercept=$wasIntercept") dragging = FlickStatus.None wasIntercept } MotionEvent.ACTION_UP -> { try { log.i("ACTION_UP wasIntercept=$wasIntercept") if (dragging == FlickStatus.Intercepted) { tracker?.let { it.addMovement(ev) it.computeCurrentVelocity(1000) val vx = it.xVelocity val vy = it.yVelocity val vxDp = vx / density val aspect = abs(vx) / abs(vy) log.i("vx=$vx vy=$vy") if (aspect < 2f) { log.i("not gesture: aspect=$aspect") } else if (abs(vxDp) < 40f) { log.i("not gesture: vxDp=$vxDp") } else { movePage((vxDp.sign * -1f).toInt()) } } } } finally { dragging = FlickStatus.None } wasIntercept } MotionEvent.ACTION_DOWN -> { log.i("ACTION_DOWN wasIntercept=$wasIntercept") // ドラッグ開始 dragging = FlickStatus.Start if (tracker == null) { tracker = VelocityTracker.obtain() } tracker?.clear() startX = ev.x startY = ev.y wasIntercept } MotionEvent.ACTION_MOVE -> { if (dragging == FlickStatus.None) { wasIntercept } else { // 移動量追跡 tracker?.addMovement(ev) val deltaX = abs(ev.x - startX) / density val deltaY = abs(ev.y - startY) / density when { // すでにインターセプトしている wasIntercept -> true // 上下方向に大きく動かしたらそれ以上追跡しない deltaY >= cancelY -> { log.i("not flick! $deltaY") dragging = FlickStatus.None false } // 横方向に大きく動かしたらインターセプトする deltaX >= interceptX && deltaX > deltaY -> { log.i("intercept! $deltaX") dragging = FlickStatus.Intercepted true } else -> { log.d("not yet intercept. $deltaX, $deltaY") false } } } } else -> log.w("handleTouch else $ev") } } catch (ex: Throwable) { log.e(ex, "handleTouch failed. ev=$ev, wasIntercept=$wasIntercept") wasIntercept } suspend fun start() { pickerCategries = buildCategoryList() bInstanceHasCustomEmoji = pickerCategries.any { it.category == EmojiCategory.Custom } pickerCategries.map { it.category }.distinct().sorted().forEach { addCategoryButton(it) } views.etFilter.addTextChangedListener(textWatcher) showFiltered(null, null) views.giGrid.intercept = { handleTouch(it, wasIntercept = false) } views.giGrid.touch = { handleTouch(it, wasIntercept = true) } views.rvGrid.adapter = adapter views.rvGrid.layoutManager = FlexboxLayoutManagerWrapper(activity).apply { flexDirection = FlexDirection.ROW flexWrap = FlexWrap.WRAP justifyContent = JustifyContent.FLEX_START } // // gridCols, // RecyclerView.VERTICAL, // false // ).also { // it.spanSizeLookup = adapter.spanSizeLookup // } showSkinTone() this.dialog = Dialog(activity) dialog.setContentView(views.root) dialog.setCancelable(true) dialog.setCanceledOnTouchOutside(true) dialog.setOnDismissListener { tracker?.recycle() } dialog.window?.let { w -> @Suppress("DEPRECATION") w.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN ) } dialog.show() } } fun launchEmojiPicker( activity: AppCompatActivity, accessInfo: SavedAccount?, closeOnSelected: Boolean, onPicked: suspend (EmojiBase, bInstanceHasCustomEmoji: Boolean) -> Unit, ) = activity.launchAndShowError { EmojiPicker( activity = activity, accessInfo = accessInfo, closeOnSelected = closeOnSelected, onPicked = onPicked, ).start() }