package org.thoughtcrime.securesms.keyboard.sticker import android.os.Bundle import android.view.View import androidx.annotation.Px import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.SmoothScroller import com.google.android.material.appbar.AppBarLayout import org.signal.libsignal.protocol.util.Pair import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.stickers.StickerEventListener import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener.RolloverStickerRetriever import org.thoughtcrime.securesms.util.DeviceProperties import org.thoughtcrime.securesms.util.InsetItemDecoration import org.thoughtcrime.securesms.util.Throttler import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList import org.thoughtcrime.securesms.util.fragments.findListener import org.thoughtcrime.securesms.util.fragments.requireListener import java.util.Optional import kotlin.math.abs import kotlin.math.max class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment), KeyboardStickerListAdapter.EventListener, StickerRolloverTouchListener.RolloverEventListener, RolloverStickerRetriever, DatabaseObserver.Observer, View.OnLayoutChangeListener { private lateinit var stickerList: RecyclerView private lateinit var stickerListAdapter: KeyboardStickerListAdapter private lateinit var layoutManager: GridLayoutManager private lateinit var listTouchListener: StickerRolloverTouchListener private lateinit var stickerPacksRecycler: RecyclerView private lateinit var appBarLayout: AppBarLayout private lateinit var stickerPacksAdapter: KeyboardStickerPackListAdapter private lateinit var viewModel: StickerKeyboardPageViewModel private val packIdSelectionOnScroll: UpdatePackSelectionOnScroll = UpdatePackSelectionOnScroll() private val observerThrottler: Throttler = Throttler(500) private val stickerThrottler: Throttler = Throttler(100) private var firstLoad: Boolean = true override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val glideRequests = GlideApp.with(this) stickerListAdapter = KeyboardStickerListAdapter(glideRequests, this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext())) layoutManager = GridLayoutManager(requireContext(), 2).apply { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { val model: Optional> = stickerListAdapter.getModel(position) if (model.isPresent && model.get() is KeyboardStickerListAdapter.StickerHeader) { return spanCount } return 1 } } } listTouchListener = StickerRolloverTouchListener(requireContext(), glideRequests, this, this) stickerList = view.findViewById(R.id.sticker_keyboard_list) stickerList.layoutManager = layoutManager stickerList.adapter = stickerListAdapter stickerList.addOnItemTouchListener(listTouchListener) stickerList.addOnScrollListener(packIdSelectionOnScroll) stickerList.addItemDecoration(InsetItemDecoration(StickerInsetSetter())) stickerPacksRecycler = view.findViewById(R.id.sticker_packs_recycler) stickerPacksAdapter = KeyboardStickerPackListAdapter(glideRequests, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()), this::onTabSelected) stickerPacksRecycler.adapter = stickerPacksAdapter appBarLayout = view.findViewById(R.id.sticker_keyboard_search_appbar) view.findViewById(R.id.sticker_keyboard_search_text).callbacks = object : KeyboardPageSearchView.Callbacks { override fun onClicked() { requireListener().openStickerSearch() } } view.findViewById(R.id.sticker_search).setOnClickListener { requireListener().openStickerSearch() } view.findViewById(R.id.sticker_manage).setOnClickListener { findListener()?.onStickerManagementClicked() } ApplicationDependencies.getDatabaseObserver().registerStickerObserver(this) ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(this) view.addOnLayoutChangeListener(this) } override fun onDestroyView() { ApplicationDependencies.getDatabaseObserver().unregisterObserver(this) requireView().removeOnLayoutChangeListener(this) super.onDestroyView() } @Suppress("DEPRECATION") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(requireActivity(), StickerKeyboardPageViewModel.Factory()) .get(StickerKeyboardPageViewModel::class.java) viewModel.stickers.observe(viewLifecycleOwner, this::updateStickerList) viewModel.packs.observe(viewLifecycleOwner, stickerPacksAdapter::submitList) viewModel.getSelectedPack().observe(viewLifecycleOwner, this::updateCategoryTab) viewModel.refreshStickers() } private fun updateStickerList(stickers: MappingModelList) { if (firstLoad) { stickerListAdapter.submitList(stickers) { layoutManager.scrollToPositionWithOffset(1, 0) } firstLoad = false } else { stickerListAdapter.submitList(stickers) } } private fun onTabSelected(stickerPack: KeyboardStickerPackListAdapter.StickerPack) { scrollTo(stickerPack.packRecord.packId) viewModel.selectPack(stickerPack.packRecord.packId) } private fun updateCategoryTab(packId: String) { stickerPacksRecycler.post { val index: Int = stickerPacksAdapter.indexOfFirst(KeyboardStickerPackListAdapter.StickerPack::class.java) { it.packRecord.packId == packId } if (index != -1) { stickerPacksRecycler.smoothScrollToPosition(index) } } } private fun scrollTo(packId: String) { val index = stickerListAdapter.indexOfFirst(KeyboardStickerListAdapter.StickerHeader::class.java) { it.packId == packId } if (index != -1) { appBarLayout.setExpanded(false, true) packIdSelectionOnScroll.startAutoScrolling() smoothScrollToPositionTop(index) } } private fun smoothScrollToPositionTop(position: Int) { val currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition() val shortTrip = abs(currentPosition - position) < 40 if (shortTrip) { val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) { override fun getVerticalSnapPreference(): Int { return SNAP_TO_START } } smoothScroller.targetPosition = position layoutManager.startSmoothScroll(smoothScroller) } else { layoutManager.scrollToPositionWithOffset(position, 0) } } override fun onStickerClicked(sticker: KeyboardStickerListAdapter.Sticker) { stickerThrottler.publish { findListener()?.onStickerSelected(sticker.stickerRecord) } } override fun onStickerLongClicked(sticker: KeyboardStickerListAdapter.Sticker) { listTouchListener.enterHoverMode(stickerList, sticker) } override fun getStickerDataFromView(view: View): Pair? { val position: Int = stickerList.getChildAdapterPosition(view) val model: Optional> = stickerListAdapter.getModel(position) if (model.isPresent && model.get() is KeyboardStickerListAdapter.Sticker) { val sticker = model.get() as KeyboardStickerListAdapter.Sticker return Pair(sticker.uri, sticker.stickerRecord.emoji) } return null } override fun onStickerPopupStarted() = Unit override fun onStickerPopupEnded() = Unit override fun onChanged() { observerThrottler.publish(viewModel::refreshStickers) } override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { onScreenWidthChanged(view?.width ?: 0) } private fun onScreenWidthChanged(@Px newWidth: Int) { layoutManager.spanCount = calculateColumnCount(newWidth) } private fun calculateColumnCount(@Px screenWidth: Int): Int { val divisor = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_width).toFloat() + resources.getDimensionPixelOffset(R.dimen.sticker_page_item_padding).toFloat() return max(1, (screenWidth / divisor).toInt()) } private inner class UpdatePackSelectionOnScroll : RecyclerView.OnScrollListener() { private var doneScrolling: Boolean = true fun startAutoScrolling() { doneScrolling = false } @Override override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == RecyclerView.SCROLL_STATE_IDLE && !doneScrolling) { doneScrolling = true onScrolled(recyclerView, 0, 0) } } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.layoutManager == null || !doneScrolling) { return } val layoutManager = recyclerView.layoutManager as LinearLayoutManager val index = layoutManager.findFirstCompletelyVisibleItemPosition() val item: Optional> = stickerListAdapter.getModel(index) if (item.isPresent && item.get() is KeyboardStickerListAdapter.HasPackId) { viewModel.selectPack((item.get() as KeyboardStickerListAdapter.HasPackId).packId) } } } interface Callback { fun openStickerSearch() } }