From e6b03b1a4a30c4c381ec9299a2d6258ac63ce4c4 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 21 Sep 2021 13:58:58 -0300 Subject: [PATCH] Implement ability to select featured badge to display on profile. --- .../securesms/badges/BadgeRepository.kt | 10 ++++++ .../securesms/badges/models/LargeBadge.kt | 11 ++++++ .../self/featured/SelectFeaturedBadgeEvent.kt | 7 ++++ .../featured/SelectFeaturedBadgeFragment.kt | 18 ++++++++-- .../self/featured/SelectFeaturedBadgeState.kt | 9 ++++- .../featured/SelectFeaturedBadgeViewModel.kt | 34 +++++++++++++++++-- .../self/overview/BadgesOverviewViewModel.kt | 3 +- .../ViewBadgeBottomSheetDialogFragment.kt | 7 +++- .../securesms/util/LifecycleDisposable.kt | 4 +++ .../main/res/layout/badge_preference_view.xml | 2 ++ ...adge_bottom_sheet_dialog_fragment_page.xml | 19 ++++------- app/src/main/res/values/strings.xml | 4 +++ 12 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeEvent.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt index dced92c079..a8238a807f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.badges import android.content.Context import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.recipients.Recipient @@ -19,4 +20,13 @@ class BadgeRepository(context: Context) { val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context) recipientDatabase.setBadges(Recipient.self().id, badges) }.subscribeOn(Schedulers.io()) + + fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction { + val badges = Recipient.self().badges + val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge) + ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges) + + val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context) + recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges) + }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt index 0f36eee284..c79f6caaf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt @@ -23,6 +23,16 @@ data class LargeBadge( } } + class EmptyModel : MappingModel { + override fun areItemsTheSame(newItem: EmptyModel): Boolean = true + override fun areContentsTheSame(newItem: EmptyModel): Boolean = true + } + + class EmptyViewHolder(itemView: View) : MappingViewHolder(itemView) { + override fun bind(model: EmptyModel) { + } + } + class ViewHolder(itemView: View) : MappingViewHolder(itemView) { private val badge: ImageView = itemView.findViewById(R.id.badge) @@ -42,6 +52,7 @@ data class LargeBadge( companion object { fun register(mappingAdapter: MappingAdapter) { mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page)) + mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeEvent.kt new file mode 100644 index 0000000000..0675bde72f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeEvent.kt @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.badges.self.featured + +enum class SelectFeaturedBadgeEvent { + NO_BADGE_SELECTED, + FAILED_TO_UPDATE_PROFILE, + SAVE_SUCCESSFUL +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt index 3e6baea899..cd3cf0ff62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.badges.self.featured import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import org.thoughtcrime.securesms.R @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.util.LifecycleDisposable /** * Fragment which allows user to select one of their badges to be their "Featured" badge. @@ -28,17 +30,19 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment( private val viewModel: SelectFeaturedBadgeViewModel by viewModels(factoryProducer = { SelectFeaturedBadgeViewModel.Factory(BadgeRepository(requireContext())) }) + private val lifecycleDisposable = LifecycleDisposable() + private lateinit var scrollShadow: View + private lateinit var save: View override fun onViewCreated(view: View, savedInstanceState: Bundle?) { scrollShadow = view.findViewById(R.id.scroll_shadow) super.onViewCreated(view, savedInstanceState) - val save: View = view.findViewById(R.id.save) + save = view.findViewById(R.id.save) save.setOnClickListener { viewModel.save() - findNavController().popBackStack() } } @@ -56,7 +60,17 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment( val previewView: View = requireView().findViewById(R.id.preview) val previewViewHolder = FeaturedBadgePreview.ViewHolder(previewView) + lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle) + lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent -> + when (event) { + SelectFeaturedBadgeEvent.NO_BADGE_SELECTED -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__you_must_select_a_badge, Toast.LENGTH_LONG).show() + SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__failed_to_update_profile, Toast.LENGTH_LONG).show() + SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL -> findNavController().popBackStack() + } + } + viewModel.state.observe(viewLifecycleOwner) { state -> + save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY previewViewHolder.bind(FeaturedBadgePreview.Model(state.selectedBadge)) adapter.submitList(getConfiguration(state).toMappingModelList()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeState.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeState.kt index a224283c0e..a1f17a79e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeState.kt @@ -3,6 +3,13 @@ package org.thoughtcrime.securesms.badges.self.featured import org.thoughtcrime.securesms.badges.models.Badge data class SelectFeaturedBadgeState( + val stage: Stage = Stage.INIT, val selectedBadge: Badge? = null, val allUnlockedBadges: List = listOf() -) +) { + enum class Stage { + INIT, + READY, + SAVING + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeViewModel.kt index ad5436cc50..1fc68bc950 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeViewModel.kt @@ -3,24 +3,37 @@ package org.thoughtcrime.securesms.badges.self.featured import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import io.reactivex.rxjava3.kotlin.subscribeBy +import io.reactivex.rxjava3.subjects.PublishSubject +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.badges.BadgeRepository import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.livedata.Store -class SelectFeaturedBadgeViewModel(repository: BadgeRepository) : ViewModel() { +private val TAG = Log.tag(SelectFeaturedBadgeViewModel::class.java) + +class SelectFeaturedBadgeViewModel(private val repository: BadgeRepository) : ViewModel() { private val store = Store(SelectFeaturedBadgeState()) + private val eventSubject = PublishSubject.create() val state: LiveData = store.stateLiveData + val events: Observable = eventSubject.observeOn(AndroidSchedulers.mainThread()) private val disposables = CompositeDisposable() init { store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state -> - state.copy(selectedBadge = recipient.badges.firstOrNull(), allUnlockedBadges = recipient.badges) + state.copy( + stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage, + selectedBadge = recipient.badges.firstOrNull(), + allUnlockedBadges = recipient.badges + ) } } @@ -29,7 +42,22 @@ class SelectFeaturedBadgeViewModel(repository: BadgeRepository) : ViewModel() { } fun save() { - // TODO "Persist selection to database" + val snapshot = store.state + if (snapshot.selectedBadge == null) { + eventSubject.onNext(SelectFeaturedBadgeEvent.NO_BADGE_SELECTED) + return + } + + store.update { it.copy(stage = SelectFeaturedBadgeState.Stage.SAVING) } + disposables += repository.setFeaturedBadge(snapshot.selectedBadge).subscribeBy( + onComplete = { + eventSubject.onNext(SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL) + }, + onError = { error -> + Log.e(TAG, "Failed to update profile.", error) + eventSubject.onNext(SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE) + } + ) } override fun onCleared() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt index c0db53bc81..d2f0e1983c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt @@ -20,7 +20,7 @@ class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : Vi private val eventSubject = PublishSubject.create() val state: LiveData = store.stateLiveData - val events: Observable = eventSubject + val events: Observable = eventSubject.observeOn(AndroidSchedulers.mainThread()) val disposables = CompositeDisposable() @@ -36,7 +36,6 @@ class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : Vi fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) { disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile) - .observeOn(AndroidSchedulers.mainThread()) .subscribe( { store.update { it.copy(stage = BadgesOverviewState.Stage.READY) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt index 697f43dadf..5e00c86d4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt @@ -32,6 +32,8 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() + val pager: ViewPager2 = view.findViewById(R.id.pager) val tabs: TabLayout = view.findViewById(R.id.tab_layout) val action: MaterialButton = view.findViewById(R.id.action) @@ -44,13 +46,16 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr LargeBadge.register(adapter) pager.adapter = adapter + adapter.submitList(listOf(LargeBadge.EmptyModel())) TabLayoutMediator(tabs, pager) { _, _ -> }.attach() pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - viewModel.onPageSelected(position) + if (adapter.getModel(position).map { it is LargeBadge.Model }.orElse(false)) { + viewModel.onPageSelected(position) + } } }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt index 9760748ea4..821b69488a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt @@ -26,4 +26,8 @@ class LifecycleDisposable : DefaultLifecycleObserver { owner.lifecycle.removeObserver(this) disposables.clear() } + + operator fun plusAssign(disposable: Disposable) { + add(disposable) + } } diff --git a/app/src/main/res/layout/badge_preference_view.xml b/app/src/main/res/layout/badge_preference_view.xml index 58f0936ec0..03c83b461d 100644 --- a/app/src/main/res/layout/badge_preference_view.xml +++ b/app/src/main/res/layout/badge_preference_view.xml @@ -24,7 +24,9 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:layout_marginTop="6dp" + android:ellipsize="end" android:lineSpacingExtra="4sp" + android:lines="2" android:textAlignment="center" android:textAppearance="@style/Signal.Text.Caption" app:layout_constraintEnd_toEndOf="@id/badge" diff --git a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml index b8bb443781..e38b0ab9f4 100644 --- a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml +++ b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml @@ -1,29 +1,25 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 970055c437..999f7c0a06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3845,8 +3845,12 @@ Select badges + Preview Select a badge + You must select a badge + Failed to update profile + Become a sustainer