Merge remote-tracking branch 'upstream/dev' into message-request-response

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
This commit is contained in:
Morgan Pretty
2023-01-24 14:44:56 +11:00
534 changed files with 7258 additions and 5873 deletions

View File

@@ -19,17 +19,16 @@ android {
dependencies {
implementation project(":libsignal")
implementation project(":liblazysodium")
// implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.google.dagger:hilt-android:$daggerVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.annimon:stream:1.1.8'
@@ -43,7 +42,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation 'junit:junit:4.12'
testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
@@ -51,7 +50,7 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.3.0'
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0"

View File

@@ -1,9 +1,11 @@
package org.session.libsession.avatars
import android.content.Context
import com.bumptech.glide.load.Key
import java.security.MessageDigest
class PlaceholderAvatarPhoto(val hashString: String,
class PlaceholderAvatarPhoto(val context: Context,
val hashString: String,
val displayName: String): Key {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {

View File

@@ -12,7 +12,6 @@ import androidx.annotation.DrawableRes;
import com.amulyakhare.textdrawable.TextDrawable;
import com.makeramen.roundedimageview.RoundedDrawable;
import org.session.libsession.R;
import org.session.libsession.utilities.ThemeUtil;
@@ -34,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER);
foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);

View File

@@ -20,7 +20,9 @@ interface MessageDataProvider {
* @return pair of sms or mms table-specific ID and whether it is in SMS table
*/
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: Long, isSms: Boolean)
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
fun updateMessageAsDeleted(timestamp: Long, author: String)
fun getServerHashForMessage(messageID: Long): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?

View File

@@ -17,6 +17,7 @@ import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.GroupMember
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
@@ -65,13 +66,12 @@ interface StorageProtocol {
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun updateOpenGroup(openGroup: OpenGroup)
fun getOpenGroup(threadId: Long): OpenGroup?
fun addOpenGroup(urlAsString: String)
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
fun onOpenGroupAdded(server: String)
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
fun getOpenGroup(room: String, server: String): OpenGroup?
fun addGroupMemberRole(member: GroupMember)
fun clearGroupMemberRoles(groupId: String)
fun setGroupMemberRoles(members: List<GroupMember>)
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?
@@ -80,6 +80,7 @@ interface StorageProtocol {
// Open Group Metadata
fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun hasDownloadedProfilePicture(groupID: String): Boolean
fun setUserCount(room: String, server: String, newValue: Int)
// Last Message Server ID
@@ -108,6 +109,7 @@ interface StorageProtocol {
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
fun clearErrorMessage(messageID: Long)
fun setMessageServerHash(messageID: Long, serverHash: String)
// Closed Groups
@@ -196,4 +198,6 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean)
fun unblock(toUnblock: List<Recipient>)
fun blockedContacts(): List<Recipient>
}

View File

@@ -77,7 +77,11 @@ object FileServerApi {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
it.body ?: throw Error.ParsingFailed
}.fail { e ->
Log.e("Loki", "File server request failed.", e)
when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "File server request failed due to error: ${e.message}")
else -> Log.e("Loki", "File server request failed", e)
}
}
} else {
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
@@ -96,7 +100,10 @@ object FileServerApi {
)
return send(request).map { response ->
val json = JsonUtil.fromJson(response, Map::class.java)
(json["id"] as? String)?.toLong() ?: throw Error.ParsingFailed
val hasId = json.containsKey("id")
val id = json.getOrDefault("id", null)
Log.d("Loki-FS", "File Upload Response hasId: $hasId of type: ${id?.javaClass}")
(id as? String)?.toLong() ?: throw Error.ParsingFailed
}
}

View File

@@ -41,15 +41,10 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
}
// get image
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server, false).get()
storage.setServerCapabilities(openGroup.server, capabilities.capabilities)
val imageId = info.imageId
storage.addOpenGroup(openGroup.joinUrl())
val info = storage.addOpenGroup(openGroup.joinUrl())
val imageId = info?.imageId
if (imageId != null) {
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get()
val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray())
storage.updateProfilePicture(groupId, bytes)
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server))
}
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
storage.onOpenGroupAdded(openGroup.server)

View File

