Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show categories in emoji picker #3300

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 62 additions & 3 deletions app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
Expand All @@ -31,8 +32,57 @@ class EmojiAdapter(
private val animate: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {

private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
private val trueEmojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
.sortedWith(compareBy<Emoji, String?>(nullsFirst()) { it.category?.lowercase(Locale.ROOT) }.thenBy { it.shortcode.lowercase(Locale.ROOT) })
private val emojiList = mutableListOf<EmojiGridItem>()
lateinit var emojiCategories: LinkedHashMap<String?, Int>
private set
private lateinit var emojiCategoryPositions: LinkedHashMap<String?, Int>

data class EmojiGridItem(val emoji: Emoji?, val trueIndex: Int?)

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)

val spanCount = (recyclerView.layoutManager as GridLayoutManager).spanCount

val catMap = LinkedHashMap<String?, Int>()
val catPosMap = LinkedHashMap<String?, Int>()
var currentCategory: String? = null
for (index in trueEmojiList.indices) {
val emoji = trueEmojiList[index]
if (emoji.category != currentCategory || index == 0) {
currentCategory = emoji.category
catMap[currentCategory] = index
if (index > 0) {
val emojiListIndex = emojiList.size - 1
repeat(2 * spanCount - (emojiListIndex % spanCount) - 1) {
emojiList.add(EmojiGridItem(null, null))
}
}
catPosMap[currentCategory] = emojiList.size
}
emojiList.add(EmojiGridItem(emoji, index))
}

emojiCategories = catMap
emojiCategoryPositions = catPosMap
}

fun getCategoryStartPosition(category: String?): Int {
return emojiCategoryPositions.getValue(category)
}

fun getCategoryForPosition(position: Int): String? {
var prevKey: String? = null
for (entry in emojiCategoryPositions) {
if (position < entry.value) {
return prevKey
}
prevKey = entry.key
}
return prevKey
}

override fun getItemCount() = emojiList.size

Expand All @@ -42,8 +92,17 @@ class EmojiAdapter(
}

override fun onBindViewHolder(holder: BindingHolder<ItemEmojiButtonBinding>, position: Int) {
val emoji = emojiList[position]
val emojiGridItem = emojiList[position]
val emoji = emojiGridItem.emoji
val emojiImageView = holder.binding.root
if (emoji == null) {
emojiImageView.background = null
Glide.with(emojiImageView).clear(emojiImageView)
emojiImageView.contentDescription = null
emojiImageView.setOnClickListener(null)
TooltipCompat.setTooltipText(emojiImageView, null)
return
}

if (animate) {
Glide.with(emojiImageView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
Expand All @@ -40,6 +43,7 @@ import android.widget.AdapterView
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -53,11 +57,15 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.ContentInfoCompat
import androidx.core.view.OnReceiveContentListener
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
Expand Down Expand Up @@ -483,7 +491,7 @@ class ComposeActivity :
composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet)
addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet)
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
emojiBehavior = BottomSheetBehavior.from(binding.emojiBottomSheet)

enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)

Expand Down Expand Up @@ -1217,10 +1225,62 @@ class ComposeActivity :
replaceTextAtCaret(":$shortcode: ")
}

private fun setActiveEmojiCategory(categoryTextView: TextView) {
binding.emojiCategoryLinearLayout.forEach { view ->
(view as TextView).setTypeface(null, if (view == categoryTextView) Typeface.BOLD else Typeface.NORMAL)
}
binding.emojiCategoryScrollView.smoothScrollTo(categoryTextView.left, 0)
}

private fun setEmojiList(emojiList: List<Emoji>?) {
if (emojiList != null) {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis)
val emojiAdapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis)
binding.emojiView.adapter = emojiAdapter

val categoryTextViews = hashMapOf<String?, TextView>()

emojiAdapter.emojiCategories.keys.forEachIndexed { i: Int, s: String? ->
val categoryTextView = TextView(this)
categoryTextView.text = s ?: getString(R.string.custom_emoji_default_category_title)
val padding = (8 * resources.displayMetrics.density + 0.5f).toInt()
categoryTextView.setPadding(padding, padding, padding, padding)
val typedValue = TypedValue()
this.theme.resolveAttribute(R.attr.status_text_medium, typedValue, true)
categoryTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, typedValue.getDimension(resources.displayMetrics))
if (i == 0) categoryTextView.setTypeface(null, Typeface.BOLD)
categoryTextView.setOnClickListener {
val p = emojiAdapter.getCategoryStartPosition(s)
val smoothScroller: RecyclerView.SmoothScroller = object : LinearSmoothScroller(binding.emojiView.context) {
override fun getHorizontalSnapPreference(): Int {
return SNAP_TO_START
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
return 2000f / binding.emojiView.computeHorizontalScrollRange()
}
}
smoothScroller.targetPosition = p
binding.emojiView.layoutManager?.startSmoothScroll(smoothScroller)

setActiveEmojiCategory(it as TextView)
}
binding.emojiCategoryLinearLayout.addView(categoryTextView)
categoryTextViews[s] = categoryTextView
}
if (emojiAdapter.emojiCategories.isNotEmpty()) {
binding.emojiCategoryScrollView.visibility = View.VISIBLE

binding.emojiView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (binding.emojiView.layoutManager?.isSmoothScrolling == true) return // TODO needs to be improved
val firstVisibleItemPosition = (binding.emojiView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
val activeCategory = emojiAdapter.getCategoryForPosition(firstVisibleItemPosition)
setActiveEmojiCategory(categoryTextViews.getValue(activeCategory))
}
})
}

enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty())
}
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ data class Emoji(
val shortcode: String,
val url: String,
@SerializedName("static_url") val staticUrl: String,
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?,
val category: String?
) : Parcelable
28 changes: 25 additions & 3 deletions app/src/main/res/layout/activity_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -236,19 +236,41 @@
android:textSize="?attr/status_text_medium" />
</LinearLayout>

<com.keylesspalace.tusky.view.EmojiPicker
android:id="@+id/emojiView"
<LinearLayout
android:id="@+id/emojiBottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:elevation="12dp"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="60dp"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

<HorizontalScrollView
android:id="@+id/emojiCategoryScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:visibility="gone">

<LinearLayout
android:id="@+id/emojiCategoryLinearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
</LinearLayout>
</HorizontalScrollView>

<com.keylesspalace.tusky.view.EmojiPicker
android:id="@+id/emojiView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

<com.keylesspalace.tusky.components.compose.view.ComposeOptionsView
android:id="@+id/composeOptionsBottomSheet"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@
<string name="action_compose_shortcut">Compose</string>

<string name="error_no_custom_emojis">Your instance %s does not have any custom emojis</string>
<string name="custom_emoji_default_category_title">Custom</string>
<string name="emoji_style">Emoji style</string>
<string name="system_default">System default</string>
<string name="download_fonts">You\'ll need to download these emoji sets first</string>
Expand Down Expand Up @@ -727,7 +728,7 @@

<string name="action_unfollow_hashtag_format">Unfollow #%s?</string>
<string name="mute_notifications_switch">Mute notifications</string>

<!-- Reading order preference -->
<string name="pref_title_reading_order">Reading order</string>
<string name="pref_reading_order_oldest_first">Oldest first</string>
Expand Down