kopia lustrzana https://github.com/ryukoposting/Signal-Android
Adjust new avatar picker logic.
* Better emoji rendering support * Deleting an avatar will deselect it * Added padding to the bottom of recyclers * Disabled save if no edit / selection has been made. * Clearing and saving will remove a user's avatar.fork-5.53.8
rodzic
a75f634c0a
commit
a27d60f830
|
@ -24,8 +24,8 @@ jobs:
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Remove Android S
|
- name: Remove Android 31 (S)
|
||||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-S"
|
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-31"
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew qa
|
run: ./gradlew qa
|
||||||
|
|
|
@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.avatar
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import com.airbnb.lottie.SimpleColorFilter
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
|
@ -28,7 +26,7 @@ import javax.annotation.meta.Exhaustive
|
||||||
*/
|
*/
|
||||||
object AvatarRenderer {
|
object AvatarRenderer {
|
||||||
|
|
||||||
private val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
|
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
|
||||||
|
|
||||||
fun getTypeface(context: Context): Typeface {
|
fun getTypeface(context: Context): Typeface {
|
||||||
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
|
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
|
||||||
|
@ -50,30 +48,8 @@ object AvatarRenderer {
|
||||||
avatar: Avatar.Text,
|
avatar: Avatar.Text,
|
||||||
inverted: Boolean = false,
|
inverted: Boolean = false,
|
||||||
size: Int = DIMENSIONS,
|
size: Int = DIMENSIONS,
|
||||||
isRect: Boolean = true
|
|
||||||
): Drawable {
|
): Drawable {
|
||||||
val typeface = getTypeface(context)
|
return TextAvatarDrawable(context, avatar, inverted, size)
|
||||||
val color: Int = if (inverted) {
|
|
||||||
avatar.color.backgroundColor
|
|
||||||
} else {
|
|
||||||
avatar.color.foregroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = TextDrawable
|
|
||||||
.builder()
|
|
||||||
.beginConfig()
|
|
||||||
.fontSize(Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f).toInt())
|
|
||||||
.textColor(color)
|
|
||||||
.useFont(typeface)
|
|
||||||
.width(size)
|
|
||||||
.height(size)
|
|
||||||
.endConfig()
|
|
||||||
|
|
||||||
return if (isRect) {
|
|
||||||
builder.buildRect(avatar.text, Color.TRANSPARENT)
|
|
||||||
} else {
|
|
||||||
builder.buildRound(avatar.text, Color.TRANSPARENT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.thoughtcrime.securesms.avatar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
|
||||||
|
*/
|
||||||
|
class TextAvatarDrawable(
|
||||||
|
context: Context,
|
||||||
|
avatar: Avatar.Text,
|
||||||
|
inverted: Boolean = false,
|
||||||
|
private val size: Int = AvatarRenderer.DIMENSIONS,
|
||||||
|
) : Drawable() {
|
||||||
|
|
||||||
|
private val layout: FrameLayout = FrameLayout(context)
|
||||||
|
private val textView: EmojiTextView = EmojiTextView(context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||||
|
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
|
||||||
|
textView.text = avatar.text
|
||||||
|
textView.gravity = Gravity.CENTER
|
||||||
|
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
|
||||||
|
|
||||||
|
layout.addView(textView)
|
||||||
|
|
||||||
|
textView.updateLayoutParams {
|
||||||
|
width = size
|
||||||
|
height = size
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.measure(size, size)
|
||||||
|
layout.layout(0, 0, size, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicHeight(): Int = size
|
||||||
|
|
||||||
|
override fun getIntrinsicWidth(): Int = size
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
layout.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) = Unit
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||||
|
|
||||||
|
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||||
companion object {
|
companion object {
|
||||||
const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR"
|
const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR"
|
||||||
const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA"
|
const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA"
|
||||||
|
const val SELECT_AVATAR_CLEAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_CLEAR"
|
||||||
|
|
||||||
private const val REQUEST_CODE_SELECT_IMAGE = 1
|
private const val REQUEST_CODE_SELECT_IMAGE = 1
|
||||||
}
|
}
|
||||||
|
@ -94,15 +95,26 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||||
photoButton.setOnIconClickedListener { openGallery() }
|
photoButton.setOnIconClickedListener { openGallery() }
|
||||||
textButton.setOnIconClickedListener { openTextEditor(null) }
|
textButton.setOnIconClickedListener { openTextEditor(null) }
|
||||||
saveButton.setOnClickListener { v ->
|
saveButton.setOnClickListener { v ->
|
||||||
viewModel.save {
|
viewModel.save(
|
||||||
setFragmentResult(
|
{
|
||||||
REQUEST_KEY_SELECT_AVATAR,
|
setFragmentResult(
|
||||||
Bundle().apply {
|
REQUEST_KEY_SELECT_AVATAR,
|
||||||
putParcelable(SELECT_AVATAR_MEDIA, it)
|
Bundle().apply {
|
||||||
}
|
putParcelable(SELECT_AVATAR_MEDIA, it)
|
||||||
)
|
}
|
||||||
Navigation.findNavController(v).popBackStack()
|
)
|
||||||
}
|
Navigation.findNavController(v).popBackStack()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setFragmentResult(
|
||||||
|
REQUEST_KEY_SELECT_AVATAR,
|
||||||
|
Bundle().apply {
|
||||||
|
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Navigation.findNavController(v).popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
clearButton.setOnClickListener { viewModel.clear() }
|
clearButton.setOnClickListener { viewModel.clear() }
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import com.airbnb.lottie.SimpleColorFilter
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.avatar.Avatar
|
import org.thoughtcrime.securesms.avatar.Avatar
|
||||||
|
@ -58,17 +57,14 @@ object AvatarPickerItem {
|
||||||
init {
|
init {
|
||||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||||
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||||
updateTextSize()
|
updateAndApplyText(textView.text.toString())
|
||||||
}
|
}
|
||||||
textView.addTextChangedListener(
|
|
||||||
afterTextChanged = {
|
|
||||||
updateTextSize()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTextSize() {
|
private fun updateAndApplyText(text: String) {
|
||||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, textView.text.toString(), textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f))
|
val textSize = Avatars.getTextSizeForLength(context, text, textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)
|
||||||
|
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
|
textView.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
|
@ -112,9 +108,7 @@ object AvatarPickerItem {
|
||||||
is Avatar.Text -> {
|
is Avatar.Text -> {
|
||||||
textView.visible = true
|
textView.visible = true
|
||||||
|
|
||||||
if (textView.text.toString() != model.avatar.text) {
|
updateAndApplyText(model.avatar.text)
|
||||||
textView.text = model.avatar.text
|
|
||||||
}
|
|
||||||
|
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.avatar.Avatar
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||||
import org.thoughtcrime.securesms.avatar.Avatars
|
import org.thoughtcrime.securesms.avatar.Avatars
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
|
@ -61,10 +62,10 @@ class AvatarPickerRepository(context: Context) {
|
||||||
)
|
)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "Failed to read group avatar!")
|
Log.w(TAG, "Failed to read group avatar!")
|
||||||
getDefaultAvatarForGroup()
|
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getDefaultAvatarForGroup()
|
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,12 +156,25 @@ class AvatarPickerRepository(context: Context) {
|
||||||
return if (initials.isNullOrBlank()) {
|
return if (initials.isNullOrBlank()) {
|
||||||
Avatar.getDefaultForSelf()
|
Avatar.getDefaultForSelf()
|
||||||
} else {
|
} else {
|
||||||
Avatar.Text(initials, Avatars.colors.random(), Avatar.DatabaseId.NotSet)
|
Avatar.Text(initials, requireNotNull(Avatars.colorMap[Recipient.self().avatarColor.serialize()]), Avatar.DatabaseId.DoNotPersist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultAvatarForGroup(): Avatar {
|
fun getDefaultAvatarForGroup(groupId: GroupId): Avatar {
|
||||||
return Avatar.getDefaultForGroup()
|
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
|
||||||
|
|
||||||
|
return getDefaultAvatarForGroup(recipient.avatarColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultAvatarForGroup(color: AvatarColor?): Avatar {
|
||||||
|
val colorPair = Avatars.colorMap[color?.serialize()]
|
||||||
|
val defaultColor = Avatar.getDefaultForGroup()
|
||||||
|
|
||||||
|
return if (colorPair != null) {
|
||||||
|
defaultColor.copy(color = colorPair)
|
||||||
|
} else {
|
||||||
|
defaultColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||||
|
|
|
@ -6,5 +6,6 @@ data class AvatarPickerState(
|
||||||
val currentAvatar: Avatar? = null,
|
val currentAvatar: Avatar? = null,
|
||||||
val selectableAvatars: List<Avatar> = listOf(),
|
val selectableAvatars: List<Avatar> = listOf(),
|
||||||
val canSave: Boolean = false,
|
val canSave: Boolean = false,
|
||||||
val canClear: Boolean = false
|
val canClear: Boolean = false,
|
||||||
|
val isCleared: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,6 +27,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
|
|
||||||
fun delete(avatar: Avatar) {
|
fun delete(avatar: Avatar) {
|
||||||
repository.delete(avatar) {
|
repository.delete(avatar) {
|
||||||
|
refreshAvatar()
|
||||||
refreshSelectableAvatars()
|
refreshSelectableAvatars()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,22 +35,26 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
fun clear() {
|
fun clear() {
|
||||||
store.update {
|
store.update {
|
||||||
val avatar = getDefaultAvatarFromRepository()
|
val avatar = getDefaultAvatarFromRepository()
|
||||||
it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = false)
|
it.copy(currentAvatar = avatar, canSave = true, canClear = false, isCleared = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save(onSaved: (Media) -> Unit) {
|
fun save(onSaved: (Media) -> Unit, onCleared: () -> Unit) {
|
||||||
val avatar = store.state.currentAvatar ?: throw AssertionError()
|
if (store.state.isCleared) {
|
||||||
persistAndCreateMedia(avatar, onSaved)
|
onCleared()
|
||||||
|
} else {
|
||||||
|
val avatar = store.state.currentAvatar ?: throw AssertionError()
|
||||||
|
persistAndCreateMedia(avatar, onSaved)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAvatarSelectedFromGrid(avatar: Avatar) {
|
fun onAvatarSelectedFromGrid(avatar: Avatar) {
|
||||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) }
|
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAvatarEditCompleted(avatar: Avatar) {
|
fun onAvatarEditCompleted(avatar: Avatar) {
|
||||||
persistAvatar(avatar) { saved ->
|
persistAvatar(avatar) { saved ->
|
||||||
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true) }
|
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true, isCleared = false) }
|
||||||
refreshSelectableAvatars()
|
refreshSelectableAvatars()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +62,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
fun onAvatarPhotoSelectionCompleted(media: Media) {
|
fun onAvatarPhotoSelectionCompleted(media: Media) {
|
||||||
repository.writeMediaToMultiSessionStorage(media) { multiSessionUri ->
|
repository.writeMediaToMultiSessionStorage(media) { multiSessionUri ->
|
||||||
persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar ->
|
persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar ->
|
||||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) }
|
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||||
refreshSelectableAvatars()
|
refreshSelectableAvatars()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +71,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
protected fun refreshAvatar() {
|
protected fun refreshAvatar() {
|
||||||
disposables.add(
|
disposables.add(
|
||||||
getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar ->
|
getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar ->
|
||||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = !isSaveable(avatar)) }
|
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = avatar is Avatar.Photo && !isSaveable(avatar), isCleared = false) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,7 +89,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSaveable(avatar: Avatar) = !(avatar is Avatar.Photo && avatar.databaseId == Avatar.DatabaseId.DoNotPersist)
|
private fun isSaveable(avatar: Avatar) = avatar.databaseId != Avatar.DatabaseId.DoNotPersist
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
disposables.dispose()
|
disposables.dispose()
|
||||||
|
@ -132,7 +137,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup()
|
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(groupId)
|
||||||
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForGroup(groupId)
|
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForGroup(groupId)
|
||||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||||
|
|
||||||
|
@ -161,11 +166,11 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
||||||
return if (initialAvatar != null) {
|
return if (initialAvatar != null) {
|
||||||
Single.just(initialAvatar)
|
Single.just(initialAvatar)
|
||||||
} else {
|
} else {
|
||||||
Single.just(getDefaultAvatarFromRepository())
|
Single.fromCallable { getDefaultAvatarFromRepository() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup()
|
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(null)
|
||||||
override fun getPersistedAvatars(): Single<List<Avatar>> = Single.just(listOf())
|
override fun getPersistedAvatars(): Single<List<Avatar>> = Single.just(listOf())
|
||||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||||
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar)
|
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar)
|
||||||
|
|
|
@ -41,6 +41,8 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||||
private val withRecyclerSet = ConstraintSet()
|
private val withRecyclerSet = ConstraintSet()
|
||||||
private val withoutRecyclerSet = ConstraintSet()
|
private val withoutRecyclerSet = ConstraintSet()
|
||||||
|
|
||||||
|
private var hasBoundFromViewModel: Boolean = false
|
||||||
|
|
||||||
private fun createFactory(): TextAvatarCreationViewModel.Factory {
|
private fun createFactory(): TextAvatarCreationViewModel.Factory {
|
||||||
val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments())
|
val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments())
|
||||||
val textBundle = args.textAvatar
|
val textBundle = args.textAvatar
|
||||||
|
@ -83,17 +85,25 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||||
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
|
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
|
||||||
|
|
||||||
val hadText = textInput.length() > 0
|
val hadText = textInput.length() > 0
|
||||||
|
val selectionStart = textInput.selectionStart
|
||||||
|
val selectionEnd = textInput.selectionEnd
|
||||||
|
|
||||||
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
|
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
|
||||||
if (!hadText) {
|
textInput.post {
|
||||||
textInput.setSelection(textInput.length())
|
if (!hadText) {
|
||||||
|
textInput.setSelection(textInput.length())
|
||||||
|
} else {
|
||||||
|
textInput.setSelection(selectionStart, selectionEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
|
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
|
||||||
|
hasBoundFromViewModel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3)
|
EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3)
|
||||||
textInput.doAfterTextChanged {
|
textInput.doAfterTextChanged {
|
||||||
if (it != null) {
|
if (it != null && hasBoundFromViewModel) {
|
||||||
viewModel.setText(it.toString())
|
viewModel.setText(it.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.avatar.text
|
package org.thoughtcrime.securesms.avatar.text
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.thoughtcrime.securesms.avatar.Avatar
|
import org.thoughtcrime.securesms.avatar.Avatar
|
||||||
|
@ -11,14 +12,20 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(TextAvatarCreationState(initialText))
|
private val store = Store(TextAvatarCreationState(initialText))
|
||||||
|
|
||||||
val state: LiveData<TextAvatarCreationState> = store.stateLiveData
|
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
|
||||||
|
|
||||||
fun setColor(colorPair: Avatars.ColorPair) {
|
fun setColor(colorPair: Avatars.ColorPair) {
|
||||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setText(text: String) {
|
fun setText(text: String) {
|
||||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(text = text)) }
|
store.update {
|
||||||
|
if (it.currentAvatar.text == text) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it.copy(currentAvatar = it.currentAvatar.copy(text = text))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentAvatar(): Avatar.Text {
|
fun getCurrentAvatar(): Avatar.Text {
|
||||||
|
|
|
@ -54,7 +54,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
||||||
if (!TextUtils.isEmpty(character)) {
|
if (!TextUtils.isEmpty(character)) {
|
||||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color);
|
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color);
|
||||||
Avatar.Text avatar = new Avatar.Text(character, new Avatars.ColorPair(color, foregroundColor), Avatar.DatabaseId.DoNotPersist.INSTANCE);
|
Avatar.Text avatar = new Avatar.Text(character, new Avatars.ColorPair(color, foregroundColor), Avatar.DatabaseId.DoNotPersist.INSTANCE);
|
||||||
Drawable foreground = AvatarRenderer.createTextDrawable(context, avatar, inverted, targetSize, false);
|
Drawable foreground = AvatarRenderer.createTextDrawable(context, avatar, inverted, targetSize);
|
||||||
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
|
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
|
||||||
|
|
||||||
background.setColorFilter(new SimpleColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt()));
|
background.setColorFilter(new SimpleColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt()));
|
||||||
|
|
|
@ -170,6 +170,12 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMediaResult(Bundle data) {
|
private void handleMediaResult(Bundle data) {
|
||||||
|
if (data.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||||
|
viewModel.setAvatarMedia(null);
|
||||||
|
viewModel.setAvatar(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final Media result = data.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
final Media result = data.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||||
final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
|
final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
|
@ -104,8 +106,14 @@ public class EditProfileFragment extends LoggingFragment {
|
||||||
initializeProfileName();
|
initializeProfileName();
|
||||||
|
|
||||||
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
||||||
Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
if (bundle.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||||
handleMediaFromResult(media);
|
viewModel.setAvatarMedia(null);
|
||||||
|
viewModel.setAvatar(null);
|
||||||
|
avatar.setImageDrawable(null);
|
||||||
|
} else {
|
||||||
|
Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||||
|
handleMediaFromResult(media);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package org.thoughtcrime.securesms.profiles.manage;
|
package org.thoughtcrime.securesms.profiles.manage;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -18,28 +19,28 @@ import androidx.core.content.res.ResourcesCompat;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
|
import com.airbnb.lottie.SimpleColorFilter;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
|
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
|
||||||
|
import org.thoughtcrime.securesms.util.NameUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
|
||||||
|
|
||||||
public class ManageProfileFragment extends LoggingFragment {
|
public class ManageProfileFragment extends LoggingFragment {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(ManageProfileFragment.class);
|
private static final String TAG = Log.tag(ManageProfileFragment.class);
|
||||||
|
|
||||||
private Toolbar toolbar;
|
private Toolbar toolbar;
|
||||||
private ImageView avatarView;
|
private ImageView avatarView;
|
||||||
private View avatarPlaceholderView;
|
private ImageView avatarPlaceholderView;
|
||||||
private TextView profileNameView;
|
private TextView profileNameView;
|
||||||
private View profileNameContainer;
|
private View profileNameContainer;
|
||||||
private TextView usernameView;
|
private TextView usernameView;
|
||||||
|
@ -48,6 +49,8 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||||
private View aboutContainer;
|
private View aboutContainer;
|
||||||
private ImageView aboutEmojiView;
|
private ImageView aboutEmojiView;
|
||||||
private AlertDialog avatarProgress;
|
private AlertDialog avatarProgress;
|
||||||
|
private TextView avatarInitials;
|
||||||
|
private ImageView avatarBackground;
|
||||||
|
|
||||||
private ManageProfileViewModel viewModel;
|
private ManageProfileViewModel viewModel;
|
||||||
|
|
||||||
|
@ -68,6 +71,8 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||||
this.aboutView = view.findViewById(R.id.manage_profile_about);
|
this.aboutView = view.findViewById(R.id.manage_profile_about);
|
||||||
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
|
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
|
||||||
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
|
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);
|
||||||
|
|
||||||
initializeViewModel();
|
initializeViewModel();
|
||||||
|
|
||||||
|
@ -87,8 +92,18 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
||||||
Media result = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
if (bundle.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||||
viewModel.onAvatarSelected(requireContext(), result);
|
viewModel.onAvatarSelected(requireContext(), null);
|
||||||
|
} else {
|
||||||
|
Media result = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||||
|
viewModel.onAvatarSelected(requireContext(), result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarInitials.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||||
|
if (avatarInitials.length() > 0) {
|
||||||
|
updateInitials(avatarInitials.getText().toString());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,9 +126,26 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||||
private void presentAvatar(@NonNull AvatarState avatarState) {
|
private void presentAvatar(@NonNull AvatarState avatarState) {
|
||||||
if (avatarState.getAvatar() == null) {
|
if (avatarState.getAvatar() == null) {
|
||||||
avatarView.setImageDrawable(null);
|
avatarView.setImageDrawable(null);
|
||||||
avatarPlaceholderView.setVisibility(View.VISIBLE);
|
|
||||||
|
CharSequence initials = NameUtil.getAbbreviation(avatarState.getSelf().getDisplayName(requireContext()));
|
||||||
|
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(avatarState.getSelf().getAvatarColor());
|
||||||
|
|
||||||
|
avatarBackground.setColorFilter(new SimpleColorFilter(avatarState.getSelf().getAvatarColor().colorInt()));
|
||||||
|
avatarPlaceholderView.setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||||
|
avatarInitials.setTextColor(foregroundColor.getColorInt());
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(initials)) {
|
||||||
|
avatarPlaceholderView.setVisibility(View.VISIBLE);
|
||||||
|
avatarInitials.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
updateInitials(initials.toString());
|
||||||
|
avatarPlaceholderView.setVisibility(View.GONE);
|
||||||
|
avatarInitials.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
avatarPlaceholderView.setVisibility(View.GONE);
|
avatarPlaceholderView.setVisibility(View.GONE);
|
||||||
|
avatarInitials.setVisibility(View.GONE);
|
||||||
|
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.load(avatarState.getAvatar())
|
.load(avatarState.getAvatar())
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
|
@ -127,6 +159,11 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateInitials(String initials) {
|
||||||
|
avatarInitials.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(requireContext(), initials, avatarInitials.getMeasuredWidth() * 0.8f, avatarInitials.getMeasuredWidth() * 0.45f));
|
||||||
|
avatarInitials.setText(initials);
|
||||||
|
}
|
||||||
|
|
||||||
private void presentProfileName(@Nullable ProfileName profileName) {
|
private void presentProfileName(@Nullable ProfileName profileName) {
|
||||||
if (profileName == null || profileName.isEmpty()) {
|
if (profileName == null || profileName.isEmpty()) {
|
||||||
profileNameView.setText(R.string.ManageProfileFragment_profile_name);
|
profileNameView.setText(R.string.ManageProfileFragment_profile_name);
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -32,26 +33,28 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(ManageProfileViewModel.class);
|
private static final String TAG = Log.tag(ManageProfileViewModel.class);
|
||||||
|
|
||||||
private final MutableLiveData<AvatarState> avatar;
|
private final MutableLiveData<InternalAvatarState> internalAvatarState;
|
||||||
private final MutableLiveData<ProfileName> profileName;
|
private final MutableLiveData<ProfileName> profileName;
|
||||||
private final MutableLiveData<String> username;
|
private final MutableLiveData<String> username;
|
||||||
private final MutableLiveData<String> about;
|
private final MutableLiveData<String> about;
|
||||||
private final MutableLiveData<String> aboutEmoji;
|
private final MutableLiveData<String> aboutEmoji;
|
||||||
private final SingleLiveEvent<Event> events;
|
private final LiveData<AvatarState> avatarState;
|
||||||
private final RecipientForeverObserver observer;
|
private final SingleLiveEvent<Event> events;
|
||||||
private final ManageProfileRepository repository;
|
private final RecipientForeverObserver observer;
|
||||||
|
private final ManageProfileRepository repository;
|
||||||
|
|
||||||
private byte[] previousAvatar;
|
private byte[] previousAvatar;
|
||||||
|
|
||||||
public ManageProfileViewModel() {
|
public ManageProfileViewModel() {
|
||||||
this.avatar = new MutableLiveData<>();
|
this.internalAvatarState = new MutableLiveData<>();
|
||||||
this.profileName = new MutableLiveData<>();
|
this.profileName = new MutableLiveData<>();
|
||||||
this.username = new MutableLiveData<>();
|
this.username = new MutableLiveData<>();
|
||||||
this.about = new MutableLiveData<>();
|
this.about = new MutableLiveData<>();
|
||||||
this.aboutEmoji = new MutableLiveData<>();
|
this.aboutEmoji = new MutableLiveData<>();
|
||||||
this.events = new SingleLiveEvent<>();
|
this.events = new SingleLiveEvent<>();
|
||||||
this.repository = new ManageProfileRepository();
|
this.repository = new ManageProfileRepository();
|
||||||
this.observer = this::onRecipientChanged;
|
this.observer = this::onRecipientChanged;
|
||||||
|
this.avatarState = LiveDataUtil.combineLatest(Recipient.self().live().getLiveData(), internalAvatarState, (self, state) -> new AvatarState(state, self));
|
||||||
|
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
onRecipientChanged(Recipient.self().fresh());
|
onRecipientChanged(Recipient.self().fresh());
|
||||||
|
@ -59,13 +62,13 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
|
StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
|
||||||
if (details != null) {
|
if (details != null) {
|
||||||
try {
|
try {
|
||||||
avatar.postValue(AvatarState.loaded(StreamUtil.readFully(details.getStream())));
|
internalAvatarState.postValue(InternalAvatarState.loaded(StreamUtil.readFully(details.getStream())));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, "Failed to read avatar!");
|
Log.w(TAG, "Failed to read avatar!");
|
||||||
avatar.postValue(AvatarState.none());
|
internalAvatarState.postValue(InternalAvatarState.none());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
avatar.postValue(AvatarState.none());
|
internalAvatarState.postValue(InternalAvatarState.none());
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
|
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
|
||||||
|
@ -75,7 +78,7 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull LiveData<AvatarState> getAvatar() {
|
public @NonNull LiveData<AvatarState> getAvatar() {
|
||||||
return avatar;
|
return avatarState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull LiveData<ProfileName> getProfileName() {
|
public @NonNull LiveData<ProfileName> getProfileName() {
|
||||||
|
@ -103,18 +106,18 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
|
public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
|
||||||
previousAvatar = avatar.getValue() != null ? avatar.getValue().getAvatar() : null;
|
previousAvatar = internalAvatarState.getValue() != null ? internalAvatarState.getValue().getAvatar() : null;
|
||||||
|
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
avatar.postValue(AvatarState.loading(null));
|
internalAvatarState.postValue(InternalAvatarState.loading(null));
|
||||||
repository.clearAvatar(context, result -> {
|
repository.clearAvatar(context, result -> {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case SUCCESS:
|
case SUCCESS:
|
||||||
avatar.postValue(AvatarState.loaded(null));
|
internalAvatarState.postValue(InternalAvatarState.loaded(null));
|
||||||
previousAvatar = null;
|
previousAvatar = null;
|
||||||
break;
|
break;
|
||||||
case FAILURE_NETWORK:
|
case FAILURE_NETWORK:
|
||||||
avatar.postValue(AvatarState.loaded(previousAvatar));
|
internalAvatarState.postValue(InternalAvatarState.loaded(previousAvatar));
|
||||||
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -125,16 +128,16 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
|
InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
|
||||||
byte[] data = StreamUtil.readFully(stream);
|
byte[] data = StreamUtil.readFully(stream);
|
||||||
|
|
||||||
avatar.postValue(AvatarState.loading(data));
|
internalAvatarState.postValue(InternalAvatarState.loading(data));
|
||||||
|
|
||||||
repository.setAvatar(context, data, media.getMimeType(), result -> {
|
repository.setAvatar(context, data, media.getMimeType(), result -> {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case SUCCESS:
|
case SUCCESS:
|
||||||
avatar.postValue(AvatarState.loaded(data));
|
internalAvatarState.postValue(InternalAvatarState.loaded(data));
|
||||||
previousAvatar = data;
|
previousAvatar = data;
|
||||||
break;
|
break;
|
||||||
case FAILURE_NETWORK:
|
case FAILURE_NETWORK:
|
||||||
avatar.postValue(AvatarState.loaded(previousAvatar));
|
internalAvatarState.postValue(InternalAvatarState.loaded(previousAvatar));
|
||||||
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -148,7 +151,7 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canRemoveAvatar() {
|
public boolean canRemoveAvatar() {
|
||||||
return avatar.getValue() != null;
|
return internalAvatarState.getValue() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRecipientChanged(@NonNull Recipient recipient) {
|
private void onRecipientChanged(@NonNull Recipient recipient) {
|
||||||
|
@ -163,25 +166,49 @@ class ManageProfileViewModel extends ViewModel {
|
||||||
Recipient.self().live().removeForeverObserver(observer);
|
Recipient.self().live().removeForeverObserver(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AvatarState {
|
public final static class AvatarState {
|
||||||
|
private final InternalAvatarState internalAvatarState;
|
||||||
|
private final Recipient self;
|
||||||
|
|
||||||
|
public AvatarState(@NonNull InternalAvatarState internalAvatarState,
|
||||||
|
@NonNull Recipient self)
|
||||||
|
{
|
||||||
|
this.internalAvatarState = internalAvatarState;
|
||||||
|
this.self = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable byte[] getAvatar() {
|
||||||
|
return internalAvatarState.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull LoadingState getLoadingState() {
|
||||||
|
return internalAvatarState.loadingState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Recipient getSelf() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static class InternalAvatarState {
|
||||||
private final byte[] avatar;
|
private final byte[] avatar;
|
||||||
private final LoadingState loadingState;
|
private final LoadingState loadingState;
|
||||||
|
|
||||||
public AvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
|
public InternalAvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
|
||||||
this.avatar = avatar;
|
this.avatar = avatar;
|
||||||
this.loadingState = loadingState;
|
this.loadingState = loadingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull AvatarState none() {
|
private static @NonNull InternalAvatarState none() {
|
||||||
return new AvatarState(null, LoadingState.LOADED);
|
return new InternalAvatarState(null, LoadingState.LOADED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull AvatarState loaded(@Nullable byte[] avatar) {
|
private static @NonNull InternalAvatarState loaded(@Nullable byte[] avatar) {
|
||||||
return new AvatarState(avatar, LoadingState.LOADED);
|
return new InternalAvatarState(avatar, LoadingState.LOADED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull AvatarState loading(@Nullable byte[] avatar) {
|
private static @NonNull InternalAvatarState loading(@Nullable byte[] avatar) {
|
||||||
return new AvatarState(avatar, LoadingState.LOADING);
|
return new InternalAvatarState(avatar, LoadingState.LOADING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable byte[] getAvatar() {
|
public @Nullable byte[] getAvatar() {
|
||||||
|
|
|
@ -111,6 +111,7 @@
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -46,6 +46,21 @@
|
||||||
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background"
|
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background"
|
||||||
app:srcCompat="@drawable/ic_profile_outline_40" />
|
app:srcCompat="@drawable/ic_profile_outline_40" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/manage_profile_avatar_initials"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/manage_profile_avatar_background"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/manage_profile_avatar_background"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/manage_profile_avatar_background"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/manage_profile_avatar_background"
|
||||||
|
tools:ignore="SpUsage"
|
||||||
|
tools:text="AF"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/manage_profile_avatar"
|
android:id="@+id/manage_profile_avatar"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -100,6 +100,7 @@
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -99,6 +99,7 @@
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
Ładowanie…
Reference in New Issue