@@ -94,12 +94,23 @@ class BatchMessageReceiveJob(
threadMap[threadID]!! += parsedParams
}
} catch (e: Exception) {
Log.e(TAG, "Couldn't receive message.", e)
if (e is MessageReceiver.Error && !e.isRetryable) {
Log.e(TAG, "Message failed permanently",e)
} else {
Log.e(TAG, "Message failed",e)
failures += messageParameters
when (e) {
is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}")
}
is MessageReceiver.Error -> {
if (!e.isRetryable) {
Log.e(TAG, "Couldn't receive message, failed permanently", e)
}
else {
Log.e(TAG, "Couldn't receive message, failed", e)
failures += messageParameters
}
}
else -> {
Log.e(TAG, "Couldn't receive message, failed", e)
failures += messageParameters
}
}
}
}

View File

@@ -14,10 +14,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
override fun execute() {
val storage = MessagingModuleConfiguration.shared.storage
val imageId = storage.getOpenGroup(room, server)?.imageId ?: return
try {
val info = OpenGroupApi.getRoomInfo(room, server).get()
val imageId = info.imageId ?: return
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get()
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
storage.updateProfilePicture(groupId, bytes)
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())

View File

@@ -26,7 +26,7 @@ class JobQueue : JobDelegate {
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
private val openGroupDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED)
@@ -56,7 +56,7 @@ class JobQueue : JobDelegate {
handleJobFailedPermanently(job, NullPointerException("Open Group ID was null"))
} else {
val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) {
Log.d("OpenGroupDispatcher", "Creating $openGroupId channel")
Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel")
val newGroupChannel = Channel<Job>(UNLIMITED)
launch(dispatcher) {
for (groupJob in newGroupChannel) {

View File

@@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
class MessageSendJob(val message: Message, val destination: Destination) : Job {
@@ -67,14 +68,25 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val promise = MessageSender.send(this.message, this.destination).success {
this.handleSuccess()
}.fail { exception ->
Log.e(TAG, "Couldn't send message due to error: $exception.")
if (exception is MessageSender.Error) {
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
var logStacktrace = true
when (exception) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> {
logStacktrace = false
if (exception.statusCode == 429) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
}
is MessageSender.Error -> {
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
}
else -> this.handleFailure(exception)
}
if (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 429) {
this.handlePermanentFailure(exception)
}
this.handleFailure(exception)
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
else { Log.e(TAG, "Couldn't send message due to error: ${exception.message}") }
}
try {
promise.get()

View File

@@ -23,14 +23,27 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val numberToDelete = messageServerIds.size
Log.d(TAG, "Deleting $numberToDelete messages")
var numberDeleted = 0
messageServerIds.forEach { serverId ->
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach
dataProvider.deleteMessage(messageId, isSms)
numberDeleted++
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
try {
val messageIds = dataProvider.getMessageIDs(messageServerIds.toList(), threadId)
// Delete the SMS messages
if (messageIds.first.isNotEmpty()) {
dataProvider.deleteMessages(messageIds.first, threadId, true)
}
// Delete the MMS messages
if (messageIds.second.isNotEmpty()) {
dataProvider.deleteMessages(messageIds.second, threadId, false)
}
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
delegate?.handleJobSucceeded(this)
}
catch (e: Exception) {
delegate?.handleJobFailed(this, e)
}
Log.d(TAG, "Deleted $numberDeleted messages successfully")
delegate?.handleJobSucceeded(this)
}
override fun serialize(): Data = Data.Builder()

View File

@@ -11,15 +11,17 @@ data class OpenGroup(
val id: String,
val name: String,
val publicKey: String,
val imageId: String?,
val infoUpdates: Int,
) {
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, infoUpdates: Int) : this(
server = server,
room = room,
id = "$server.$room",
name = name,
publicKey = publicKey,
imageId = imageId,
infoUpdates = infoUpdates,
)
@@ -31,11 +33,12 @@ data class OpenGroup(
if (!json.has("room")) return null
val room = json.get("room").asText().toLowerCase(Locale.US)
val server = json.get("server").asText().toLowerCase(Locale.US)
val displayName = json.get("displayName").asText()
val publicKey = json.get("publicKey").asText()
val displayName = json.get("displayName").asText()
val imageId = json.get("imageId")?.asText()
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
OpenGroup(server, room, displayName, infoUpdates, publicKey)
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, infoUpdates = infoUpdates)
} catch (e: Exception) {
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
null
@@ -53,11 +56,12 @@ data class OpenGroup(
}
}
fun toJson(): Map<String,String> = mapOf(
fun toJson(): Map<String,String?> = mapOf(
"room" to room,
"server" to server,
"displayName" to name,
"publicKey" to publicKey,
"displayName" to name,
"imageId" to imageId,
"infoUpdates" to infoUpdates.toString(),
)

View File

@@ -91,7 +91,7 @@ object OpenGroupApi {
val created: Long = 0,
val activeUsers: Int = 0,
val activeUsersCutoff: Int = 0,
val imageId: Long? = null,
val imageId: String? = null,
val pinnedMessages: List<PinnedMessage> = emptyList(),
val admin: Boolean = false,
val globalAdmin: Boolean = false,
@@ -148,7 +148,7 @@ object OpenGroupApi {
)
enum class Capability {
BLIND, REACTIONS
SOGS, BLIND, REACTIONS
}
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
@@ -337,7 +337,7 @@ object OpenGroupApi {
.plus(request.verb.rawValue.toByteArray())
.plus("/${request.endpoint.value}".toByteArray())
.plus(bodyHash)
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
pubKey = SessionId(
IdPrefix.BLINDED,
@@ -383,7 +383,11 @@ object OpenGroupApi {
}
return if (request.useOnionRouting) {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
Log.e("SOGS", "Failed onion request", e)
when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}")
else -> Log.e("SOGS", "Failed onion request", e)
}
}
} else {
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
@@ -395,13 +399,13 @@ object OpenGroupApi {
fun downloadOpenGroupProfilePicture(
server: String,
roomID: String,
imageId: Long
imageId: String
): Promise<ByteArray, Exception> {
val request = Request(
verb = GET,
room = roomID,
server = server,
endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString())
endpoint = Endpoint.RoomFileIndividual(roomID, imageId)
)
return getResponseBody(request)
}
@@ -794,16 +798,14 @@ object OpenGroupApi {
private fun sequentialBatch(
server: String,
requests: MutableList<BatchRequestInfo<*>>,
authRequired: Boolean = true
requests: MutableList<BatchRequestInfo<*>>
): Promise<List<BatchResponse<*>>, Exception> {
val request = Request(
verb = POST,
room = null,
server = server,
endpoint = Endpoint.Sequence,
parameters = requests.map { it.request },
isAuthRequired = authRequired
parameters = requests.map { it.request }
)
return getBatchResponseJson(request, requests)
}
@@ -912,8 +914,7 @@ object OpenGroupApi {
fun getCapabilitiesAndRoomInfo(
room: String,
server: String,
authRequired: Boolean = true
server: String
): Promise<Pair<Capabilities, RoomInfo>, Exception> {
val requests = mutableListOf<BatchRequestInfo<*>>(
BatchRequestInfo(
@@ -933,7 +934,7 @@ object OpenGroupApi {
responseType = object : TypeReference<RoomInfo>(){}
)
)
return sequentialBatch(server, requests, authRequired).map {
return sequentialBatch(server, requests).map {
val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed
val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed
capabilities to roomInfo

View File

@@ -32,12 +32,7 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.defaultRequiresAuth
import org.session.libsignal.utilities.hasNamespaces
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
@@ -326,6 +321,8 @@ object MessageSender {
message.serverHash?.let {
storage.setMessageServerHash(messageID, it)
}
// in case any errors from previous sends
storage.clearErrorMessage(messageID)
// Track the open group server message ID
if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
val server: String

View File

@@ -231,12 +231,18 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
throw MessageReceiver.Error.NoThread
}
val threadRecipient = storage.getRecipientForThread(threadID)
val userBlindedKey = openGroupID?.let {
val openGroup = storage.getOpenGroup(threadID) ?: return@let null
val blindedKey = SodiumUtilities.blindedKeyPair(openGroup.publicKey, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) ?: return@let null
SessionId(
IdPrefix.BLINDED, blindedKey.publicKey.asBytes
).hexString
}
// Update profile if needed
val recipient = Recipient.from(context, Address.fromSerialized(messageSender!!), false)
if (runProfileUpdate) {
val profile = message.profile
val isUserBlindedSender = messageSender == storage.getOpenGroup(threadID)?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
val isUserBlindedSender = messageSender == userBlindedKey
if (profile != null && userPublicKey != messageSender && !isUserBlindedSender) {
val profileManager = SSKEnvironment.shared.profileManager
val name = profile.displayName!!
@@ -260,7 +266,13 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
var quoteModel: QuoteModel? = null
if (message.quote != null && proto.dataMessage.hasQuote()) {
val quote = proto.dataMessage.quote
val author = Address.fromSerialized(quote.author)
val author = if (quote.author == userBlindedKey) {
Address.fromSerialized(userPublicKey!!)
} else {
Address.fromSerialized(quote.author)
}
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
quoteModel = if (messageInfo != null) {
@@ -295,6 +307,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
return@mapNotNull attachment
}
}
// Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!)
// Parse reaction if needed
val threadIsGroup = threadRecipient?.isGroupRecipient == true
message.reaction?.let { reaction ->
@@ -320,8 +334,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
}
return messageID
}
// Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!)
return null
}
@@ -411,7 +423,7 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
if (!recipient.isApproved) return
if (!recipient.isApproved && !recipient.isLocalNumber) return
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }

View File

@@ -13,6 +13,7 @@ import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.guava.Optional;
import java.io.IOException;
import java.util.Objects;
public class LinkPreview {
@@ -75,4 +76,17 @@ public class LinkPreview {
public static LinkPreview deserialize(@NonNull String serialized) throws IOException {
return JsonUtil.fromJson(serialized, LinkPreview.class);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LinkPreview that = (LinkPreview) o;
return Objects.equals(url, that.url) && Objects.equals(title, that.title) && Objects.equals(attachmentId, that.attachmentId) && Objects.equals(thumbnail, that.thumbnail);
}
@Override
public int hashCode() {
return Objects.hash(url, title, attachmentId, thumbnail);
}
}

View File

@@ -59,7 +59,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
responses.filterNot { it.body == null }.forEach { response ->
when (response.endpoint) {
@@ -117,15 +117,17 @@ class OpenGroupPoller(private val server: String, private val executorService: S
) {
val storage = MessagingModuleConfiguration.shared.storage
val groupId = "$server.$roomToken"
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
val publicKey = existingOpenGroup?.publicKey ?: return
val openGroup = OpenGroup(
server = server,
room = pollInfo.token,
name = pollInfo.details?.name ?: "",
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name },
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates },
publicKey = publicKey,
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId }
)
// - Open Group changes
storage.updateOpenGroup(openGroup)
@@ -134,20 +136,42 @@ class OpenGroupPoller(private val server: String, private val executorService: S
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
// - Moderators
storage.clearGroupMemberRoles(groupId)
pollInfo.details?.moderators?.forEach {
storage.addGroupMemberRole(GroupMember(groupId, it, GroupMemberRole.MODERATOR))
pollInfo.details?.moderators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.MODERATOR)
})
}
pollInfo.details?.hiddenModerators?.forEach {
storage.addGroupMemberRole(GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR))
pollInfo.details?.hiddenModerators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)
})
}
// - Admins
pollInfo.details?.admins?.forEach {
storage.addGroupMemberRole(GroupMember(groupId, it, GroupMemberRole.ADMIN))
pollInfo.details?.admins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.ADMIN)
})
}
pollInfo.details?.hiddenAdmins?.forEach {
storage.addGroupMemberRole(GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN))
pollInfo.details?.hiddenAdmins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
})
}
if (
(
pollInfo.details != null &&
pollInfo.details.imageId != null && (
pollInfo.details.imageId != existingOpenGroup.imageId ||
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) || (
pollInfo.details == null &&
existingOpenGroup.imageId != null &&
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) {
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
}
}
@@ -278,16 +302,4 @@ class OpenGroupPoller(private val server: String, private val executorService: S
JobQueue.shared.add(deleteJob)
}
}
private fun downloadGroupAvatarIfNeeded(room: String) {
val storage = MessagingModuleConfiguration.shared.storage
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
storage.getGroup(groupId)?.let {
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
}
}
}
}

