Add CFV2 Sticker Suggestions.

This commit is contained in:
Alex Hart
2023-06-16 12:26:23 -03:00
committed by Nicholas Tinsley
parent 2fbcc23451
commit 4ce05a064c
6 changed files with 160 additions and 24 deletions

View File

@@ -2175,7 +2175,7 @@ public class ConversationParentFragment extends Fragment
}
private void initializeStickerObserver() {
StickerSearchRepository repository = new StickerSearchRepository(requireContext());
StickerSearchRepository repository = new StickerSearchRepository();
stickerViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) new ConversationStickerViewModel.Factory(requireActivity().getApplication(), repository))
.get(ConversationStickerViewModel.class);

View File

@@ -188,6 +188,7 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiatio
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
@@ -229,6 +230,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity
import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
@@ -333,6 +335,10 @@ class ConversationFragment :
ConversationSearchViewModel(getString(R.string.note_to_self))
}
private val stickerViewModel: StickerSuggestionsViewModel by viewModel {
StickerSuggestionsViewModel()
}
private val conversationTooltips = ConversationTooltips(this)
private val colorizer = Colorizer()
private val textDraftSaveDebouncer = Debouncer(500)
@@ -430,6 +436,7 @@ class ConversationFragment :
container.fragmentManager = childFragmentManager
ToolbarDependentMarginListener(binding.toolbar)
initializeMediaKeyboardToggle()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@@ -676,6 +683,7 @@ class ConversationFragment :
initializeSearch()
initializeLinkPreviews()
initializeStickerSuggestions()
inputPanel.setListener(InputPanelListener())
}
@@ -1056,6 +1064,36 @@ class ConversationFragment :
.addTo(disposables)
}
private fun initializeMediaKeyboardToggle() {
val isSystemEmojiPreferred = SignalStore.settings().isPreferSystemEmoji
val keyboardMode: TextSecurePreferences.MediaKeyboardMode = TextSecurePreferences.getMediaKeyboardMode(requireContext())
val stickerIntro: Boolean = !TextSecurePreferences.hasSeenStickerIntroTooltip(requireContext())
inputPanel.showMediaKeyboardToggle(true)
val toggleMode = when (keyboardMode) {
TextSecurePreferences.MediaKeyboardMode.EMOJI -> if (isSystemEmojiPreferred) KeyboardPage.STICKER else KeyboardPage.EMOJI
TextSecurePreferences.MediaKeyboardMode.STICKER -> KeyboardPage.STICKER
TextSecurePreferences.MediaKeyboardMode.GIF -> KeyboardPage.GIF
}
inputPanel.setMediaKeyboardToggleMode(toggleMode)
if (stickerIntro) {
TextSecurePreferences.setMediaKeyboardMode(requireContext(), TextSecurePreferences.MediaKeyboardMode.STICKER)
inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER)
conversationTooltips.displayStickerIntroductionTooltip(inputPanel.mediaKeyboardToggleAnchorView) {
EventBus.getDefault().removeStickyEvent(StickerPackInstallEvent::class.java)
}
}
}
private fun initializeStickerSuggestions() {
stickerViewModel.stickers
.subscribeBy(onNext = inputPanel::setStickerSuggestions)
.addTo(disposables)
}
private fun updateLinkPreviewState() {
if (/* TODO [cfv2] -- viewModel.isPushAvailable && */ !attachmentManager.isAttachmentPresent && context != null) {
linkPreviewViewModel.onEnabled()
@@ -2739,7 +2777,8 @@ class ConversationFragment :
if (composeText.textTrimmed.isEmpty() || beforeLength == 0) {
composeText.postDelayed({ updateToggleButtonState() }, 50)
}
// todo [cfv2] stickerViewModel.onInputTextUpdated(s.toString())
stickerViewModel.onInputTextUpdated(s.toString())
}
override fun onFocusChange(v: View, hasFocus: Boolean) {
@@ -2942,6 +2981,23 @@ class ConversationFragment :
viewModel.updateIdentityRecords()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onStickerPackInstalled(event: StickerPackInstallEvent?) {
if (event == null) {
return
}
if (!TextSecurePreferences.hasSeenStickerIntroTooltip(requireContext())) {
return
}
EventBus.getDefault().removeStickyEvent(event)
if (!inputPanel.isStickerMode) {
conversationTooltips.displayStickerPackInstalledTooltip(inputPanel.mediaKeyboardToggleAnchorView, event)
}
}
//endregion
private inner class SearchEventListener : ConversationSearchBottomBar.EventListener {

View File

@@ -9,6 +9,8 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TooltipPopup
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent
import org.thoughtcrime.securesms.util.TextSecurePreferences
/**
* Any and all tooltips that the conversation can display, and a light amount of related presentation logic.
@@ -51,6 +53,31 @@ class ConversationTooltips(fragment: Fragment) {
.show(TooltipPopup.POSITION_BELOW)
}
/**
* Displayed to teach the user about sticker packs
*/
fun displayStickerIntroductionTooltip(anchor: View, onDismiss: () -> Unit) {
TooltipPopup.forTarget(anchor)
.setBackgroundTint(ContextCompat.getColor(anchor.context, R.color.core_ultramarine))
.setTextColor(ContextCompat.getColor(anchor.context, R.color.core_white))
.setText(R.string.ConversationActivity_new_say_it_with_stickers)
.setOnDismissListener {
TextSecurePreferences.setHasSeenStickerIntroTooltip(anchor.context, true)
onDismiss()
}
.show(TooltipPopup.POSITION_ABOVE)
}
/**
* Displayed after a sticker pack is installed
*/
fun displayStickerPackInstalledTooltip(anchor: View, event: StickerPackInstallEvent) {
TooltipPopup.forTarget(anchor)
.setText(R.string.ConversationActivity_sticker_pack_installed)
.setIconGlideModel(event.iconGlideModel)
.show(TooltipPopup.POSITION_ABOVE)
}
/**
* ViewModel which holds different bits of session-local persistent state for different tooltips.
*/

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import org.thoughtcrime.securesms.database.model.StickerRecord
import org.thoughtcrime.securesms.stickers.StickerSearchRepository
class StickerSuggestionsViewModel(
private val stickerSearchRepository: StickerSearchRepository = StickerSearchRepository()
) : ViewModel() {
private val stickerSearchProcessor = BehaviorProcessor.createDefault("")
val stickers: Flowable<List<StickerRecord>> = stickerSearchProcessor
.switchMapSingle { stickerSearchRepository.searchByEmoji(it) }
fun onInputTextUpdated(text: String) {
stickerSearchProcessor.onNext(text)
}
}

View File

@@ -4,7 +4,6 @@ import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.stickers.StickerSearchRepository
import org.thoughtcrime.securesms.util.DefaultValueLiveData
@@ -22,7 +21,7 @@ class KeyboardPagerViewModel : ViewModel() {
pages = DefaultValueLiveData(startingPages)
page = DefaultValueLiveData(startingPages.first())
StickerSearchRepository(ApplicationDependencies.getApplication()).getStickerFeatureAvailability { available ->
StickerSearchRepository().getStickerFeatureAvailability { available ->
if (!available) {
val updatedPages = pages.value.toMutableSet().apply { remove(KeyboardPage.STICKER) }
pages.postValue(updatedPages)

View File

@@ -1,9 +1,9 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
@@ -12,52 +12,80 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.StickerTable.StickerRecordReader;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class StickerSearchRepository {
private final StickerTable stickerDatabase;
private final AttachmentTable attachmentDatabase;
public StickerSearchRepository(@NonNull Context context) {
public StickerSearchRepository() {
this.stickerDatabase = SignalDatabase.stickers();
this.attachmentDatabase = SignalDatabase.attachments();
}
public @NonNull Single<List<StickerRecord>> searchByEmoji(@NonNull String emoji) {
if (emoji.isEmpty() || emoji.length() > EmojiSource.getLatest().getMaxEmojiLength()) {
return Single.just(Collections.emptyList());
}
return Single.fromCallable(() -> searchByEmojiSync(emoji));
}
public void searchByEmoji(@NonNull String emoji, @NonNull Callback<List<StickerRecord>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji);
List<StickerRecord> out = new ArrayList<>();
Set<String> possible = EmojiUtil.getAllRepresentations(searchEmoji);
callback.onResult(searchByEmojiSync(emoji));
});
}
for (String candidate : possible) {
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) {
StickerRecord record = null;
while ((record = reader.getNext()) != null) {
out.add(record);
}
@WorkerThread
private List<StickerRecord> searchByEmojiSync(@NonNull String emoji) {
String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji);
List<StickerRecord> out = new ArrayList<>();
Set<String> possible = EmojiUtil.getAllRepresentations(searchEmoji);
for (String candidate : possible) {
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) {
StickerRecord record = null;
while ((record = reader.getNext()) != null) {
out.add(record);
}
}
}
callback.onResult(out);
});
return out;
}
public @NonNull Single<Boolean> getStickerFeatureAvailability() {
return Single.fromCallable(this::getStickerFeatureAvailabilitySync)
.observeOn(Schedulers.io());
}
public void getStickerFeatureAvailability(@NonNull Callback<Boolean> callback) {
SignalExecutors.BOUNDED.execute(() -> {
try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) {
if (cursor != null && cursor.moveToFirst()) {
callback.onResult(true);
} else {
callback.onResult(attachmentDatabase.hasStickerAttachments());
}
}
callback.onResult(getStickerFeatureAvailabilitySync());
});
}
@WorkerThread
private Boolean getStickerFeatureAvailabilitySync() {
try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) {
if (cursor != null && cursor.moveToFirst()) {
return true;
} else {
return attachmentDatabase.hasStickerAttachments();
}
}
}
public interface Callback<T> {
void onResult(T result);
}