diff --git a/res/drawable/profile_picture_view_medium_background.xml b/res/drawable/profile_picture_view_medium_background.xml index 287d6298e2..143223ac1b 100644 --- a/res/drawable/profile_picture_view_medium_background.xml +++ b/res/drawable/profile_picture_view_medium_background.xml @@ -6,6 +6,4 @@ - - \ No newline at end of file diff --git a/res/drawable/profile_picture_view_rss_medium_background.xml b/res/drawable/profile_picture_view_rss_medium_background.xml index b060c41b4b..4d9a6a86d9 100644 --- a/res/drawable/profile_picture_view_rss_medium_background.xml +++ b/res/drawable/profile_picture_view_rss_medium_background.xml @@ -6,6 +6,4 @@ - - \ No newline at end of file diff --git a/res/drawable/profile_picture_view_small_background.xml b/res/drawable/profile_picture_view_small_background.xml index 2f44fefe9f..523316cf4e 100644 --- a/res/drawable/profile_picture_view_small_background.xml +++ b/res/drawable/profile_picture_view_small_background.xml @@ -6,6 +6,4 @@ - - \ No newline at end of file diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml index 13eb8bda5e..b9dee38117 100644 --- a/res/layout/activity_home.xml +++ b/res/layout/activity_home.xml @@ -34,7 +34,8 @@ android:layout_height="@dimen/small_profile_picture_size" android:layout_alignParentLeft="true" android:layout_centerVertical="true" - android:layout_marginLeft="9dp" /> + android:layout_marginLeft="9dp" + android:foreground="@drawable/circle_touch_highlight_background"/> + android:layout_marginTop="@dimen/medium_spacing" + android:foreground="@drawable/circle_touch_highlight_background" /> - + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto"> - - - - @@ -60,11 +51,6 @@ android:layout_height="match_parent" android:background="@drawable/profile_picture_view_medium_background" /> - - - - \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index c9356d308b..390c3be09e 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -107,4 +107,12 @@ #121212 #171717 + + #18da80 + #ee8917 + #239edf + #c33fe9 + #83b433 + + diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index ca3d10db17..ed4f24d110 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -804,6 +804,7 @@ public class ConversationItem extends LinearLayout bodyBubble.setLayoutParams(layoutParams); if (profilePictureView == null) return; profilePictureView.setPublicKey(recipient.getAddress().toString()); + profilePictureView.setDisplayName(recipient.getName()); profilePictureView.setAdditionalPublicKey(null); profilePictureView.setRSSFeed(false); profilePictureView.setGlide(glideRequests); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 43e0647d2d..af6f3b9979 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.loki.activities import android.app.AlertDialog -import androidx.lifecycle.Observer import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -11,10 +10,6 @@ import android.net.Uri import android.os.AsyncTask import android.os.Bundle import android.os.Handler -import androidx.loader.app.LoaderManager -import androidx.loader.content.Loader -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.recyclerview.widget.LinearLayoutManager import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan @@ -22,6 +17,11 @@ import android.util.DisplayMetrics import android.view.View import android.widget.RelativeLayout import android.widget.Toast +import androidx.lifecycle.Observer +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.activity_home.* import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext @@ -42,14 +42,13 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol -import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol +import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol import org.whispersystems.signalservice.loki.utilities.toHexString @@ -98,6 +97,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe // Set up toolbar buttons profileButton.glide = glide profileButton.publicKey = publicKey + profileButton.displayName = TextSecurePreferences.getProfileName(this) profileButton.update() profileButton.setOnClickListener { openSettings() } pathStatusViewContainer.setOnClickListener { showPath() } diff --git a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index 1a88113784..0d052d4221 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -74,14 +74,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { setContentView(R.layout.activity_settings) + val origUserDisplayName = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey) + glide = GlideApp.with(this) profilePictureView.glide = glide profilePictureView.publicKey = hexEncodedPublicKey + profilePictureView.displayName = displayNameToBeUploaded profilePictureView.isLarge = true profilePictureView.update() profilePictureView.setOnClickListener { showEditProfilePictureUI() } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } - btnGroupNameDisplay.text = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey) + btnGroupNameDisplay.text = origUserDisplayName publicKeyTextView.text = hexEncodedPublicKey copyButton.setOnClickListener { copyPublicKey() } shareButton.setOnClickListener { sharePublicKey() } diff --git a/src/org/thoughtcrime/securesms/loki/todo/AvatarPlaceholderGenerator.kt b/src/org/thoughtcrime/securesms/loki/todo/AvatarPlaceholderGenerator.kt new file mode 100644 index 0000000000..bca93db549 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/todo/AvatarPlaceholderGenerator.kt @@ -0,0 +1,83 @@ +package org.thoughtcrime.securesms.loki.todo + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.text.TextPaint +import android.text.TextUtils +import androidx.annotation.ColorInt +import androidx.core.graphics.ColorUtils +import network.loki.messenger.R +import java.util.* + +object AvatarPlaceholderGenerator { + + private const val EMPTY_LABEL = "0"; + + private val tmpFloatArray = FloatArray(3) + + fun generate(context: Context, pixelSize: Int, hashString: String, displayName: String?): BitmapDrawable { + //TODO That should be replaced with a proper hash extraction code. + val hash: Long + val hexRegex = Regex("^[0-9A-Fa-f]+\$") + if (hashString.length >= 12 && hashString.matches(hexRegex)) { + hash = hashString.substring(0 until 12).toLong(16) + } else { + hash = hashString.toLong() + } + + // Do not cache color array, it may be different depends on the current theme. + val colorArray = context.resources.getIntArray(R.array.user_pic_placeholder_primary) + val colorPrimary = colorArray[(hash % colorArray.size).toInt()] + val colorSecondary = changeColorHueBy(colorPrimary, 16f) + + val labelText = when { + !TextUtils.isEmpty(displayName) -> extractLabel(displayName!!) + !TextUtils.isEmpty(hashString) -> extractLabel(hashString) + else -> EMPTY_LABEL + } + + val bitmap = Bitmap.createBitmap(pixelSize, pixelSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + // Draw background/frame + val paint = Paint() + paint.isAntiAlias = true + paint.shader = LinearGradient(0f, 0f, 0f, pixelSize.toFloat(), + colorPrimary, + colorSecondary, + Shader.TileMode.REPEAT) + canvas.drawCircle(pixelSize.toFloat() / 2, pixelSize.toFloat() / 2, pixelSize.toFloat() / 2, paint) + + // Draw text + val textPaint = TextPaint() + textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) + textPaint.isAntiAlias = true + textPaint.textSize = pixelSize * 0.5f + textPaint.color = Color.WHITE + val areaRect = Rect(0, 0, pixelSize, pixelSize) + val textBounds = RectF(areaRect) + textBounds.right = textPaint.measureText(labelText) + textBounds.bottom = textPaint.descent() - textPaint.ascent() + textBounds.left += (areaRect.width() - textBounds.right) * 0.5f + textBounds.top += (areaRect.height() - textBounds.bottom) * 0.5f + canvas.drawText(labelText, textBounds.left, textBounds.top - textPaint.ascent(), textPaint) + + return BitmapDrawable(context.resources, bitmap) + } + + @ColorInt + private fun changeColorHueBy(@ColorInt color: Int, hueDelta: Float): Int { + val hslColor = tmpFloatArray + ColorUtils.colorToHSL(color, hslColor) + hslColor[0] = (hslColor[0] + hueDelta) % 360f + return ColorUtils.HSLToColor(hslColor) + } + + private fun extractLabel(content: String): String { + var content = content.trim() + if (content.isEmpty()) return EMPTY_LABEL + + return content.first().toString().toUpperCase(Locale.ROOT) + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/views/ConversationView.kt b/src/org/thoughtcrime/securesms/loki/views/ConversationView.kt index 382ac63a84..181598b730 100644 --- a/src/org/thoughtcrime/securesms/loki/views/ConversationView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/ConversationView.kt @@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.loki.views import android.content.Context import android.graphics.Typeface +import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import kotlinx.android.synthetic.main.view_conversation.view.* import network.loki.messenger.R +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions @@ -58,22 +60,29 @@ class ConversationView : LinearLayout { if (thread.recipient.isGroupRecipient) { if ("Session Public Chat" == thread.recipient.name) { profilePictureView.publicKey = "" + profilePictureView.displayName = "" profilePictureView.additionalPublicKey = null profilePictureView.isRSSFeed = true } else { - val users = MentionsManager.shared.userPublicKeyCache[thread.threadId]?.toMutableList() ?: mutableListOf() - users.remove(TextSecurePreferences.getLocalNumber(context)) + val userKeys = MentionsManager.shared.userPublicKeyCache[thread.threadId]?.toMutableList() ?: mutableListOf() + userKeys.remove(TextSecurePreferences.getLocalNumber(context)) val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) if (masterPublicKey != null) { - users.remove(masterPublicKey) + userKeys.remove(masterPublicKey) } - val randomUsers = users.sorted() // Sort to provide a level of stability - profilePictureView.publicKey = randomUsers.getOrNull(0) ?: "" - profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: "" + + val sortedUserKeys = userKeys.sorted() // Sort to provide a level of stability + val userKey0 = sortedUserKeys.getOrNull(0) ?: "" + val userKey1 = sortedUserKeys.getOrNull(1) ?: "" + profilePictureView.publicKey = userKey0 + profilePictureView.displayName = getUserDisplayName(userKey0) + profilePictureView.additionalPublicKey = userKey1 + profilePictureView.additionalDisplayName = getUserDisplayName(userKey1) profilePictureView.isRSSFeed = thread.recipient.name == "Loki News" || thread.recipient.name == "Session Updates" } } else { profilePictureView.publicKey = thread.recipient.address.toString() + profilePictureView.displayName = thread.recipient.name profilePictureView.additionalPublicKey = null profilePictureView.isRSSFeed = false } @@ -103,5 +112,10 @@ class ConversationView : LinearLayout { else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) } } + + private fun getUserDisplayName(publicKey: String?): String? { + if (TextUtils.isEmpty(publicKey)) return null + return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!) + } // endregion } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt b/src/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt index 1e6a0d057c..a9b62a5fd9 100644 --- a/src/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt @@ -32,6 +32,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: private fun update() { btnGroupNameDisplay.text = mentionCandidate.displayName profilePictureView.publicKey = mentionCandidate.publicKey + profilePictureView.displayName = mentionCandidate.displayName profilePictureView.additionalPublicKey = null profilePictureView.isRSSFeed = false profilePictureView.glide = glide!! diff --git a/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index 362f5ff35d..d70f20fdc2 100644 --- a/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -1,18 +1,18 @@ package org.thoughtcrime.securesms.loki.views import android.content.Context -import androidx.annotation.DimenRes import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.ImageView import android.widget.RelativeLayout +import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy import kotlinx.android.synthetic.main.view_profile_picture.view.* import network.loki.messenger.R import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto import org.thoughtcrime.securesms.database.Address -import org.thoughtcrime.securesms.loki.todo.JazzIdenticonDrawable +import org.thoughtcrime.securesms.loki.todo.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -22,7 +22,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences class ProfilePictureView : RelativeLayout { lateinit var glide: GlideRequests var publicKey: String? = null + var displayName: String? = null var additionalPublicKey: String? = null + var additionalDisplayName: String? = null var isRSSFeed = false var isLarge = false @@ -58,28 +60,51 @@ class ProfilePictureView : RelativeLayout { singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE - fun setProfilePictureIfNeeded(imageView: ImageView, hexEncodedPublicKey: String, @DimenRes sizeID: Int) { - glide.clear(imageView) - if (hexEncodedPublicKey.isNotEmpty()) { - val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false); - val signalProfilePicture = recipient.contactPhoto - if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { - glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) - } else { - val size = resources.getDimensionPixelSize(sizeID) - val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - val hepk = if (recipient.isLocalNumber && masterHexEncodedPublicKey != null) masterHexEncodedPublicKey else hexEncodedPublicKey - val jazzIcon = JazzIdenticonDrawable(size, size, hepk) - glide.load(jazzIcon).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) - } + + setProfilePictureIfNeeded( + doubleModeImageView1, + publicKey, + displayName, + R.dimen.small_profile_picture_size) + setProfilePictureIfNeeded( + doubleModeImageView2, + additionalPublicKey ?: "", + additionalDisplayName, + R.dimen.small_profile_picture_size) + setProfilePictureIfNeeded( + singleModeImageView, + publicKey, + displayName, + R.dimen.medium_profile_picture_size) + setProfilePictureIfNeeded( + largeSingleModeImageView, + publicKey, + displayName, + R.dimen.large_profile_picture_size) + } + + private fun setProfilePictureIfNeeded(imageView: ImageView, hexEncodedPublicKey: String, displayName: String?, @DimenRes sizeResId: Int) { + glide.clear(imageView) + if (hexEncodedPublicKey.isNotEmpty()) { + val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false); + val signalProfilePicture = recipient.contactPhoto + if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { + glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) } else { - imageView.setImageDrawable(null) + val sizePx = resources.getDimensionPixelSize(sizeResId) + val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + val hepk = if (recipient.isLocalNumber && masterHexEncodedPublicKey != null) masterHexEncodedPublicKey else hexEncodedPublicKey +// val jazzIcon = JazzIdenticonDrawable(size, size, hepk) + glide.load(AvatarPlaceholderGenerator.generate( + context, + sizePx, + hepk, + displayName + )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) } + } else { + imageView.setImageDrawable(null) } - setProfilePictureIfNeeded(doubleModeImageView1, publicKey, R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey ?: "", R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded(singleModeImageView, publicKey, R.dimen.medium_profile_picture_size) - setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, R.dimen.large_profile_picture_size) } // endregion } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/views/UserView.kt b/src/org/thoughtcrime/securesms/loki/views/UserView.kt index 9619a90389..2347f07cc9 100644 --- a/src/org/thoughtcrime/securesms/loki/views/UserView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/UserView.kt @@ -51,6 +51,7 @@ class UserView : LinearLayout { if (user.isGroupRecipient) { if ("Session Public Chat" == user.name || user.address.isRSSFeed) { profilePictureView.publicKey = "" + profilePictureView.displayName = null profilePictureView.additionalPublicKey = null profilePictureView.isRSSFeed = true } else { @@ -58,11 +59,13 @@ class UserView : LinearLayout { val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toList() ?: listOf() val randomUsers = users.sorted() // Sort to provide a level of stability profilePictureView.publicKey = randomUsers.getOrNull(0) ?: "" + profilePictureView.displayName = null profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: "" profilePictureView.isRSSFeed = false } } else { profilePictureView.publicKey = address + profilePictureView.displayName = null profilePictureView.additionalPublicKey = null profilePictureView.isRSSFeed = false }