feat: add contact syncing, UI improvements for profile syncing and conversation threads in the home screen

This commit is contained in:
jubb 2021-03-01 17:16:15 +11:00
parent 11c122e376
commit 3a09d23337
10 changed files with 90 additions and 14 deletions

View File

@ -1,5 +1,4 @@
buildscript { buildscript {
ext.kotlin_version = "1.4.0"
ext.kovenant_version = "3.3.0" ext.kovenant_version = "3.3.0"
repositories { repositories {
@ -124,8 +123,8 @@ dependencies {
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
implementation "com.github.lelloman:android-identicons:v11" implementation "com.github.lelloman:android-identicons:v11"

View File

@ -249,7 +249,14 @@ public class RecipientDatabase extends Database {
recipient.resolve().setProfileAvatar(profileAvatar); recipient.resolve().setProfileAvatar(profileAvatar);
} }
public void setProfileSharing(@NonNull Recipient recipient, @SuppressWarnings("SameParameterValue") boolean enabled) { public void setProfileName(@NonNull Recipient recipient, @Nullable String profileName) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(SYSTEM_DISPLAY_NAME, profileName);
updateOrInsert(recipient.getAddress(), contentValues);
recipient.resolve().setProfileName(profileName);
}
public void setProfileSharing(@NonNull Recipient recipient, boolean enabled) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0); contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
updateOrInsert(recipient.getAddress(), contentValues); updateOrInsert(recipient.getAddress(), contentValues);

View File

@ -506,6 +506,10 @@ public class ThreadDatabase extends Database {
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
} }
public void notifyUpdatedFromConfig() {
notifyConversationListListeners();
}
public boolean update(long threadId, boolean unarchive) { public boolean update(long threadId, boolean unarchive) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId); long count = mmsSmsDatabase.getConversationCount(threadId);

View File

@ -22,9 +22,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.conversation.ConversationActivity
@ -46,7 +48,10 @@ import org.thoughtcrime.securesms.loki.dialogs.*
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import java.io.IOException import java.io.IOException
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { class HomeActivity : PassphraseRequiredActionBarActivity,
ConversationClickListener,
SeedReminderViewDelegate,
NewConversationButtonSetViewDelegate {
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null private var broadcastReceiver: BroadcastReceiver? = null
@ -71,9 +76,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
glide = GlideApp.with(this) glide = GlideApp.with(this)
// Set up toolbar buttons // Set up toolbar buttons
profileButton.glide = glide profileButton.glide = glide
profileButton.publicKey = publicKey updateProfileButton()
profileButton.displayName = TextSecurePreferences.getProfileName(this)
profileButton.update()
profileButton.setOnClickListener { openSettings() } profileButton.setOnClickListener { openSettings() }
pathStatusViewContainer.disableClipping() pathStatusViewContainer.disableClipping()
pathStatusViewContainer.setOnClickListener { showPath() } pathStatusViewContainer.setOnClickListener { showPath() }
@ -149,6 +152,12 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
} }
this.broadcastReceiver = broadcastReceiver this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged")) LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
lifecycleScope.launchWhenResumed {
// update things based on TextSecurePrefs (profile info etc)
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
updateProfileButton()
}
}
} }
override fun onResume() { override fun onResume() {
@ -198,6 +207,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
} }
private fun updateProfileButton() {
profileButton.publicKey = publicKey
profileButton.displayName = TextSecurePreferences.getProfileName(this)
profileButton.recycle()
profileButton.update()
}
// endregion // endregion
// region Interaction // region Interaction
@ -356,7 +372,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
private fun openSettings() { private fun openSettings() {
val intent = Intent(this, SettingsActivity::class.java) val intent = Intent(this, SettingsActivity::class.java)
show(intent) show(intent, isForResult = true)
} }
private fun showPath() { private fun showPath() {

View File

@ -68,6 +68,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
return TextSecurePreferences.getLocalNumber(this)!! return TextSecurePreferences.getLocalNumber(this)!!
} }
companion object {
const val updatedProfileResultCode = 1234
}
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)

View File

