mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Add CFV2 Sticker Suggestions.
This commit is contained in:
committed by
Nicholas Tinsley
parent
2fbcc23451
commit
4ce05a064c
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user