View File

@@ -3,7 +3,7 @@ package org.session.libsession.messaging.sending_receiving.quotes
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.utilities.Address
class QuoteModel(val id: Long,
data class QuoteModel(val id: Long,
val author: Address,
val text: String?,
val missing: Boolean,

View File

@@ -78,8 +78,8 @@ object OnionRequestAPI {
// endregion
class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination)
open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String)
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.")
open class HTTPRequestFailedAtDestinationException(statusCode: Int, json: Map<*, *>, val destination: String)
: HTTP.HTTPRequestFailedException(statusCode, json, "HTTP request failed at destination ($destination) with status code $statusCode.")
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
private data class OnionBuildingResult(

View File

@@ -56,6 +56,10 @@ object SnodeAPI {
* user's clock is incorrect.
*/
internal var clockOffset = 0L
val nowWithOffset
get() = System.currentTimeMillis() + clockOffset
internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue ->
if (newValue > oldValue) {
Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue")

View File

@@ -2,6 +2,7 @@ package org.session.libsession.utilities
import okhttp3.HttpUrl
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
import java.io.*
@@ -40,7 +41,11 @@ object DownloadUtilities {
outputStream.write(it)
}
} catch (e: Exception) {
Log.e("Loki", "Couldn't download attachment.", e)
when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "Couldn't download attachment due to error: ${e.message}")
else -> Log.e("Loki", "Couldn't download attachment", e)
}
throw e
}
}

