Implement ability to select featured badge to display on profile.

This commit is contained in:
Alex Hart 2021-09-21 13:58:58 -03:00
parent fb86fdfcd9
commit e6b03b1a4a
12 changed files with 106 additions and 22 deletions

View file

@ -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())
}

View file

@ -23,6 +23,16 @@ data class LargeBadge(
}
}
class EmptyModel : MappingModel<EmptyModel> {
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
override fun areContentsTheSame(newItem: EmptyModel): Boolean = true
}
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
override fun bind(model: EmptyModel) {
}
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(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))
}
}
}

View file

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.badges.self.featured
enum class SelectFeaturedBadgeEvent {
NO_BADGE_SELECTED,
FAILED_TO_UPDATE_PROFILE,
SAVE_SUCCESSFUL
}

View file

@ -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())
}

View file

@ -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<Badge> = listOf()
)
) {
enum class Stage {
INIT,
READY,
SAVING
}
}

View file

@ -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<SelectFeaturedBadgeEvent>()
val state: LiveData<SelectFeaturedBadgeState> = store.stateLiveData
val events: Observable<SelectFeaturedBadgeEvent> = 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() {

View file

@ -20,7 +20,7 @@ class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : Vi
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
val state: LiveData<BadgesOverviewState> = store.stateLiveData
val events: Observable<BadgesOverviewEvent> = eventSubject
val events: Observable<BadgesOverviewEvent> = 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) }

View file

@ -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)
}
}
})

View file

@ -26,4 +26,8 @@ class LifecycleDisposable : DefaultLifecycleObserver {
owner.lifecycle.removeObserver(this)
disposables.clear()
}
operator fun plusAssign(disposable: Disposable) {
add(disposable)
}
}

View file

@ -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"

View file

@ -1,29 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/badge"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/BadgesOverviewFragment__featured_badge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/test_gradient" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.Signal.Title2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/badge"
tools:text="Signal Sustainer" />
<TextView
@ -40,9 +36,6 @@
android:paddingEnd="32dp"
android:textAlignment="center"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="Paige supports Signal by making a monthly donation. Get your own badge by donating below." />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -3845,8 +3845,12 @@
<string name="BadgeSelectionFragment__select_badges">Select badges</string>
<string name="SelectFeaturedBadgeFragment__preview">Preview</string>
<string name="SelectFeaturedBadgeFragment__select_a_badge">Select a badge</string>
<string name="SelectFeaturedBadgeFragment__you_must_select_a_badge">You must select a badge</string>
<string name="SelectFeaturedBadgeFragment__failed_to_update_profile">Failed to update profile</string>
<string name="ViewBadgeBottomSheetDialogFragment__become_a_sustainer">Become a sustainer</string>
<!-- EOF -->