mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 16:47:42 +00:00
Merge remote-tracking branch 'upstream/dev' into message-request-fixes
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt # libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
This commit is contained in:
@@ -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"
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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?
|
||||
|
@@ -16,6 +16,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
|
||||
@@ -66,13 +67,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?
|
||||
@@ -81,6 +81,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
|
||||
@@ -109,6 +110,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
|
||||
@@ -197,4 +199,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>
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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())
|
||||
|
@@ -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) {
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
@@ -11,16 +11,17 @@ data class OpenGroup(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String,
|
||||
val imageId: String?,
|
||||
val infoUpdates: Int,
|
||||
val canWrite: Boolean,
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String, canWrite: Boolean) : this(
|
||||
constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, canWrite: Boolean, infoUpdates: Int) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
imageId = imageId,
|
||||
infoUpdates = infoUpdates,
|
||||
canWrite = canWrite
|
||||
)
|
||||
@@ -35,9 +36,10 @@ data class OpenGroup(
|
||||
val server = json.get("server").asText().lowercase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||
val imageId = json.get("imageId")?.asText()
|
||||
val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true
|
||||
OpenGroup(server, room, displayName, infoUpdates, publicKey, canWrite)
|
||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
@@ -55,11 +57,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(),
|
||||
"canWrite" to canWrite.toString()
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -8,11 +8,7 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.NotifyPNServerJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
@@ -32,12 +28,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
|
||||
@@ -337,6 +328,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
|
||||
|
@@ -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() }
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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,16 +117,18 @@ 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 },
|
||||
publicKey = publicKey,
|
||||
canWrite = pollInfo.write
|
||||
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId },
|
||||
canWrite = pollInfo.write,
|
||||
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates }
|
||||
)
|
||||
// - Open Group changes
|
||||
storage.updateOpenGroup(openGroup)
|
||||
@@ -135,20 +137,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,16 +303,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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,
|
||||
|
@@ -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(
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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>
|
||||
|
24
libsession/src/main/res/values/themes.xml
Normal file
24
libsession/src/main/res/values/themes.xml
Normal 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>
|
Reference in New Issue
Block a user