diff --git a/build.gradle b/build.gradle
index d003f75977..ea4d2068e4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -197,6 +197,7 @@ dependencies {
}
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
+ implementation "com.github.ybq:Android-SpinKit:1.4.0"
}
def canonicalVersionCode = 23
diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml
index 7bb054d771..498738cf9d 100644
--- a/res/layout/activity_home.xml
+++ b/res/layout/activity_home.xml
@@ -24,7 +24,7 @@
android:id="@+id/profileButton"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
- android:layout_marginLeft="8dp"
+ android:layout_marginLeft="10dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true" />
diff --git a/res/layout/activity_settings.xml b/res/layout/activity_settings.xml
index fb333e9969..d2b6fd429f 100644
--- a/res/layout/activity_settings.xml
+++ b/res/layout/activity_settings.xml
@@ -1,258 +1,283 @@
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/default_session_background">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:scrollbars="none">
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java
index 62f4cd26a6..22c3ea398a 100644
--- a/src/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java
+++ b/src/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java
@@ -8,7 +8,6 @@ import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
-import org.thoughtcrime.securesms.util.Conversions;
import java.io.IOException;
import java.io.InputStream;
@@ -17,7 +16,7 @@ import java.security.MessageDigest;
public class ProfileContactPhoto implements ContactPhoto {
private final @NonNull Address address;
- private final @NonNull String avatarObject;
+ public final @NonNull String avatarObject;
public ProfileContactPhoto(@NonNull Address address, @NonNull String avatarObject) {
this.address = address;
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
index 120eddf126..ef7b7d2591 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
@@ -91,14 +91,14 @@ class RegisterActivity : BaseActionBarActivity() {
val hexEncodedPublicKey = keyPair!!.hexEncodedPublicKey
val characterCount = hexEncodedPublicKey.count()
var count = 0
- val limit = 40
+ val limit = 32
fun animate() {
- val numberOfIndexesToShuffle = (0 until (40 - count)).random()
+ val numberOfIndexesToShuffle = 32 - count
val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle)
var mangledHexEncodedPublicKey = hexEncodedPublicKey
for (index in indexesToShuffle) {
try {
- mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef________________".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count())
+ mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count())
} catch (exception: Exception) {
// Do nothing
}
@@ -108,7 +108,7 @@ class RegisterActivity : BaseActionBarActivity() {
publicKeyTextView.text = mangledHexEncodedPublicKey
Handler().postDelayed({
animate()
- }, 40)
+ }, 32)
} else {
publicKeyTextView.text = hexEncodedPublicKey
}
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/SettingsActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/SettingsActivity.kt
index d991e79837..fd898e7186 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/SettingsActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/SettingsActivity.kt
@@ -1,30 +1,56 @@
package org.thoughtcrime.securesms.loki.redesign.activities
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
+import android.net.Uri
+import android.os.AsyncTask
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.R
+import nl.komponents.kovenant.Promise
+import nl.komponents.kovenant.all
+import nl.komponents.kovenant.deferred
+import nl.komponents.kovenant.ui.alwaysUi
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
+import org.thoughtcrime.securesms.avatar.AvatarSelection
+import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
+import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.redesign.utilities.push
import org.thoughtcrime.securesms.loki.toPx
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.profiles.AvatarHelper
+import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
+import org.thoughtcrime.securesms.util.BitmapDecodingException
+import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
+import org.whispersystems.signalservice.api.crypto.ProfileCipher
+import org.whispersystems.signalservice.api.util.StreamDetails
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.security.SecureRandom
class SettingsActivity : PassphraseRequiredActionBarActivity() {
private lateinit var glide: GlideRequests
private var isEditingDisplayName = false
set(value) { field = value; handleIsEditingDisplayNameChanged() }
private var displayNameToBeUploaded: String? = null
+ private var profilePictureToBeUploaded: ByteArray? = null
+ private var tempFile: File? = null
private val hexEncodedPublicKey: String
get() {
@@ -50,6 +76,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey
profilePictureView.isLarge = true
profilePictureView.update()
+ profilePictureView.setOnClickListener { showEditProfilePictureUI() }
// Set up display name container
displayNameContainer.setOnClickListener { showEditDisplayNameUI() }
// Set up display name text view
@@ -61,6 +88,34 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// Set up share button
shareButton.setOnClickListener { sharePublicKey() }
}
+
+ public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when (requestCode) {
+ AvatarSelection.REQUEST_CODE_AVATAR -> {
+ if (resultCode != Activity.RESULT_OK) { return }
+ val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
+ var inputFile: Uri? = data?.data
+ if (inputFile == null && tempFile != null) {
+ inputFile = Uri.fromFile(tempFile)
+ }
+ AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
+ }
+ AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
+ if (resultCode != Activity.RESULT_OK) { return }
+ AsyncTask.execute {
+ try {
+ profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
+ Handler(Looper.getMainLooper()).post {
+ updateProfile(true)
+ }
+ } catch (e: BitmapDecodingException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+ }
// endregion
// region Updating
@@ -82,18 +137,62 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
}
- private fun updateProfile(isUpdatingDisplayName: Boolean, isUpdatingProfilePicture: Boolean) {
- val displayName = displayNameToBeUploaded ?: TextSecurePreferences.getProfileName(this)
- TextSecurePreferences.setProfileName(this, displayName)
- val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
- if (publicChatAPI != null) {
- val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
- for (server in servers) {
- publicChatAPI.setDisplayName(displayName, server)
+ private fun updateProfile(isUpdatingProfilePicture: Boolean) {
+ showLoader()
+ val promises = mutableListOf>()
+ val displayName = displayNameToBeUploaded
+ if (displayName != null) {
+ val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
+ if (publicChatAPI != null) {
+ val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
+ promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
}
+ TextSecurePreferences.setProfileName(this, displayName)
}
- displayNameTextView.text = displayName
- displayNameToBeUploaded = null
+ val profilePicture = profilePictureToBeUploaded
+ val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
+ val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
+ if (isUpdatingProfilePicture && profilePicture != null) {
+ val storageAPI = LokiStorageAPI.shared
+ val deferred = deferred()
+ AsyncTask.execute {
+ val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
+ val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream)
+ TextSecurePreferences.setProfileAvatarUrl(this, url)
+ deferred.resolve(Unit)
+ }
+ promises.add(deferred.promise)
+ }
+ all(promises).alwaysUi {
+ if (displayName != null) {
+ displayNameTextView.text = displayName
+ }
+ displayNameToBeUploaded = null
+ if (isUpdatingProfilePicture && profilePicture != null) {
+ AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), profilePicture)
+ TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
+ ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
+ ApplicationContext.getInstance(this).updatePublicChatProfileAvatarIfNeeded()
+ profilePictureView.update()
+ }
+ profilePictureToBeUploaded = null
+ hideLoader()
+ }
+ }
+
+ private fun showLoader() {
+ loader.visibility = View.VISIBLE
+ loader.animate().setDuration(150).alpha(1.0f).start()
+ }
+
+ private fun hideLoader() {
+ loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
+
+ override fun onAnimationEnd(animation: Animator?) {
+ super.onAnimationEnd(animation)
+ loader.visibility = View.GONE
+ }
+ })
}
// endregion
@@ -103,11 +202,19 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
private fun saveDisplayName() {
- val displayName = displayNameEditText.text.trim().toString()
- // TODO: Validation
+ val displayName = displayNameEditText.text.toString().trim()
+ if (displayName.isEmpty()) {
+ return Toast.makeText(this, "Please pick a display name", Toast.LENGTH_SHORT).show()
+ }
+ if (!displayName.matches(Regex("[a-zA-Z0-9_]+"))) {
+ return Toast.makeText(this, "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", Toast.LENGTH_SHORT).show()
+ }
+ if (displayName.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
+ return Toast.makeText(this, "Please pick a shorter display name", Toast.LENGTH_SHORT).show()
+ }
isEditingDisplayName = false
displayNameToBeUploaded = displayName
- updateProfile(true, false)
+ updateProfile(false)
}
private fun showQRCode() {
@@ -115,6 +222,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
push(intent)
}
+ private fun showEditProfilePictureUI() {
+ tempFile = AvatarSelection.startAvatarSelection(this, false, true)
+ }
+
private fun showEditDisplayNameUI() {
isEditingDisplayName = true
}
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/views/ProfilePictureView.kt b/src/org/thoughtcrime/securesms/loki/redesign/views/ProfilePictureView.kt
index 084d6a3caa..87fddbfb3e 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/views/ProfilePictureView.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/views/ProfilePictureView.kt
@@ -10,6 +10,7 @@ import android.widget.RelativeLayout
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.JazzIdenticonDrawable
import org.thoughtcrime.securesms.mms.GlideRequests
@@ -60,7 +61,7 @@ class ProfilePictureView : RelativeLayout {
glide.clear(imageView)
if (hexEncodedPublicKey.isNotEmpty()) {
val signalProfilePicture = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).contactPhoto
- if (signalProfilePicture != null) {
+ if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0") {
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
} else {
val size = resources.getDimensionPixelSize(sizeID)
diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java
index d3db74308e..aa70a329da 100644
--- a/src/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -475,7 +475,7 @@ public class Recipient implements RecipientModifiedListener {
}
public synchronized @Nullable ContactPhoto getContactPhoto() {
- if (isLocalNumber && profileAvatar != null) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
+ if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);