Implement ability to view badges and modify whether they appear.
Note: this is available in staging only.
This commit is contained in:
parent
556ca5a573
commit
77cf029fdc
48 changed files with 1880 additions and 100 deletions
|
@ -0,0 +1,22 @@
|
|||
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.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
|
||||
class BadgeRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) }
|
||||
ProfileUtil.uploadProfileWithBadges(context, badges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.setBadges(Recipient.self().id, badges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.graphics.withScale
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeAnimator
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
|
||||
object Badges {
|
||||
fun Drawable.insetWithOutline(
|
||||
@Px outlineWidth: Float,
|
||||
@ColorInt outlineColor: Int
|
||||
): Drawable {
|
||||
val clone = mutate().constantState?.newDrawable()?.mutate()
|
||||
clone?.colorFilter = SimpleColorFilter(outlineColor)
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
clone?.bounds = wrapped.bounds
|
||||
clone?.draw(canvas)
|
||||
|
||||
val scale = 1 - ((outlineWidth * 2) / canvas.width)
|
||||
|
||||
canvas.withScale(x = scale, y = scale, canvas.width / 2f, canvas.height / 2f) {
|
||||
wrapped.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Drawable.selectable(
|
||||
@Px outlineWidth: Float,
|
||||
@ColorInt outlineColor: Int,
|
||||
@ColorInt gapColor: Int,
|
||||
animator: BadgeAnimator
|
||||
): Drawable {
|
||||
val outline = mutate().constantState?.newDrawable()?.mutate()
|
||||
outline?.colorFilter = SimpleColorFilter(outlineColor)
|
||||
|
||||
val gap = mutate().constantState?.newDrawable()?.mutate()
|
||||
gap?.colorFilter = SimpleColorFilter(gapColor)
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
outline?.bounds = wrapped.bounds
|
||||
gap?.bounds = wrapped.bounds
|
||||
|
||||
outline?.draw(canvas)
|
||||
|
||||
val scale = 1 - ((outlineWidth * 2) / wrapped.bounds.width())
|
||||
val interpolatedScale = scale + (1f - scale) * animator.getFraction()
|
||||
|
||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
||||
gap?.draw(canvas)
|
||||
|
||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
||||
wrapped.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
if (animator.shouldInvalidate()) {
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DSLConfiguration.displayBadges(badges: List<Badge>, selectedBadge: Badge? = null) {
|
||||
badges
|
||||
.map { Badge.Model(it, it == selectedBadge) }
|
||||
.forEach { customPref(it) }
|
||||
|
||||
val empties = (4 - (badges.size % 4)) % 4
|
||||
repeat(empties) {
|
||||
customPref(Badge.EmptyModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun createLayoutManagerForGridWithBadges(context: Context): RecyclerView.LayoutManager {
|
||||
val layoutManager = FlexboxLayoutManager(context)
|
||||
|
||||
layoutManager.flexDirection = FlexDirection.ROW
|
||||
layoutManager.alignItems = AlignItems.CENTER
|
||||
layoutManager.justifyContent = JustifyContent.CENTER
|
||||
|
||||
return layoutManager
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.load.Key
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges.selectable
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import java.security.MessageDigest
|
||||
|
||||
typealias OnBadgeClicked = (Badge, Boolean) -> Unit
|
||||
|
||||
/**
|
||||
* A Badge that can be collected and displayed by a user.
|
||||
*/
|
||||
data class Badge(
|
||||
val id: String,
|
||||
val category: Category,
|
||||
val imageUrl: Uri,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val expirationTimestamp: Long,
|
||||
val visible: Boolean
|
||||
) : Parcelable, Key {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
requireNotNull(parcel.readString()),
|
||||
Category.fromCode(requireNotNull(parcel.readString())),
|
||||
requireNotNull(parcel.readParcelable(Uri::class.java.classLoader)),
|
||||
requireNotNull(parcel.readString()),
|
||||
requireNotNull(parcel.readString()),
|
||||
parcel.readLong(),
|
||||
parcel.readByte() == 1.toByte()
|
||||
)
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(id)
|
||||
parcel.writeString(category.code)
|
||||
parcel.writeParcelable(imageUrl, flags)
|
||||
parcel.writeString(name)
|
||||
parcel.writeString(description)
|
||||
parcel.writeLong(expirationTimestamp)
|
||||
parcel.writeByte(if (visible) 1 else 0)
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||
}
|
||||
|
||||
fun resolveDescription(shortName: String): String {
|
||||
return description.replace("{short_name}", shortName)
|
||||
}
|
||||
|
||||
class EmptyModel : PreferenceModel<EmptyModel>() {
|
||||
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
|
||||
}
|
||||
|
||||
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
|
||||
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
init {
|
||||
itemView.isEnabled = false
|
||||
itemView.isFocusable = false
|
||||
itemView.isClickable = false
|
||||
itemView.visibility = View.INVISIBLE
|
||||
|
||||
name.text = " "
|
||||
}
|
||||
|
||||
override fun bind(model: EmptyModel) = Unit
|
||||
}
|
||||
|
||||
class Model(
|
||||
val badge: Badge,
|
||||
val isSelected: Boolean = false
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.badge.id == badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge && isSelected == newItem.isSelected
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: Model): Any? {
|
||||
return if (badge == newItem.badge && isSelected != newItem.isSelected) {
|
||||
SELECTION_CHANGED
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
private val target = Target(badge)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener {
|
||||
onBadgeClicked(model.badge, model.isSelected)
|
||||
}
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
if (model.isSelected) {
|
||||
target.animateToStart()
|
||||
} else {
|
||||
target.animateToEnd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
GlideApp.with(badge)
|
||||
.load(model.badge)
|
||||
.into(target)
|
||||
|
||||
if (model.isSelected) {
|
||||
target.setAnimationToStart()
|
||||
} else {
|
||||
target.setAnimationToEnd()
|
||||
}
|
||||
|
||||
name.text = model.badge.name
|
||||
}
|
||||
}
|
||||
|
||||
enum class Category(val code: String) {
|
||||
Donor("donor"),
|
||||
Other("other"),
|
||||
Testing("testing"); // Will be removed before final release
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): Category {
|
||||
return when (code) {
|
||||
"donor" -> Donor
|
||||
"testing" -> Testing
|
||||
else -> Other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
||||
|
||||
private val animator: BadgeAnimator = BadgeAnimator()
|
||||
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
view.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
val drawable = resource.selectable(
|
||||
DimensionUnit.DP.toPixels(2.5f),
|
||||
ContextCompat.getColor(view.context, R.color.signal_inverse_primary),
|
||||
ContextCompat.getColor(view.context, R.color.signal_background_primary),
|
||||
animator
|
||||
)
|
||||
|
||||
view.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
view.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
fun setAnimationToStart() {
|
||||
animator.setState(BadgeAnimator.State.START)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun setAnimationToEnd() {
|
||||
animator.setState(BadgeAnimator.State.END)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun animateToStart() {
|
||||
animator.setState(BadgeAnimator.State.REVERSE)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
|
||||
fun animateToEnd() {
|
||||
animator.setState(BadgeAnimator.State.FORWARD)
|
||||
view.drawable?.invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Badge> {
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): Badge {
|
||||
return Badge(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Badge?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
class BadgeAnimator {
|
||||
|
||||
val duration = 250L
|
||||
|
||||
var state: State = State.START
|
||||
private set
|
||||
|
||||
private var startTime: Long = 0L
|
||||
|
||||
fun getFraction(): Float {
|
||||
return when (state) {
|
||||
State.START -> 0f
|
||||
State.END -> 1f
|
||||
State.FORWARD -> Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
||||
State.REVERSE -> 1f - Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
||||
}
|
||||
}
|
||||
|
||||
fun setState(newState: State) {
|
||||
shouldInvalidate()
|
||||
|
||||
if (state == newState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newState == State.END || newState == State.START) {
|
||||
state = newState
|
||||
startTime = 0L
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.START && newState == State.REVERSE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.END && newState == State.FORWARD) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.START && newState == State.FORWARD) {
|
||||
state = State.FORWARD
|
||||
startTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.END && newState == State.REVERSE) {
|
||||
state = State.REVERSE
|
||||
startTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.FORWARD && newState == State.REVERSE) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val delta = duration - elapsed
|
||||
startTime -= delta
|
||||
state = State.REVERSE
|
||||
return
|
||||
}
|
||||
|
||||
if (state == State.REVERSE && newState == State.FORWARD) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val delta = duration - elapsed
|
||||
startTime -= delta
|
||||
state = State.FORWARD
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldInvalidate(): Boolean {
|
||||
if (state == State.START || state == State.END) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (state == State.FORWARD && getFraction() == 1f) {
|
||||
state = State.END
|
||||
return false
|
||||
}
|
||||
|
||||
if (state == State.REVERSE && getFraction() == 0f) {
|
||||
state = State.START
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
enum class State {
|
||||
START,
|
||||
FORWARD,
|
||||
REVERSE,
|
||||
END
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
object FeaturedBadgePreview {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
}
|
||||
|
||||
data class Model(val badge: Badge?) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.badge?.id == badge?.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val target: Target = Target(badge)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
avatar.setRecipient(Recipient.self())
|
||||
avatar.disableQuickContact()
|
||||
|
||||
if (model.badge != null) {
|
||||
GlideApp.with(badge)
|
||||
.load(model.badge)
|
||||
.into(target)
|
||||
} else {
|
||||
GlideApp.with(badge).clear(badge)
|
||||
badge.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
view.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
view.setImageDrawable(
|
||||
resource.insetWithOutline(
|
||||
DimensionUnit.DP.toPixels(2.5f),
|
||||
ContextCompat.getColor(view.context, R.color.signal_background_primary)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
view.setImageDrawable(placeholder)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
data class LargeBadge(
|
||||
val badge: Badge
|
||||
) {
|
||||
|
||||
class Model(val largeBadge: LargeBadge, val shortName: String) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
private val description: TextView = itemView.findViewById(R.id.description)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
GlideApp.with(badge)
|
||||
.load(model.largeBadge.badge)
|
||||
.into(badge)
|
||||
|
||||
name.text = model.largeBadge.badge.name
|
||||
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.FeaturedBadgePreview
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
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
|
||||
|
||||
/**
|
||||
* Fragment which allows user to select one of their badges to be their "Featured" badge.
|
||||
*/
|
||||
class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
titleId = R.string.BadgesOverviewFragment__featured_badge,
|
||||
layoutId = R.layout.select_featured_badge_fragment,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val viewModel: SelectFeaturedBadgeViewModel by viewModels(factoryProducer = { SelectFeaturedBadgeViewModel.Factory(BadgeRepository(requireContext())) })
|
||||
|
||||
private lateinit var scrollShadow: 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.setOnClickListener {
|
||||
viewModel.save()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(scrollShadow)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, isSelected ->
|
||||
if (!isSelected) {
|
||||
viewModel.setSelectedBadge(badge)
|
||||
}
|
||||
}
|
||||
|
||||
val previewView: View = requireView().findViewById(R.id.preview)
|
||||
val previewViewHolder = FeaturedBadgePreview.ViewHolder(previewView)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
previewViewHolder.bind(FeaturedBadgePreview.Model(state.selectedBadge))
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
|
||||
displayBadges(state.allUnlockedBadges, state.selectedBadge)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class SelectFeaturedBadgeState(
|
||||
val selectedBadge: Badge? = null,
|
||||
val allUnlockedBadges: List<Badge> = listOf()
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
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 store = Store(SelectFeaturedBadgeState())
|
||||
|
||||
val state: LiveData<SelectFeaturedBadgeState> = store.stateLiveData
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
state.copy(selectedBadge = recipient.badges.firstOrNull(), allUnlockedBadges = recipient.badges)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedBadge(badge: Badge) {
|
||||
store.update { it.copy(selectedBadge = badge) }
|
||||
}
|
||||
|
||||
fun save() {
|
||||
// TODO "Persist selection to database"
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(SelectFeaturedBadgeViewModel(badgeRepository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
enum class BadgesOverviewEvent {
|
||||
FAILED_TO_UPDATE_PROFILE
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
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.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Fragment to allow user to manage options related to the badges they've unlocked.
|
||||
*/
|
||||
class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ManageProfileFragment_badges,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val viewModel: BadgesOverviewViewModel by viewModels(factoryProducer = { BadgesOverviewViewModel.Factory(BadgeRepository(requireContext())) })
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _ ->
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
lifecycleDisposable.add(
|
||||
viewModel.events.subscribe { event: BadgesOverviewEvent ->
|
||||
when (event) {
|
||||
BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.BadgesOverviewFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BadgesOverviewState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
|
||||
|
||||
displayBadges(state.allUnlockedBadges)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
||||
isChecked = state.displayBadgesOnProfile,
|
||||
onClick = {
|
||||
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY,
|
||||
onClick = {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class BadgesOverviewState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val allUnlockedBadges: List<Badge> = listOf(),
|
||||
val featuredBadge: Badge? = null,
|
||||
val displayBadgesOnProfile: Boolean = false
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
UPDATING
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
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.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
|
||||
class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : ViewModel() {
|
||||
private val store = Store(BadgesOverviewState())
|
||||
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
|
||||
|
||||
val state: LiveData<BadgesOverviewState> = store.stateLiveData
|
||||
val events: Observable<BadgesOverviewEvent> = eventSubject
|
||||
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
state.copy(
|
||||
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
||||
allUnlockedBadges = recipient.badges,
|
||||
displayBadgesOnProfile = recipient.badges.firstOrNull()?.visible == true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
|
||||
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
},
|
||||
{ error ->
|
||||
Log.e(TAG, "Failed to update visibility.", error)
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
eventSubject.onNext(BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
private val viewModel: ViewBadgeViewModel by viewModels(factoryProducer = { ViewBadgeViewModel.Factory(getStartBadge(), getRecipientId(), BadgeRepository(requireContext())) })
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
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)
|
||||
|
||||
if (getRecipientId() == Recipient.self().id) {
|
||||
action.visible = false
|
||||
}
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
|
||||
LargeBadge.register(adapter)
|
||||
pager.adapter = adapter
|
||||
|
||||
TabLayoutMediator(tabs, pager) { _, _ ->
|
||||
}.attach()
|
||||
|
||||
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
viewModel.onPageSelected(position)
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.recipient == null || state.badgeLoadState == ViewBadgeState.LoadState.INIT) {
|
||||
return@observe
|
||||
}
|
||||
|
||||
if (state.allBadgesVisibleOnProfile.isEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
adapter.submitList(
|
||||
state.allBadgesVisibleOnProfile.map {
|
||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()))
|
||||
}
|
||||
) {
|
||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||
if (state.selectedBadge != null && pager.currentItem != stateSelectedIndex) {
|
||||
pager.currentItem = stateSelectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStartBadge(): Badge? = requireArguments().getParcelable(ARG_START_BADGE)
|
||||
|
||||
private fun getRecipientId(): RecipientId = requireNotNull(requireArguments().getParcelable(ARG_RECIPIENT_ID))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_START_BADGE = "start_badge"
|
||||
private const val ARG_RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(
|
||||
fragmentManager: FragmentManager,
|
||||
recipientId: RecipientId,
|
||||
startBadge: Badge? = null
|
||||
) {
|
||||
ViewBadgeBottomSheetDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_START_BADGE, startBadge)
|
||||
putParcelable(ARG_RECIPIENT_ID, recipientId)
|
||||
}
|
||||
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ViewBadgeState(
|
||||
val allBadgesVisibleOnProfile: List<Badge> = listOf(),
|
||||
val badgeLoadState: LoadState = LoadState.INIT,
|
||||
val selectedBadge: Badge? = null,
|
||||
val recipient: Recipient? = null
|
||||
) {
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
LOADED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ViewBadgeViewModel(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val store = Store(ViewBadgeState())
|
||||
|
||||
val state: LiveData<ViewBadgeState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
allBadgesVisibleOnProfile = recipient.badges,
|
||||
selectedBadge = startBadge ?: recipient.badges.firstOrNull(),
|
||||
badgeLoadState = ViewBadgeState.LoadState.LOADED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun onPageSelected(position: Int) {
|
||||
if (position > store.state.allBadgesVisibleOnProfile.size - 1 || position < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(selectedBadge = it.allBadgesVisibleOnProfile[position])
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(ViewBadgeViewModel(startBadge, recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -10,6 +11,7 @@ import androidx.annotation.StringRes
|
|||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
|
@ -18,7 +20,8 @@ import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimation
|
|||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
|
||||
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
@ -46,6 +49,7 @@ abstract class DSLSettingsFragment(
|
|||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val adapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView.layoutManager = layoutManagerProducer(requireContext())
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(scrollAnimationHelper)
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.cash.exhaustive.Exhaustive
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
|
@ -30,6 +30,10 @@ import org.thoughtcrime.securesms.MuteDialog
|
|||
import org.thoughtcrime.securesms.PushContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
|
@ -85,7 +89,8 @@ private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
|
|||
|
||||
class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
menuId = R.menu.conversation_settings
|
||||
menuId = R.menu.conversation_settings,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
|
@ -175,6 +180,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
BioTextPreference.register(adapter)
|
||||
AvatarPreference.register(adapter)
|
||||
ButtonStripPreference.register(adapter)
|
||||
|
@ -185,6 +192,13 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
GroupDescriptionPreference.register(adapter)
|
||||
LegacyGroupPreference.register(adapter)
|
||||
|
||||
val recipientId = args.recipientId
|
||||
if (recipientId != null) {
|
||||
Badge.register(adapter) { badge, _ ->
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, recipientId, badge)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
|
||||
if (state.recipient != Recipient.UNKNOWN) {
|
||||
|
@ -466,12 +480,20 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { groupState ->
|
||||
if (groupState.selfHasGroups) {
|
||||
state.withRecipientSettingsState { recipientSettingsState ->
|
||||
if (state.recipient.badges.isNotEmpty()) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ManageProfileFragment_badges)
|
||||
|
||||
displayBadges(state.recipient.badges)
|
||||
}
|
||||
|
||||
if (recipientSettingsState.selfHasGroups) {
|
||||
|
||||
dividerPref()
|
||||
|
||||
val groupsInCommonCount = groupState.allGroupsInCommon.size
|
||||
val groupsInCommonCount = recipientSettingsState.allGroupsInCommon.size
|
||||
sectionHeaderPref(
|
||||
DSLSettingsText.from(
|
||||
if (groupsInCommonCount == 0) {
|
||||
|
@ -496,7 +518,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
)
|
||||
)
|
||||
|
||||
for (group in groupState.groupsInCommon) {
|
||||
for (group in recipientSettingsState.groupsInCommon) {
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = group,
|
||||
|
@ -508,7 +530,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
)
|
||||
}
|
||||
|
||||
if (groupState.canShowMoreGroupsInCommon) {
|
||||
if (recipientSettingsState.canShowMoreGroupsInCommon) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
|
@ -718,7 +740,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
private val rect = Rect()
|
||||
|
||||
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val layoutManager = recyclerView.layoutManager as FlexboxLayoutManager
|
||||
|
||||
return if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
val firstChild = requireNotNull(layoutManager.getChildAt(0))
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException;
|
|||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
|
@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
|||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData;
|
||||
|
@ -153,6 +155,7 @@ public class RecipientDatabase extends Database {
|
|||
private static final String GROUPS_IN_COMMON = "groups_in_common";
|
||||
private static final String CHAT_COLORS = "chat_colors";
|
||||
private static final String CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id";
|
||||
private static final String BADGES = "badges";
|
||||
|
||||
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
|
||||
private static final String SORT_NAME = "sort_name";
|
||||
|
@ -188,7 +191,8 @@ public class RecipientDatabase extends Database {
|
|||
MENTION_SETTING,
|
||||
ABOUT, ABOUT_EMOJI,
|
||||
EXTRAS, GROUPS_IN_COMMON,
|
||||
CHAT_COLORS, CUSTOM_CHAT_COLORS_ID
|
||||
CHAT_COLORS, CUSTOM_CHAT_COLORS_ID,
|
||||
BADGES
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
|
@ -372,7 +376,8 @@ public class RecipientDatabase extends Database {
|
|||
EXTRAS + " BLOB DEFAULT NULL, " +
|
||||
GROUPS_IN_COMMON + " INTEGER DEFAULT 0, " +
|
||||
CHAT_COLORS + " BLOB DEFAULT NULL, " +
|
||||
CUSTOM_CHAT_COLORS_ID + " INTEGER DEFAULT 0);";
|
||||
CUSTOM_CHAT_COLORS_ID + " INTEGER DEFAULT 0, " +
|
||||
BADGES + " BLOB DEFAULT NULL);";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
|
@ -1208,49 +1213,52 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor, @NonNull String idColumnName) {
|
||||
long id = CursorUtil.requireLong(cursor, idColumnName);
|
||||
UUID uuid = UuidUtil.parseOrNull(CursorUtil.requireString(cursor, UUID));
|
||||
String username = CursorUtil.requireString(cursor, USERNAME);
|
||||
String e164 = CursorUtil.requireString(cursor, PHONE);
|
||||
String email = CursorUtil.requireString(cursor, EMAIL);
|
||||
GroupId groupId = GroupId.parseNullableOrThrow(CursorUtil.requireString(cursor, GROUP_ID));
|
||||
int groupType = CursorUtil.requireInt(cursor, GROUP_TYPE);
|
||||
boolean blocked = CursorUtil.requireBoolean(cursor, BLOCKED);
|
||||
String messageRingtone = CursorUtil.requireString(cursor, MESSAGE_RINGTONE);
|
||||
String callRingtone = CursorUtil.requireString(cursor, CALL_RINGTONE);
|
||||
int messageVibrateState = CursorUtil.requireInt(cursor, MESSAGE_VIBRATE);
|
||||
int callVibrateState = CursorUtil.requireInt(cursor, CALL_VIBRATE);
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
int insightsBannerTier = CursorUtil.requireInt(cursor, SEEN_INVITE_REMINDER);
|
||||
int defaultSubscriptionId = CursorUtil.requireInt(cursor, DEFAULT_SUBSCRIPTION_ID);
|
||||
int expireMessages = CursorUtil.requireInt(cursor, MESSAGE_EXPIRATION_TIME);
|
||||
int registeredState = CursorUtil.requireInt(cursor, REGISTERED);
|
||||
String profileKeyString = CursorUtil.requireString(cursor, PROFILE_KEY);
|
||||
String profileKeyCredentialString = CursorUtil.requireString(cursor, PROFILE_KEY_CREDENTIAL);
|
||||
String systemGivenName = CursorUtil.requireString(cursor, SYSTEM_GIVEN_NAME);
|
||||
String systemFamilyName = CursorUtil.requireString(cursor, SYSTEM_FAMILY_NAME);
|
||||
String systemDisplayName = CursorUtil.requireString(cursor, SYSTEM_JOINED_NAME);
|
||||
String systemContactPhoto = CursorUtil.requireString(cursor, SYSTEM_PHOTO_URI);
|
||||
String systemPhoneLabel = CursorUtil.requireString(cursor, SYSTEM_PHONE_LABEL);
|
||||
String systemContactUri = CursorUtil.requireString(cursor, SYSTEM_CONTACT_URI);
|
||||
String profileGivenName = CursorUtil.requireString(cursor, PROFILE_GIVEN_NAME);
|
||||
String profileFamilyName = CursorUtil.requireString(cursor, PROFILE_FAMILY_NAME);
|
||||
String signalProfileAvatar = CursorUtil.requireString(cursor, SIGNAL_PROFILE_AVATAR);
|
||||
boolean profileSharing = CursorUtil.requireBoolean(cursor, PROFILE_SHARING);
|
||||
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
|
||||
String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL);
|
||||
int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE);
|
||||
boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION);
|
||||
long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES);
|
||||
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
|
||||
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
|
||||
byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER);
|
||||
byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS);
|
||||
long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID);
|
||||
String serializedAvatarColor = CursorUtil.requireString(cursor, AVATAR_COLOR);
|
||||
String about = CursorUtil.requireString(cursor, ABOUT);
|
||||
String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI);
|
||||
boolean hasGroupsInCommon = CursorUtil.requireBoolean(cursor, GROUPS_IN_COMMON);
|
||||
long id = CursorUtil.requireLong(cursor, idColumnName);
|
||||
UUID uuid = UuidUtil.parseOrNull(CursorUtil.requireString(cursor, UUID));
|
||||
String username = CursorUtil.requireString(cursor, USERNAME);
|
||||
String e164 = CursorUtil.requireString(cursor, PHONE);
|
||||
String email = CursorUtil.requireString(cursor, EMAIL);
|
||||
GroupId groupId = GroupId.parseNullableOrThrow(CursorUtil.requireString(cursor, GROUP_ID));
|
||||
int groupType = CursorUtil.requireInt(cursor, GROUP_TYPE);
|
||||
boolean blocked = CursorUtil.requireBoolean(cursor, BLOCKED);
|
||||
String messageRingtone = CursorUtil.requireString(cursor, MESSAGE_RINGTONE);
|
||||
String callRingtone = CursorUtil.requireString(cursor, CALL_RINGTONE);
|
||||
int messageVibrateState = CursorUtil.requireInt(cursor, MESSAGE_VIBRATE);
|
||||
int callVibrateState = CursorUtil.requireInt(cursor, CALL_VIBRATE);
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
int insightsBannerTier = CursorUtil.requireInt(cursor, SEEN_INVITE_REMINDER);
|
||||
int defaultSubscriptionId = CursorUtil.requireInt(cursor, DEFAULT_SUBSCRIPTION_ID);
|
||||
int expireMessages = CursorUtil.requireInt(cursor, MESSAGE_EXPIRATION_TIME);
|
||||
int registeredState = CursorUtil.requireInt(cursor, REGISTERED);
|
||||
String profileKeyString = CursorUtil.requireString(cursor, PROFILE_KEY);
|
||||
String profileKeyCredentialString = CursorUtil.requireString(cursor, PROFILE_KEY_CREDENTIAL);
|
||||
String systemGivenName = CursorUtil.requireString(cursor, SYSTEM_GIVEN_NAME);
|
||||
String systemFamilyName = CursorUtil.requireString(cursor, SYSTEM_FAMILY_NAME);
|
||||
String systemDisplayName = CursorUtil.requireString(cursor, SYSTEM_JOINED_NAME);
|
||||
String systemContactPhoto = CursorUtil.requireString(cursor, SYSTEM_PHOTO_URI);
|
||||
String systemPhoneLabel = CursorUtil.requireString(cursor, SYSTEM_PHONE_LABEL);
|
||||
String systemContactUri = CursorUtil.requireString(cursor, SYSTEM_CONTACT_URI);
|
||||
String profileGivenName = CursorUtil.requireString(cursor, PROFILE_GIVEN_NAME);
|
||||
String profileFamilyName = CursorUtil.requireString(cursor, PROFILE_FAMILY_NAME);
|
||||
String signalProfileAvatar = CursorUtil.requireString(cursor, SIGNAL_PROFILE_AVATAR);
|
||||
boolean profileSharing = CursorUtil.requireBoolean(cursor, PROFILE_SHARING);
|
||||
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
|
||||
String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL);
|
||||
int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE);
|
||||
boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION);
|
||||
long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES);
|
||||
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
|
||||
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
|
||||
byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER);
|
||||
byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS);
|
||||
long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID);
|
||||
String serializedAvatarColor = CursorUtil.requireString(cursor, AVATAR_COLOR);
|
||||
String about = CursorUtil.requireString(cursor, ABOUT);
|
||||
String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI);
|
||||
boolean hasGroupsInCommon = CursorUtil.requireBoolean(cursor, GROUPS_IN_COMMON);
|
||||
byte[] serializedBadgeList = CursorUtil.requireBlob(cursor, BADGES);
|
||||
|
||||
List<Badge> badges = parseBadgeList(serializedBadgeList);
|
||||
|
||||
byte[] profileKey = null;
|
||||
ProfileKeyCredential profileKeyCredential = null;
|
||||
|
@ -1343,7 +1351,40 @@ public class RecipientDatabase extends Database {
|
|||
aboutEmoji,
|
||||
getSyncExtras(cursor),
|
||||
getExtras(cursor),
|
||||
hasGroupsInCommon);
|
||||
hasGroupsInCommon,
|
||||
badges);
|
||||
}
|
||||
|
||||
private static @NonNull List<Badge> parseBadgeList(byte[] serializedBadgeList) {
|
||||
BadgeList badgeList = null;
|
||||
if (serializedBadgeList != null) {
|
||||
try {
|
||||
badgeList = BadgeList.parseFrom(serializedBadgeList);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
List<Badge> badges;
|
||||
if (badgeList != null) {
|
||||
List<BadgeList.Badge> protoBadges = badgeList.getBadgesList();
|
||||
badges = new ArrayList<>(protoBadges.size());
|
||||
for (BadgeList.Badge protoBadge : protoBadges) {
|
||||
badges.add(new Badge(
|
||||
protoBadge.getId(),
|
||||
Badge.Category.Companion.fromCode(protoBadge.getCategory()),
|
||||
Uri.parse(protoBadge.getImageUrl()),
|
||||
protoBadge.getName(),
|
||||
protoBadge.getDescription(),
|
||||
protoBadge.getExpiration(),
|
||||
protoBadge.getVisible()
|
||||
));
|
||||
}
|
||||
} else {
|
||||
badges = Collections.emptyList();
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) {
|
||||
|
@ -1639,6 +1680,28 @@ public class RecipientDatabase extends Database {
|
|||
return DeviceLastResetTime.newBuilder().build();
|
||||
}
|
||||
|
||||
public void setBadges(@NonNull RecipientId id, @NonNull List<Badge> badges) {
|
||||
BadgeList.Builder badgeListBuilder = BadgeList.newBuilder();
|
||||
|
||||
for (final Badge badge : badges) {
|
||||
badgeListBuilder.addBadges(BadgeList.Badge.newBuilder()
|
||||
.setId(badge.getId())
|
||||
.setCategory(badge.getCategory().getCode())
|
||||
.setDescription(badge.getDescription())
|
||||
.setExpiration(badge.getExpirationTimestamp())
|
||||
.setVisible(badge.getVisible())
|
||||
.setName(badge.getName())
|
||||
.setImageUrl(badge.getImageUrl().toString()));
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(BADGES, badgeListBuilder.build().toByteArray());
|
||||
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) {
|
||||
long value = 0;
|
||||
|
||||
|
@ -3228,6 +3291,7 @@ public class RecipientDatabase extends Database {
|
|||
private final SyncExtras syncExtras;
|
||||
private final Recipient.Extras extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final List<Badge> badges;
|
||||
|
||||
RecipientSettings(@NonNull RecipientId id,
|
||||
@Nullable UUID uuid,
|
||||
|
@ -3271,7 +3335,8 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable String aboutEmoji,
|
||||
@NonNull SyncExtras syncExtras,
|
||||
@Nullable Recipient.Extras extras,
|
||||
boolean hasGroupsInCommon)
|
||||
boolean hasGroupsInCommon,
|
||||
@NonNull List<Badge> badges)
|
||||
{
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
|
@ -3318,9 +3383,10 @@ public class RecipientDatabase extends Database {
|
|||
this.avatarColor = avatarColor;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.syncExtras = syncExtras;
|
||||
this.extras = extras;
|
||||
this.hasGroupsInCommon = hasGroupsInCommon;
|
||||
this.syncExtras = syncExtras;
|
||||
this.extras = extras;
|
||||
this.hasGroupsInCommon = hasGroupsInCommon;
|
||||
this.badges = badges;
|
||||
}
|
||||
|
||||
public RecipientId getId() {
|
||||
|
@ -3511,6 +3577,10 @@ public class RecipientDatabase extends Database {
|
|||
return hasGroupsInCommon;
|
||||
}
|
||||
|
||||
public @NonNull List<Badge> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
long getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
|
|
@ -216,8 +216,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
private static final int GROUP_CALL_RING_TABLE = 115;
|
||||
private static final int CLEANUP_SESSION_MIGRATION = 116;
|
||||
private static final int RECEIPT_TIMESTAMP = 117;
|
||||
private static final int BADGES = 118;
|
||||
|
||||
private static final int DATABASE_VERSION = 117;
|
||||
private static final int DATABASE_VERSION = 118;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -2043,6 +2044,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
db.execSQL("ALTER TABLE mms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1");
|
||||
}
|
||||
|
||||
if (oldVersion < BADGES) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN badges BLOB DEFAULT NULL");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceTrustStore;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* A simple model loader for fetching media over http/https using OkHttp.
|
||||
*/
|
||||
public class BadgeLoader implements ModelLoader<Badge, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private BadgeLoader(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull Badge badge, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(badge, new OkHttpStreamFetcher(client, new GlideUrl(badge.getImageUrl().toString())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull Badge badge) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Factory createFactory() {
|
||||
try {
|
||||
OkHttpClient baseClient = ApplicationDependencies.getOkHttpClient();
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
TrustStore trustStore = new SignalServiceTrustStore(ApplicationDependencies.getApplication());
|
||||
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(trustStore);
|
||||
|
||||
sslContext.init(null, trustManagers, null);
|
||||
|
||||
OkHttpClient client = baseClient.newBuilder()
|
||||
.sslSocketFactory(new Tls12SocketFactory(sslContext.getSocketFactory()), (X509TrustManager) trustManagers[0])
|
||||
.connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))
|
||||
.build();
|
||||
|
||||
return new Factory(client);
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<Badge, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private Factory(@NonNull OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<Badge, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new BadgeLoader(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -8,6 +9,7 @@ import androidx.annotation.Nullable;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -28,6 +30,10 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -83,13 +89,14 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
}
|
||||
|
||||
Recipient self = Recipient.self();
|
||||
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfileSync(context, self, getRequestType(self));
|
||||
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfileSync(context, self, getRequestType(self), false);
|
||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||
|
||||
setProfileName(profile.getName());
|
||||
setProfileAbout(profile.getAbout(), profile.getAboutEmoji());
|
||||
setProfileAvatar(profile.getAvatar());
|
||||
setProfileCapabilities(profile.getCapabilities());
|
||||
setProfileBadges(profile.getBadges());
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get());
|
||||
|
@ -159,6 +166,32 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
DatabaseFactory.getRecipientDatabase(context).setCapabilities(Recipient.self().getId(), capabilities);
|
||||
}
|
||||
|
||||
private void setProfileBadges(@Nullable List<SignalServiceProfile.Badge> badges) {
|
||||
if (badges == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setBadges(Recipient.self().getId(),
|
||||
badges.stream().map(RefreshOwnProfileJob::adaptFromServiceBadge).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static Badge adaptFromServiceBadge(@NonNull SignalServiceProfile.Badge serviceBadge) {
|
||||
return new Badge(
|
||||
serviceBadge.getId(),
|
||||
Badge.Category.Companion.fromCode(serviceBadge.getCategory()),
|
||||
Uri.parse(serviceBadge.getImageUrl()),
|
||||
serviceBadge.getName(),
|
||||
serviceBadge.getDescription(),
|
||||
getTimestamp(serviceBadge.getExpiration()),
|
||||
serviceBadge.isVisible()
|
||||
);
|
||||
}
|
||||
|
||||
private static long getTimestamp(@NonNull BigDecimal bigDecimal) {
|
||||
return new Timestamp(bigDecimal.longValue() * 1000).getTime();
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RefreshOwnProfileJob> {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
|
|||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -15,6 +16,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
|
@ -32,6 +34,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
|
@ -329,6 +332,7 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
setProfileName(recipient, profile.getName());
|
||||
setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji());
|
||||
setProfileAvatar(recipient, profile.getAvatar());
|
||||
setProfileBadges(recipient, profile.getBadges());
|
||||
clearUsername(recipient);
|
||||
setProfileCapabilities(recipient, profile.getCapabilities());
|
||||
setIdentityKey(recipient, profile.getIdentityKey());
|
||||
|
@ -342,6 +346,28 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private void setProfileBadges(@NonNull Recipient recipient, @Nullable List<SignalServiceProfile.Badge> badges) {
|
||||
if (badges == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setBadges(recipient.getId(),
|
||||
badges.stream().map(RetrieveProfileJob::adaptFromServiceBadge).collect(java.util.stream.Collectors.toList()));
|
||||
}
|
||||
|
||||
private static Badge adaptFromServiceBadge(@NonNull SignalServiceProfile.Badge serviceBadge) {
|
||||
return new Badge(
|
||||
serviceBadge.getId(),
|
||||
Badge.Category.Companion.fromCode(serviceBadge.getCategory()),
|
||||
Uri.parse(serviceBadge.getImageUrl()),
|
||||
serviceBadge.getName(),
|
||||
serviceBadge.getDescription(),
|
||||
0L,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private void setProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ProfileKeyCredential credential)
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
|||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHashModelLoader;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHashResourceDecoder;
|
||||
|
@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.glide.BadgeLoader;
|
||||
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
||||
|
@ -97,6 +99,7 @@ public class SignalGlideModule extends AppGlideModule {
|
|||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory());
|
||||
registry.append(BlurHash.class, BlurHash.class, new BlurHashModelLoader.Factory());
|
||||
registry.append(Badge.class, InputStream.class, BadgeLoader.createFactory());
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
|||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.NameUtil;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
|
@ -51,6 +52,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
private AlertDialog avatarProgress;
|
||||
private TextView avatarInitials;
|
||||
private ImageView avatarBackground;
|
||||
private View badgesContainer;
|
||||
|
||||
private ManageProfileViewModel viewModel;
|
||||
|
||||
|
@ -73,6 +75,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
|
||||
this.avatarInitials = view.findViewById(R.id.manage_profile_avatar_initials);
|
||||
this.avatarBackground = view.findViewById(R.id.manage_profile_avatar_background);
|
||||
this.badgesContainer = view.findViewById(R.id.manage_profile_badges_container);
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
|
@ -105,6 +108,14 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
updateInitials(avatarInitials.getText().toString());
|
||||
}
|
||||
});
|
||||
|
||||
if (FeatureFlags.donorBadges()) {
|
||||
badgesContainer.setOnClickListener(v -> {
|
||||
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageProfileFragmentToBadgeManageFragment());
|
||||
});
|
||||
} else {
|
||||
badgesContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.annimon.stream.Stream;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
|
@ -124,6 +125,7 @@ public class Recipient {
|
|||
private final String systemContactName;
|
||||
private final Optional<Extras> extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final List<Badge> badges;
|
||||
|
||||
/**
|
||||
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
|
||||
|
@ -376,6 +378,7 @@ public class Recipient {
|
|||
this.systemContactName = null;
|
||||
this.extras = Optional.absent();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
}
|
||||
|
||||
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
|
||||
|
@ -429,6 +432,7 @@ public class Recipient {
|
|||
this.systemContactName = details.systemContactName;
|
||||
this.extras = details.extras;
|
||||
this.hasGroupsInCommon = details.hasGroupsInCommon;
|
||||
this.badges = details.badges;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
|
@ -1023,6 +1027,10 @@ public class Recipient {
|
|||
return aboutEmoji;
|
||||
}
|
||||
|
||||
public @NonNull List<Badge> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
public @Nullable String getCombinedAboutAndEmoji() {
|
||||
if (!Util.isEmpty(aboutEmoji)) {
|
||||
if (!Util.isEmpty(about)) {
|
||||
|
@ -1202,7 +1210,8 @@ public class Recipient {
|
|||
Objects.equals(about, other.about) &&
|
||||
Objects.equals(aboutEmoji, other.aboutEmoji) &&
|
||||
Objects.equals(extras, other.extras) &&
|
||||
hasGroupsInCommon == other.hasGroupsInCommon;
|
||||
hasGroupsInCommon == other.hasGroupsInCommon &&
|
||||
Objects.equals(badges, other.badges);
|
||||
}
|
||||
|
||||
private static boolean allContentsAreTheSame(@NonNull List<Recipient> a, @NonNull List<Recipient> b) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
|
@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -77,6 +79,7 @@ public class RecipientDetails {
|
|||
final ProfileName systemProfileName;
|
||||
final Optional<Recipient.Extras> extras;
|
||||
final boolean hasGroupsInCommon;
|
||||
final List<Badge> badges;
|
||||
|
||||
public RecipientDetails(@Nullable String groupName,
|
||||
@Nullable String systemContactName,
|
||||
|
@ -136,6 +139,7 @@ public class RecipientDetails {
|
|||
this.systemContactName = systemContactName;
|
||||
this.extras = Optional.fromNullable(settings.getExtras());
|
||||
this.hasGroupsInCommon = settings.hasGroupsInCommon();
|
||||
this.badges = settings.getBadges();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,6 +195,7 @@ public class RecipientDetails {
|
|||
this.systemContactName = null;
|
||||
this.extras = Optional.absent();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
|
||||
object Environment {
|
||||
const val IS_STAGING: Boolean = BuildConfig.BUILD_ENVIRONMENT_TYPE == "Staging"
|
||||
}
|
|
@ -84,6 +84,7 @@ public final class FeatureFlags {
|
|||
private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize";
|
||||
private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging";
|
||||
private static final String CHANGE_NUMBER_ENABLED = "android.changeNumber";
|
||||
private static final String DONOR_BADGES = "android.donorBadges";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -126,7 +127,8 @@ public final class FeatureFlags {
|
|||
@VisibleForTesting
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
|
||||
PHONE_NUMBER_PRIVACY_VERSION,
|
||||
CHANGE_NUMBER_ENABLED
|
||||
CHANGE_NUMBER_ENABLED,
|
||||
DONOR_BADGES
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -394,11 +396,20 @@ public final class FeatureFlags {
|
|||
return getBoolean(GROUP_CALL_RINGING, false);
|
||||
}
|
||||
|
||||
/** Weather or not to show change number in the UI. */
|
||||
/** Whether or not to show change number in the UI. */
|
||||
public static boolean changeNumber() {
|
||||
return getBoolean(CHANGE_NUMBER_ENABLED, false);
|
||||
}
|
||||
|
||||
/** Whether or not to show donor badges in the UI. */
|
||||
public static boolean donorBadges() {
|
||||
if (Environment.IS_STAGING) {
|
||||
return true;
|
||||
} else {
|
||||
return getBoolean(DONOR_BADGES, false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.WorkerThread;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
|
@ -42,6 +43,8 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
|
|||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
|
@ -60,12 +63,22 @@ public final class ProfileUtil {
|
|||
@NonNull Recipient recipient,
|
||||
@NonNull SignalServiceProfile.RequestType requestType)
|
||||
throws IOException
|
||||
{
|
||||
return retrieveProfileSync(context, recipient, requestType, true);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull ProfileAndCredential retrieveProfileSync(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull SignalServiceProfile.RequestType requestType,
|
||||
boolean allowUnidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(),
|
||||
ApplicationDependencies.getSignalServiceMessageReceiver(),
|
||||
ApplicationDependencies.getSignalWebSocket());
|
||||
|
||||
Pair<Recipient, ServiceResponse<ProfileAndCredential>> response = retrieveProfile(context, recipient, requestType, profileService).blockingGet();
|
||||
Pair<Recipient, ServiceResponse<ProfileAndCredential>> response = retrieveProfile(context, recipient, requestType, profileService, allowUnidentifiedAccess).blockingGet();
|
||||
return new ProfileService.ProfileResponseProcessor(response.second()).getResultOrThrow();
|
||||
}
|
||||
|
||||
|
@ -74,7 +87,16 @@ public final class ProfileUtil {
|
|||
@NonNull SignalServiceProfile.RequestType requestType,
|
||||
@NonNull ProfileService profileService)
|
||||
{
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
|
||||
return retrieveProfile(context, recipient, requestType, profileService, true);
|
||||
}
|
||||
|
||||
private static Single<Pair<Recipient, ServiceResponse<ProfileAndCredential>>> retrieveProfile(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull SignalServiceProfile.RequestType requestType,
|
||||
@NonNull ProfileService profileService,
|
||||
boolean allowUnidentifiedAccess)
|
||||
{
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.absent();
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
|
||||
return Single.fromCallable(() -> toSignalServiceAddress(context, recipient))
|
||||
|
@ -163,6 +185,23 @@ public final class ProfileUtil {
|
|||
return profileKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the profile based on all state that's written to disk, except we'll use the provided
|
||||
* list of badges instead. This is useful when you want to ensure that the profile has been uploaded
|
||||
* successfully before persisting the change to disk.
|
||||
*/
|
||||
public static void uploadProfileWithBadges(@NonNull Context context, @NonNull List<Badge> badges) throws IOException {
|
||||
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
|
||||
uploadProfile(context,
|
||||
Recipient.self().getProfileName(),
|
||||
Optional.fromNullable(Recipient.self().getAbout()).or(""),
|
||||
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
|
||||
getSelfPaymentsAddressProtobuf(),
|
||||
avatar,
|
||||
badges);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the profile based on all state that's written to disk, except we'll use the provided
|
||||
* profile name instead. This is useful when you want to ensure that the profile has been uploaded
|
||||
|
@ -175,7 +214,8 @@ public final class ProfileUtil {
|
|||
Optional.fromNullable(Recipient.self().getAbout()).or(""),
|
||||
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
|
||||
getSelfPaymentsAddressProtobuf(),
|
||||
avatar);
|
||||
avatar,
|
||||
Recipient.self().getBadges());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,7 +231,8 @@ public final class ProfileUtil {
|
|||
about,
|
||||
emoji,
|
||||
getSelfPaymentsAddressProtobuf(),
|
||||
avatar);
|
||||
avatar,
|
||||
Recipient.self().getBadges());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +256,8 @@ public final class ProfileUtil {
|
|||
Optional.fromNullable(Recipient.self().getAbout()).or(""),
|
||||
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
|
||||
getSelfPaymentsAddressProtobuf(),
|
||||
avatar);
|
||||
avatar,
|
||||
Recipient.self().getBadges());
|
||||
}
|
||||
|
||||
private static void uploadProfile(@NonNull Context context,
|
||||
|
@ -223,13 +265,21 @@ public final class ProfileUtil {
|
|||
@Nullable String about,
|
||||
@Nullable String aboutEmoji,
|
||||
@Nullable SignalServiceProtos.PaymentAddress paymentsAddress,
|
||||
@Nullable StreamDetails avatar)
|
||||
@Nullable StreamDetails avatar,
|
||||
@NonNull List<Badge> badges)
|
||||
throws IOException
|
||||
{
|
||||
|
||||
List<String> badgeIds = badges.stream()
|
||||
.filter(Badge::getVisible)
|
||||
.map(Badge::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Log.d(TAG, "Uploading " + (!Util.isEmpty(about) ? "non-" : "") + "empty about.");
|
||||
Log.d(TAG, "Uploading " + (!Util.isEmpty(aboutEmoji) ? "non-" : "") + "empty emoji.");
|
||||
Log.d(TAG, "Uploading " + (paymentsAddress != null ? "non-" : "") + "empty payments address.");
|
||||
Log.d(TAG, "Uploading " + (avatar != null && avatar.getLength() != 0 ? "non-" : "") + "empty avatar.");
|
||||
Log.d(TAG, "Uploading " + ((!badgeIds.isEmpty()) ? "non-" : "") + "empty badge list");
|
||||
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
|
@ -239,7 +289,8 @@ public final class ProfileUtil {
|
|||
about,
|
||||
aboutEmoji,
|
||||
Optional.fromNullable(paymentsAddress),
|
||||
avatar).orNull();
|
||||
avatar,
|
||||
badgeIds).orNull();
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), avatarPath);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,20 @@ message ReactionList {
|
|||
repeated Reaction reactions = 1;
|
||||
}
|
||||
|
||||
message BadgeList {
|
||||
message Badge {
|
||||
string id = 1;
|
||||
string category = 2;
|
||||
string name = 3;
|
||||
string description = 4;
|
||||
string imageUrl = 5;
|
||||
uint64 expiration = 6;
|
||||
bool visible = 7;
|
||||
}
|
||||
|
||||
repeated Badge badges = 1;
|
||||
}
|
||||
|
||||
|
||||
import "SignalService.proto";
|
||||
import "DecryptedGroups.proto";
|
||||
|
|
8
app/src/main/res/drawable/default_dot.xml
Normal file
8
app/src/main/res/drawable/default_dot.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:innerRadius="0dp"
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="false">
|
||||
<solid android:color="@color/signal_inverse_transparent_20" />
|
||||
</shape>
|
8
app/src/main/res/drawable/selected_dot.xml
Normal file
8
app/src/main/res/drawable/selected_dot.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:innerRadius="0dp"
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="false">
|
||||
<solid android:color="@color/signal_accent_primary" />
|
||||
</shape>
|
6
app/src/main/res/drawable/tab_selector.xml
Normal file
6
app/src/main/res/drawable/tab_selector.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/selected_dot" android:state_selected="true" />
|
||||
|
||||
<item android:drawable="@drawable/default_dot" />
|
||||
</selector>
|
36
app/src/main/res/layout/badge_preference_view.xml
Normal file
36
app/src/main/res/layout/badge_preference_view.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/circle_ultramarine" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:lineSpacingExtra="4sp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/badge"
|
||||
app:layout_constraintStart_toStartOf="@id/badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/badge"
|
||||
app:layout_constraintWidth_max="64dp"
|
||||
tools:text="Signal Sustainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="33dp"
|
||||
android:contentDescription="@string/BadgesOverviewFragment__featured_badge"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/avatar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -227,6 +227,53 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/manage_profile_badges_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:background="?selectableItemBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/manage_profile_about_container">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/manage_profile_badges_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_compose_24"
|
||||
app:tint="@color/signal_text_primary"
|
||||
app:layout_constraintTop_toTopOf="@id/manage_profile_badges"
|
||||
app:layout_constraintBottom_toBottomOf="@id/manage_profile_badges_subtitle"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/manage_profile_badges"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/manage_profile_badges_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@string/ManageProfileFragment_badges"
|
||||
app:emoji_forceCustom="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manage_profile_badges_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/manage_profile_badges"
|
||||
app:layout_constraintStart_toStartOf="@id/manage_profile_badges"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
android:id="@+id/description_text"
|
||||
style="@style/Signal.Text.Preview"
|
||||
|
@ -239,7 +286,7 @@
|
|||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/manage_profile_about_container"
|
||||
app:layout_constraintTop_toBottomOf="@+id/manage_profile_badges_container"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
76
app/src/main/res/layout/select_featured_badge_fragment.xml
Normal file
76
app/src/main/res/layout/select_featured_badge_fragment.xml
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/section_header_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
android:paddingBottom="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/section_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/SelectFeaturedBadgeFragment__preview"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
android:textStyle="bold" />
|
||||
</FrameLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/preview"
|
||||
layout="@layout/featured_badge_preview_preference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/section_header_container" />
|
||||
|
||||
<View
|
||||
android:id="@+id/scroll_shadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/toolbar_shadow"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/recycler" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="56dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/preview" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/save"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/save"
|
||||
app:cornerRadius="68dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pull_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:src="@drawable/bottom_sheet_handle"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabBackground="@drawable/tab_selector"
|
||||
app:tabGravity="center"
|
||||
app:tabIndicatorHeight="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/action"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/ViewBadgeBottomSheetDialogFragment__become_a_sustainer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,48 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
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_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
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:lineSpacingExtra="6sp"
|
||||
android:lines="3"
|
||||
android:maxLines="3"
|
||||
android:minLines="3"
|
||||
android:paddingStart="32dp"
|
||||
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>
|
|
@ -9,7 +9,7 @@
|
|||
android:id="@+id/manageProfileFragment"
|
||||
android:name="org.thoughtcrime.securesms.profiles.manage.ManageProfileFragment"
|
||||
android:label="fragment_manage_profile"
|
||||
tools:layout="@layout/profile_create_fragment">
|
||||
tools:layout="@layout/manage_profile_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_manageUsername"
|
||||
|
@ -55,6 +55,14 @@
|
|||
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_manageProfileFragment_to_badgeManageFragment"
|
||||
app:destination="@id/badgeManageFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -75,6 +83,26 @@
|
|||
android:label="fragment_manage_about"
|
||||
tools:layout="@layout/edit_about_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/badgeManageFragment"
|
||||
android:name="org.thoughtcrime.securesms.badges.self.overview.BadgesOverviewFragment"
|
||||
android:label="fragment_manage_badges" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_badgeManageFragment_to_featuredBadgeFragment"
|
||||
app:destination="@id/featuredBadgeFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/featuredBadgeFragment"
|
||||
android:name="org.thoughtcrime.securesms.badges.self.featured.SelectFeaturedBadgeFragment"
|
||||
android:label="fragment_featured_badge" />
|
||||
|
||||
<include app:graph="@navigation/avatar_picker" />
|
||||
|
||||
</navigation>
|
|
@ -775,6 +775,7 @@
|
|||
<string name="ManageProfileFragment_your_name">Your name</string>
|
||||
<string name="ManageProfileFragment_your_username">Your username</string>
|
||||
<string name="ManageProfileFragment_failed_to_set_avatar">Failed to set avatar</string>
|
||||
<string name="ManageProfileFragment_badges">Badges</string>
|
||||
|
||||
<!-- ManageRecipientActivity -->
|
||||
<string name="ManageRecipientActivity_no_groups_in_common">No groups in common</string>
|
||||
|
@ -3812,6 +3813,7 @@
|
|||
<string name="MediaReviewFragment__add_a_message">Add a message</string>
|
||||
<string name="MediaReviewFragment__add_a_reply">Add a reply</string>
|
||||
<string name="MediaReviewFragment__send_to">Send to</string>
|
||||
<string name="MediaReviewFragment__view_once_message">View once message</string>
|
||||
|
||||
<string name="ImageEditorHud__cancel">Cancel</string>
|
||||
<string name="ImageEditorHud__draw">Draw</string>
|
||||
|
@ -3822,18 +3824,30 @@
|
|||
<string name="ImageEditorHud__clear_all">Clear all</string>
|
||||
<string name="ImageEditorHud__undo">Undo</string>
|
||||
<string name="ImageEditorHud__toggle_between_marker_and_highlighter">Toggle between marker and highlighter</string>
|
||||
<string name="ImageEditorHud__delete">Delete</string>
|
||||
<string name="ImageEditorHud__toggle_between_text_styles">Toggle between text styles</string>
|
||||
|
||||
<string name="MediaCountIndicatorButton__send">Send</string>
|
||||
|
||||
<string name="MediaReviewSelectedItem__tap_to_remove">Tap to remove</string>
|
||||
<string name="MediaReviewSelectedItem__tap_to_select">Tap to select</string>
|
||||
|
||||
<string name="MediaReviewImagePageFragment__discard">Discard</string>
|
||||
<string name="MediaReviewImagePageFragment__discard_changes">Discard changes?</string>
|
||||
<string name="MediaReviewFragment__view_once_message">View once message</string>
|
||||
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
||||
<string name="ImageEditorHud__delete">Delete</string>
|
||||
|
||||
<string name="CameraFragment__failed_to_open_camera">Failed to open camera</string>
|
||||
<string name="ImageEditorHud__toggle_between_text_styles">Toggle between text styles</string>
|
||||
|
||||
<string name="BadgesOverviewFragment__my_badges">My badges</string>
|
||||
<string name="BadgesOverviewFragment__featured_badge">Featured badge</string>
|
||||
<string name="BadgesOverviewFragment__display_badges_on_profile">Display badges on profile</string>
|
||||
<string name="BadgesOverviewFragment__failed_to_update_profile">Failed to update profile</string>
|
||||
|
||||
|
||||
<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="ViewBadgeBottomSheetDialogFragment__become_a_sustainer">Become a sustainer</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||
|
||||
import android.net.Uri
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
|
@ -75,7 +76,8 @@ object RecipientDatabaseTestUtils {
|
|||
false
|
||||
),
|
||||
extras: Recipient.Extras? = null,
|
||||
hasGroupsInCommon: Boolean = false
|
||||
hasGroupsInCommon: Boolean = false,
|
||||
badges: List<Badge> = emptyList()
|
||||
): Recipient = Recipient(
|
||||
recipientId,
|
||||
RecipientDetails(
|
||||
|
@ -128,7 +130,8 @@ object RecipientDatabaseTestUtils {
|
|||
aboutEmoji,
|
||||
syncExtras,
|
||||
extras,
|
||||
hasGroupsInCommon
|
||||
hasGroupsInCommon,
|
||||
badges
|
||||
),
|
||||
participants
|
||||
),
|
||||
|
|
|
@ -183,10 +183,6 @@ public final class MainActivity extends AppCompatActivity {
|
|||
imageEditorView.startDrawing(0.02f, Paint.Cap.ROUND, false);
|
||||
return true;
|
||||
|
||||
case R.id.action_rotate_right_90:
|
||||
imageEditorView.getModel().rotate90clockwise();
|
||||
return true;
|
||||
|
||||
case R.id.action_rotate_left_90:
|
||||
imageEditorView.getModel().rotate90anticlockwise();
|
||||
return true;
|
||||
|
@ -195,10 +191,6 @@ public final class MainActivity extends AppCompatActivity {
|
|||
imageEditorView.getModel().flipHorizontal();
|
||||
return true;
|
||||
|
||||
case R.id.action_flip_vertical:
|
||||
imageEditorView.getModel().flipVertical();
|
||||
return true;
|
||||
|
||||
case R.id.action_edit_text:
|
||||
editText();
|
||||
return true;
|
||||
|
|
|
@ -37,12 +37,6 @@
|
|||
android:title="@string/draw"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rotate_right_90"
|
||||
android:icon="@drawable/ic_rotate_right_black_24dp"
|
||||
android:title="@string/rotate_90_right"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rotate_left_90"
|
||||
android:icon="@drawable/ic_rotate_left_black_24dp"
|
||||
|
@ -55,11 +49,6 @@
|
|||
android:title="@string/flip_horizontal"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_flip_vertical"
|
||||
android:title="@string/flip_vertical"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_text"
|
||||
android:title="@string/edit_text"
|
||||
|
|
|
@ -722,7 +722,8 @@ public class SignalServiceAccountManager {
|
|||
String about,
|
||||
String aboutEmoji,
|
||||
Optional<SignalServiceProtos.PaymentAddress> paymentsAddress,
|
||||
StreamDetails avatar)
|
||||
StreamDetails avatar,
|
||||
List<String> visibleBadgeIds)
|
||||
throws IOException
|
||||
{
|
||||
if (name == null) name = "";
|
||||
|
@ -748,7 +749,8 @@ public class SignalServiceAccountManager {
|
|||
ciphertextEmoji,
|
||||
ciphertextMobileCoinAddress,
|
||||
hasAvatar,
|
||||
profileKey.getCommitment(uuid).serialize()),
|
||||
profileKey.getCommitment(uuid).serialize(),
|
||||
visibleBadgeIds),
|
||||
profileAvatarData);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.profiles;
|
|||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
@ -12,6 +13,8 @@ import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
|||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SignalServiceProfile {
|
||||
|
@ -58,6 +61,9 @@ public class SignalServiceProfile {
|
|||
@JsonProperty
|
||||
private byte[] credential;
|
||||
|
||||
@JsonProperty
|
||||
private List<Badge> badges;
|
||||
|
||||
@JsonIgnore
|
||||
private RequestType requestType;
|
||||
|
||||
|
@ -99,6 +105,10 @@ public class SignalServiceProfile {
|
|||
return capabilities;
|
||||
}
|
||||
|
||||
public List<Badge> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
@ -111,6 +121,57 @@ public class SignalServiceProfile {
|
|||
this.requestType = requestType;
|
||||
}
|
||||
|
||||
public static class Badge {
|
||||
@JsonProperty
|
||||
private String id;
|
||||
|
||||
@JsonProperty
|
||||
private String category;
|
||||
|
||||
@JsonProperty
|
||||
private String imageUrl;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String description;
|
||||
|
||||
@JsonProperty
|
||||
private BigDecimal expiration;
|
||||
|
||||
@JsonProperty
|
||||
private boolean visible;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public BigDecimal getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Capabilities {
|
||||
@JsonProperty
|
||||
private boolean gv2;
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.whispersystems.signalservice.api.profiles;
|
|||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SignalServiceProfileWrite {
|
||||
|
||||
@JsonProperty
|
||||
|
@ -26,11 +28,14 @@ public class SignalServiceProfileWrite {
|
|||
@JsonProperty
|
||||
private byte[] commitment;
|
||||
|
||||
@JsonProperty
|
||||
private List<String> badgeIds;
|
||||
|
||||
@JsonCreator
|
||||
public SignalServiceProfileWrite(){
|
||||
}
|
||||
|
||||
public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, boolean avatar, byte[] commitment) {
|
||||
public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, boolean avatar, byte[] commitment, List<String> badgeIds) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
|
@ -38,6 +43,7 @@ public class SignalServiceProfileWrite {
|
|||
this.paymentAddress = paymentAddress;
|
||||
this.avatar = avatar;
|
||||
this.commitment = commitment;
|
||||
this.badgeIds = badgeIds;
|
||||
}
|
||||
|
||||
public boolean hasAvatar() {
|
||||
|
|
Loading…
Add table
Reference in a new issue