@ -12,12 +12,18 @@ import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.sskenvironment.ProfileManager
import java.util.* import java.util.*
object MultiDeviceProtocol { object MultiDeviceProtocol {
@ -76,7 +82,7 @@ object MultiDeviceProtocol {
// TODO: remove this after we migrate to new message receiving pipeline // TODO: remove this after we migrate to new message receiving pipeline
@JvmStatic @JvmStatic
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) { fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return // if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
if (senderPublicKey != userPublicKey) return if (senderPublicKey != userPublicKey) return
@ -103,6 +109,37 @@ object MultiDeviceProtocol {
if (allOpenGroups.contains(openGroup)) continue if (allOpenGroups.contains(openGroup)) continue
OpenGroupUtilities.addGroup(context, openGroup, 1) OpenGroupUtilities.addGroup(context, openGroup, 1)
} }
if (configurationMessage.profileKey.isNotEmpty()) {
val profileKey = Base64.encodeBytes(configurationMessage.profileKey)
TextSecurePreferences.setProfileKey(context, profileKey)
}
if (!configurationMessage.profilePicture.isNullOrEmpty()) {
TextSecurePreferences.setProfilePictureURL(context, configurationMessage.profilePicture)
}
if (configurationMessage.displayName.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, configurationMessage.displayName)
}
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
for (contact in configurationMessage.contacts) {
val address = Address.fromSerialized(contact.publicKey)
val recipient = Recipient.from(context, address, true)
if (!contact.profilePicture.isNullOrEmpty()) {
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
}
if (contact.profileKey?.isNotEmpty() == true) {
recipientDatabase.setProfileKey(recipient, contact.profileKey)
}
if (contact.name.isNotEmpty()) {
recipientDatabase.setProfileName(recipient, contact.name)
}
recipientDatabase.setProfileSharing(recipient, true)
// create Thread if needed
threadDatabase.getOrCreateThreadIdFor(recipient)
}
if (configurationMessage.contacts.isNotEmpty()) {
threadDatabase.notifyUpdatedFromConfig()
}
// TODO: handle new configuration message fields or handle in new pipeline // TODO: handle new configuration message fields or handle in new pipeline
TextSecurePreferences.setConfigurationMessageSynced(context, true) TextSecurePreferences.setConfigurationMessageSynced(context, true)
} }

View File

@ -58,6 +58,7 @@ allprojects {
} }
project.ext { project.ext {
kotlin_version = "1.4.31"
androidBuildToolsVersion = '29.0.3' androidBuildToolsVersion = '29.0.3'
androidCompileSdkVersion = 29 // This is also our target SDK. androidCompileSdkVersion = 29 // This is also our target SDK.
androidMinSdkVersion = 21 androidMinSdkVersion = 21

View File

@ -38,7 +38,7 @@ dependencies {
// Remote: // Remote:
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'
implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar" implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'
@ -61,10 +61,10 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.threeten:threetenbp:1.3.6" implementation "org.threeten:threetenbp:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation "junit:junit:3.8.2" testImplementation "junit:junit:3.8.2"

View File

@ -123,7 +123,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
val displayName = configurationProto.displayName val displayName = configurationProto.displayName
val profilePicture = configurationProto.profilePicture val profilePicture = configurationProto.profilePicture
val profileKey = configurationProto.profileKey val profileKey = configurationProto.profileKey
return ConfigurationMessage(closedGroups, openGroups, listOf(), displayName, profilePicture, profileKey.toByteArray()) val contacts = configurationProto.contactsList.mapNotNull { Contact.fromProto(it) }
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey.toByteArray())
} }
} }

View File

@ -7,6 +7,9 @@ import android.preference.PreferenceManager.getDefaultSharedPreferences
import android.provider.Settings import android.provider.Settings
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R import org.session.libsession.R
import org.session.libsession.utilities.preferences.NotificationPrivacyPreference import org.session.libsession.utilities.preferences.NotificationPrivacyPreference
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
@ -16,6 +19,9 @@ import java.util.*
object TextSecurePreferences { object TextSecurePreferences {
private val TAG = TextSecurePreferences::class.simpleName private val TAG = TextSecurePreferences::class.simpleName
private val _events = MutableSharedFlow<String>(0, 64, BufferOverflow.DROP_OLDEST)
val events get() = _events.asSharedFlow()
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
const val THEME_PREF = "pref_theme" const val THEME_PREF = "pref_theme"
const val LANGUAGE_PREF = "pref_language" const val LANGUAGE_PREF = "pref_language"
@ -321,6 +327,7 @@ object TextSecurePreferences {
@JvmStatic @JvmStatic
fun setProfileName(context: Context, name: String?) { fun setProfileName(context: Context, name: String?) {
setStringPreference(context, PROFILE_NAME_PREF, name) setStringPreference(context, PROFILE_NAME_PREF, name)
_events.tryEmit(PROFILE_NAME_PREF)
} }
@JvmStatic @JvmStatic