View File

@@ -5,6 +5,7 @@ import android.hardware.Camera
import android.net.Uri
import android.provider.Settings
import androidx.annotation.ArrayRes
import androidx.annotation.StyleRes
import androidx.core.app.NotificationCompat
import androidx.preference.PreferenceManager.getDefaultSharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -13,8 +14,16 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.BuildConfig
import org.session.libsession.R
import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_LIGHT
import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME
import org.session.libsession.utilities.TextSecurePreferences.Companion.LEGACY_PREF_KEY_SELECTED_UI_MODE
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
import org.session.libsession.utilities.TextSecurePreferences.Companion.SELECTED_STYLE
import org.session.libsession.utilities.TextSecurePreferences.Companion.SHOWN_CALL_NOTIFICATION
import org.session.libsession.utilities.TextSecurePreferences.Companion.SHOWN_CALL_WARNING
import org.session.libsignal.utilities.Log
@@ -113,9 +122,7 @@ interface TextSecurePreferences {
fun setNotificationRingtone(ringtone: String?)
fun setNotificationVibrateEnabled(enabled: Boolean)
fun isNotificationVibrateEnabled(): Boolean
fun getNotificationLedColor(): String?
fun getNotificationLedPattern(): String?
fun getNotificationLedPatternCustom(): String?
fun getNotificationLedColor(): Int
fun isThreadLengthTrimmingEnabled(): Boolean
fun isSystemEmojiPreferred(): Boolean
fun getMobileMediaDownloadAllowed(): Set<String>?
@@ -164,6 +171,14 @@ interface TextSecurePreferences {
fun setLastVacuumNow()
fun getFingerprintKeyGenerated(): Boolean
fun setFingerprintKeyGenerated()
@StyleRes fun getAccentColorStyle(): Int?
fun setAccentColorStyle(@StyleRes newColorStyle: Int?)
fun getThemeStyle(): String
fun getFollowSystemSettings(): Boolean
fun setThemeStyle(themeStyle: String)
fun setFollowSystemSettings(followSystemSettings: Boolean)
fun autoplayAudioMessages(): Boolean
fun hasPreference(key: String): Boolean
fun clearAll()
companion object {
@@ -180,6 +195,7 @@ interface TextSecurePreferences {
const val VIBRATE_PREF = "pref_key_vibrate"
const val NOTIFICATION_PREF = "pref_key_enable_notifications"
const val LED_COLOR_PREF = "pref_led_color"
const val LED_COLOR_PREF_PRIMARY = "pref_led_color_primary"
const val LED_BLINK_PREF = "pref_led_blink"
const val LED_BLINK_PREF_CUSTOM = "pref_led_blink_custom"
const val PASSPHRASE_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval"
@@ -194,6 +210,7 @@ interface TextSecurePreferences {
const val UPDATE_APK_DOWNLOAD_ID = "pref_update_apk_download_id"
const val UPDATE_APK_DIGEST = "pref_update_apk_digest"
const val IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"
const val IN_APP_NOTIFICATION_SOUNDS = "pref_sound_when_app_open"
const val MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size"
const val LOCAL_REGISTRATION_ID_PREF = "pref_local_registration_id"
const val REPEAT_ALERTS_PREF = "pref_repeat_alerts"
@@ -242,9 +259,27 @@ interface TextSecurePreferences {
const val HAS_HIDDEN_MESSAGE_REQUESTS = "pref_message_requests_hidden"
const val CALL_NOTIFICATIONS_ENABLED = "pref_call_notifications_enabled"
const val SHOWN_CALL_WARNING = "pref_shown_call_warning" // call warning is user-facing warning of enabling calls
const val SHOWN_CALL_NOTIFICATION = "pref_shown_call_notification" // call notification is a promp to check privacy settings
const val SHOWN_CALL_NOTIFICATION = "pref_shown_call_notification" // call notification is a prompt to check privacy settings
const val LAST_VACUUM_TIME = "pref_last_vacuum_time"
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color"
const val GREEN_ACCENT = "accent_green"
const val BLUE_ACCENT = "accent_blue"
const val PURPLE_ACCENT = "accent_purple"
const val PINK_ACCENT = "accent_pink"
const val RED_ACCENT = "accent_red"
const val ORANGE_ACCENT = "accent_orange"
const val YELLOW_ACCENT = "accent_yellow"
const val SELECTED_STYLE = "pref_selected_style" // classic_dark/light, ocean_dark/light
const val FOLLOW_SYSTEM_SETTINGS = "pref_follow_system" // follow system day/night
const val LEGACY_PREF_KEY_SELECTED_UI_MODE = "SELECTED_UI_MODE" // this will be cleared upon launching app, for users migrating to theming build
const val CLASSIC_DARK = "classic.dark"
const val CLASSIC_LIGHT = "classic.light"
const val OCEAN_DARK = "ocean.dark"
const val OCEAN_LIGHT = "ocean.light"
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
@@ -687,18 +722,8 @@ interface TextSecurePreferences {
}
@JvmStatic
fun getNotificationLedColor(context: Context): String? {
return getStringPreference(context, LED_COLOR_PREF, "blue")
}
@JvmStatic
fun getNotificationLedPattern(context: Context): String? {
return getStringPreference(context, LED_BLINK_PREF, "500,2000")
}
@JvmStatic
fun getNotificationLedPatternCustom(context: Context): String? {
return getStringPreference(context, LED_BLINK_PREF_CUSTOM, "500,2000")
fun getNotificationLedColor(context: Context): Int {
return getIntegerPreference(context, LED_COLOR_PREF_PRIMARY, ThemeUtil.getThemedColor(context, R.attr.colorAccent))
}
@JvmStatic
@@ -929,6 +954,34 @@ interface TextSecurePreferences {
setBooleanPreference(context, FINGERPRINT_KEY_GENERATED, true)
}
@JvmStatic @StyleRes
fun getAccentColorStyle(context: Context): Int? {
return when (getStringPreference(context, SELECTED_ACCENT_COLOR, ORANGE_ACCENT)) {
GREEN_ACCENT -> R.style.PrimaryGreen
BLUE_ACCENT -> R.style.PrimaryBlue
PURPLE_ACCENT -> R.style.PrimaryPurple
PINK_ACCENT -> R.style.PrimaryPink
RED_ACCENT -> R.style.PrimaryRed
ORANGE_ACCENT -> R.style.PrimaryOrange
YELLOW_ACCENT -> R.style.PrimaryYellow
else -> null
}
}
@JvmStatic
fun setAccentColorStyle(context: Context, @StyleRes newColor: Int?) {
setStringPreference(context, SELECTED_ACCENT_COLOR, when (newColor) {
R.style.PrimaryGreen -> GREEN_ACCENT
R.style.PrimaryBlue -> BLUE_ACCENT
R.style.PrimaryPurple -> PURPLE_ACCENT
R.style.PrimaryPink -> PINK_ACCENT
R.style.PrimaryRed -> RED_ACCENT
R.style.PrimaryOrange -> ORANGE_ACCENT
R.style.PrimaryYellow -> YELLOW_ACCENT
else -> null
})
}
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
@@ -1309,16 +1362,8 @@ class AppTextSecurePreferences @Inject constructor(
return getBooleanPreference(TextSecurePreferences.VIBRATE_PREF, true)
}
override fun getNotificationLedColor(): String? {
return getStringPreference(TextSecurePreferences.LED_COLOR_PREF, "blue")
}
override fun getNotificationLedPattern(): String? {
return getStringPreference(TextSecurePreferences.LED_BLINK_PREF, "500,2000")
}
override fun getNotificationLedPatternCustom(): String? {
return getStringPreference(TextSecurePreferences.LED_BLINK_PREF_CUSTOM, "500,2000")
override fun getNotificationLedColor(): Int {
return getIntegerPreference(TextSecurePreferences.LED_COLOR_PREF_PRIMARY, context.getColor(R.color.accent_green))
}
override fun isThreadLengthTrimmingEnabled(): Boolean {
@@ -1413,6 +1458,10 @@ class AppTextSecurePreferences @Inject constructor(
getDefaultSharedPreferences(context).edit().putLong(key, value).apply()
}
override fun hasPreference(key: String): Boolean {
return getDefaultSharedPreferences(context).contains(key)
}
override fun removePreference(key: String) {
getDefaultSharedPreferences(context).edit().remove(key).apply()
}
@@ -1533,6 +1582,92 @@ class AppTextSecurePreferences @Inject constructor(
setBooleanPreference(TextSecurePreferences.FINGERPRINT_KEY_GENERATED, true)
}
@StyleRes
override fun getAccentColorStyle(): Int? {
val prefColor = getStringPreference(
TextSecurePreferences.SELECTED_ACCENT_COLOR,
null
)
return when (prefColor) {
TextSecurePreferences.GREEN_ACCENT -> R.style.PrimaryGreen
TextSecurePreferences.BLUE_ACCENT -> R.style.PrimaryBlue
TextSecurePreferences.PURPLE_ACCENT -> R.style.PrimaryPurple
TextSecurePreferences.PINK_ACCENT -> R.style.PrimaryPink
TextSecurePreferences.RED_ACCENT -> R.style.PrimaryRed
TextSecurePreferences.ORANGE_ACCENT -> R.style.PrimaryOrange
TextSecurePreferences.YELLOW_ACCENT -> R.style.PrimaryYellow
else -> null
}
}
override fun setAccentColorStyle(@StyleRes newColorStyle: Int?) {
setStringPreference(
TextSecurePreferences.SELECTED_ACCENT_COLOR, when (newColorStyle) {
R.style.PrimaryGreen -> TextSecurePreferences.GREEN_ACCENT
R.style.PrimaryBlue -> TextSecurePreferences.BLUE_ACCENT
R.style.PrimaryPurple -> TextSecurePreferences.PURPLE_ACCENT
R.style.PrimaryPink -> TextSecurePreferences.PINK_ACCENT
R.style.PrimaryRed -> TextSecurePreferences.RED_ACCENT
R.style.PrimaryOrange -> TextSecurePreferences.ORANGE_ACCENT
R.style.PrimaryYellow -> TextSecurePreferences.YELLOW_ACCENT
else -> null
}
)
}
override fun getThemeStyle(): String {
val hasLegacy = getStringPreference(LEGACY_PREF_KEY_SELECTED_UI_MODE, null)
if (!hasLegacy.isNullOrEmpty()) {
migrateLegacyUiPref()
}
return getStringPreference(SELECTED_STYLE, CLASSIC_DARK)!!
}
override fun setThemeStyle(themeStyle: String) {
val safeTheme = if (themeStyle !in listOf(CLASSIC_DARK, CLASSIC_LIGHT, OCEAN_DARK, OCEAN_LIGHT)) CLASSIC_DARK else themeStyle
setStringPreference(SELECTED_STYLE, safeTheme)
}
override fun getFollowSystemSettings(): Boolean {
val hasLegacy = getStringPreference(LEGACY_PREF_KEY_SELECTED_UI_MODE, null)
if (!hasLegacy.isNullOrEmpty()) {
migrateLegacyUiPref()
}
return getBooleanPreference(FOLLOW_SYSTEM_SETTINGS, false)
}
private fun migrateLegacyUiPref() {
val legacy = getStringPreference(LEGACY_PREF_KEY_SELECTED_UI_MODE, null) ?: return
val (mode, followSystem) = when (legacy) {
"DAY" -> {
CLASSIC_LIGHT to false
}
"NIGHT" -> {
CLASSIC_DARK to false
}
"SYSTEM_DEFAULT" -> {
CLASSIC_DARK to true
}
else -> {
CLASSIC_DARK to false
}
}
if (!hasPreference(FOLLOW_SYSTEM_SETTINGS) && !hasPreference(SELECTED_STYLE)) {
setThemeStyle(mode)
setFollowSystemSettings(followSystem)
}
removePreference(LEGACY_PREF_KEY_SELECTED_UI_MODE)
}
override fun setFollowSystemSettings(followSystemSettings: Boolean) {
setBooleanPreference(FOLLOW_SYSTEM_SETTINGS, followSystemSettings)
}
override fun autoplayAudioMessages(): Boolean {
return getBooleanPreference(AUTOPLAY_AUDIO_MESSAGES, false)
}
override fun clearAll() {
getDefaultSharedPreferences(context).edit().clear().commit()

View File

@@ -0,0 +1,16 @@
package org.session.libsession.utilities
import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
@ColorInt
fun Context.getColorFromAttr(
@AttrRes attrColor: Int,
typedValue: TypedValue = TypedValue(),
resolveRefs: Boolean = true
): Int {
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
return typedValue.data
}

View File

@@ -50,10 +50,12 @@ import org.session.libsession.utilities.recipients.RecipientProvider.RecipientDe
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
@@ -732,20 +734,19 @@ public class Recipient implements RecipientModifiedListener {
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Recipient)) return false;
Recipient that = (Recipient) o;
return this.address.equals(that.address);
if (o == null || getClass() != o.getClass()) return false;
Recipient recipient = (Recipient) o;
return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar);
}
@Override
public int hashCode() {
return this.address.hashCode();
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar);
result = 31 * result + Arrays.hashCode(profileKey);
return result;
}
public void notifyListeners() {

View File

@@ -1,8 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<color name="accent">#00F782</color>
<color name="accent_alpha50">#8000F782</color>
<color name="text">#FFFFFF</color>
<color name="destructive">#FF453A</color>
<color name="unimportant">#D8D8D8</color>
<color name="profile_picture_background">#353535</color>
@@ -18,10 +15,6 @@
<color name="compose_view_background">#1B1B1B</color>
<color name="compose_text_view_background">#141414</color>
<color name="quote_not_found_background">#80FFFFFF</color>
<color name="new_conversation_button_collapsed_background">#1F1F1F</color>
<color name="new_conversation_button_shadow">#077C44</color>
<color name="pn_option_background">#1B1B1B</color>
<color name="pn_option_border">#212121</color>
<color name="paths_building">#FFCE3A</color>
<array name="profile_picture_placeholder_colors">
@@ -33,8 +26,8 @@
<color name="loki_darkest_gray">#0a0a0a</color>
<color name="signal_primary">@color/accent</color>
<color name="signal_primary_dark">@color/accent</color>
<color name="signal_primary">@color/accent_green</color>
<color name="signal_primary_dark">@color/accent_green</color>
<color name="textsecure_primary">@color/signal_primary</color>
<color name="textsecure_primary_dark">@color/signal_primary_dark</color>
@@ -75,4 +68,13 @@
<color name="default_background_start">#121212</color>
<color name="default_background_end">#171717</color>
<color name="accent_green">#ff31F196</color>
<color name="accent_blue">#ff57C9FA</color>
<color name="accent_purple">#ffC993FF</color>
<color name="accent_pink">#ffFF95EF</color>
<color name="accent_red">#ffFF9C8E</color>
<color name="accent_orange">#ffFCB159</color>
<color name="accent_yellow">#ffFAD657</color>
</resources>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="PrimaryGreen">
<item name="colorAccent">@color/accent_green</item>
</style>
<style name="PrimaryBlue">
<item name="colorAccent">@color/accent_blue</item>
</style>
<style name="PrimaryPurple">
<item name="colorAccent">@color/accent_purple</item>
</style>
<style name="PrimaryPink">
<item name="colorAccent">@color/accent_pink</item>
</style>
<style name="PrimaryRed">
<item name="colorAccent">@color/accent_red</item>
</style>
<style name="PrimaryOrange">
<item name="colorAccent">@color/accent_orange</item>
</style>
<style name="PrimaryYellow">
<item name="colorAccent">@color/accent_yellow</item>
</style>
</resources>