mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 20:37:39 +00:00
Add emoji reacts support (#889)
* feat: Add emoji reacts support * Remove message multi-selection * Add emoji reaction model * Add emoji reaction panel * Blur reacts panel background * Show emoji keyboard * Add emoji sprites * Update reaction proto * Emoji database updates * Emoji database refactor * Emoji reaction persistence * Optimize reactions retrieval * Fix emoji group query * Display emojis * Fix emoji persistence * Cleanup * Persistence refactor * Add reactions bottom sheet * Cleanup * Ui tweaks * React with any emoji * Show emoji react notifications * Remove reaction * Show reactions modal on long press * Click to react (+1) with an emoji * Click to react with an emoji * Enable emoji expand/collapse * fix: some compile issues from merge conflicts * fix: compile issues merging quote and media message UI * fix: xml IDs and adding in legacy is selected for future inclusion * Fix view constraints * Fix merge issue * Add message selection option in conversation context menu * Add sogs emoji integration * Handle sogs emoji reactions * Enable sending/deleting sogs emojis * fix: improve the visible message layout * fix: add file IDs to request parameters for message send (#940) * Fix open group polling from seqno instead of last hash (#939) * fix: reset seqno to get recent messages from open groups * build: upgrade build numbers * fix: actually run the migration * Using StringBuilder to construct request url * Fix reaction filter * fix: is_mms added in second projection query * Update default emojis * fix: include legacy and new open groups in server ID tracking (#941) * feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942) * Cleanup * Fix view constraints * Add reactions capability check * Fix reactions alignment * Ui fixes * Display reactions list * feat: add formatted count strings * fix: account for negatives and add tests * Migrate old official open group locations for polling and adding (#932) * feat: adding in first part of open group migrations and tests for migration logic / helpers * feat: test code and migration logic for open groups in the case of no conflicts * feat: add in extra test cases and refactor code for migrator * refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it * refactor: joining open groups using OpenGroupUrlParser.kt now * fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2 * fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key * fix: room and server swapped parameters * fix: replace default server for config messages * fix: actually using public key to de-dupe didn't work for rooms * build: bump version code and name * Display reactions list on open groups for moderators * Ui tweaks * Ui tweaks for moderation * Refactor * fix: compile issue * fix: de-duping joined queries in the get X from cursor * Restore import * fix: colouring the reaction overlay scrubber * fix: highlight colour, show reaction count if 1 or above * Cleanup * fix: light mode accent * fix: light / dark mode themeing in reactions dialog fragment * Emoji notification blinded id check * fix: show reaction list correctly and pass isUserModerator to bind methods * fix: remove unnecessary places for the moderator * fix: X button for removing own react not showing up properly * feat: add clear all header view * fix: migrate the clear all to the correct location * fix: use display instead of base * Truncate emoji sender ids * feat: add notify thread function in thread db * Notify threads on reaction received * fix: design fixes for the reaction list * fix: emoji reactions bottom sheet dialog UI designs * feat: add unsupported emoji reaction * fix: crash and doing vector properly * Fix reaction database queries * Fix background open group adder job * Show new open group reactions * Fetch a maximum of 5 reactors * Handle open group reactions polling conflicts * Add count to user reaction * Show number of additional reactors * fix: unreads set same as the unread query * fix: design changes * fix: update dependency to improve flexboxlayout behaviour, design consistencies * Add select message icon and update long press menu items order and wording * Fix crash on reactors dialog * fix: colours and backgrounds to match designs * fix: add header in recipient item * fix: margins * fix: alignments and layout issues for emoji reactions view * feat: add overflow previews and logic for overflow * Dim action bar * Add emoji search * Search index fix * Set count for 1:1 and closed group reactions when inserting in local database * Use on screen toolbar to allow overlaying * Show/hide scroll to bottom button * feat: add extended properties so it doesn't collapse on re-bind * Cleanup * feat: prevent keeping extended on rebinding if we get a new message ID * fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds * Display message timestamp * Fix modal items alignment * fix: sort order and emoji count in compareTo * Scale down really large messages to fit * Prevent closed group crash * Fix reaction author Co-authored-by: charles <charles@oxen.io> Co-authored-by: jubb <hjubb@users.noreply.github.com>
This commit is contained in:
@@ -33,7 +33,7 @@ interface MessageDataProvider {
|
||||
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
||||
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
|
||||
fun isMmsOutgoing(mmsMessageId: Long): Boolean
|
||||
fun isOutgoingMessage(mmsId: Long): Boolean
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||
|
@@ -7,9 +7,11 @@ import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
@@ -65,7 +67,7 @@ interface StorageProtocol {
|
||||
fun updateOpenGroup(openGroup: OpenGroup)
|
||||
fun getOpenGroup(threadId: Long): OpenGroup?
|
||||
fun addOpenGroup(urlAsString: String)
|
||||
fun onOpenGroupAdded(urlAsString: String)
|
||||
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?
|
||||
@@ -188,4 +190,8 @@ interface StorageProtocol {
|
||||
fun removeLastOutboxMessageId(server: String)
|
||||
fun getOrCreateBlindedIdMapping(blindedId: String, server: String, serverPublicKey: String, fromOutbox: Boolean = false): BlindedIdMapping
|
||||
|
||||
fun addReaction(reaction: Reaction)
|
||||
fun removeReaction(emoji: String, messageTimestamp: Long, author: String)
|
||||
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
|
||||
fun deleteReactions(messageId: Long, mms: Boolean)
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
@@ -30,31 +31,28 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
|
||||
override fun execute() {
|
||||
try {
|
||||
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||
if (allOpenGroups.contains(joinUrl)) {
|
||||
if (allOpenGroups.contains(openGroup.joinUrl())) {
|
||||
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
||||
delegate?.handleJobFailed(this, DuplicateGroupException())
|
||||
return
|
||||
}
|
||||
// get image
|
||||
val url = HttpUrl.parse(joinUrl) ?: throw Exception("Group joinUrl isn't valid")
|
||||
val server = OpenGroup.getServer(joinUrl)
|
||||
val serverString = server.toString().removeSuffix("/")
|
||||
val publicKey = url.queryParameter("public_key") ?: throw Exception("Group public key isn't valid")
|
||||
val room = url.pathSegments().firstOrNull() ?: throw Exception("Group room isn't valid")
|
||||
storage.setOpenGroupPublicKey(serverString, publicKey)
|
||||
// get info and auth token
|
||||
storage.addOpenGroup(joinUrl)
|
||||
val info = OpenGroupApi.getRoomInfo(room, serverString).get()
|
||||
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())
|
||||
if (imageId != null) {
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(serverString, room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
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())
|
||||
}
|
||||
storage.onOpenGroupAdded(joinUrl)
|
||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||
storage.onOpenGroupAdded(openGroup.server)
|
||||
} catch (e: Exception) {
|
||||
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
||||
delegate?.handleJobFailed(this, e)
|
||||
|
@@ -13,8 +13,10 @@ import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.ParsedMessage
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions
|
||||
import org.session.libsession.messaging.sending_receiving.handleVisibleMessage
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@@ -27,7 +29,8 @@ import org.session.libsignal.utilities.Log
|
||||
data class MessageReceiveParameters(
|
||||
val data: ByteArray,
|
||||
val serverHash: String? = null,
|
||||
val openGroupMessageServerID: Long? = null
|
||||
val openGroupMessageServerID: Long? = null,
|
||||
val reactions: Map<String, OpenGroupApi.Reaction>? = null
|
||||
)
|
||||
|
||||
class BatchMessageReceiveJob(
|
||||
@@ -114,11 +117,14 @@ class BatchMessageReceiveJob(
|
||||
runThreadUpdate = false,
|
||||
runProfileUpdate = true
|
||||
)
|
||||
if (messageId != null) {
|
||||
if (messageId != null && message.reaction == null) {
|
||||
val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
|
||||
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||
messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender)
|
||||
}
|
||||
parameters.openGroupMessageServerID?.let {
|
||||
MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions)
|
||||
}
|
||||
} else {
|
||||
MessageReceiver.handle(message, proto, openGroupID)
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
}
|
||||
|
||||
if (message != null) {
|
||||
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!) && message.reaction == null) return // The message has been deleted
|
||||
val attachmentIDs = mutableListOf<Long>()
|
||||
attachmentIDs.addAll(message.attachmentIDs)
|
||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class Reaction() {
|
||||
var timestamp: Long? = 0
|
||||
var localId: Long? = 0
|
||||
var isMms: Boolean? = false
|
||||
var publicKey: String? = null
|
||||
var emoji: String? = null
|
||||
var react: Boolean? = true
|
||||
var serverId: String? = null
|
||||
var count: Long? = 0
|
||||
var index: Long? = 0
|
||||
var dateSent: Long? = 0
|
||||
var dateReceived: Long? = 0
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (timestamp != null && publicKey != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "Quote"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.DataMessage.Reaction): Reaction? {
|
||||
val react = proto.action == Action.REACT
|
||||
return Reaction(publicKey = proto.author, emoji = proto.emoji, react = react, timestamp = proto.id, count = 1)
|
||||
}
|
||||
|
||||
fun from(timestamp: Long, author: String, emoji: String, react: Boolean): Reaction? {
|
||||
return Reaction(author, emoji, react, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(publicKey: String, emoji: String, react: Boolean, timestamp: Long? = 0, localId: Long? = 0, isMms: Boolean? = false, serverId: String? = null, count: Long? = 0, index: Long? = 0) : this() {
|
||||
this.timestamp = timestamp
|
||||
this.publicKey = publicKey
|
||||
this.emoji = emoji
|
||||
this.react = react
|
||||
this.serverId = serverId
|
||||
this.localId = localId
|
||||
this.isMms = isMms
|
||||
this.count = count
|
||||
this.index = index
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.Reaction? {
|
||||
val timestamp = timestamp
|
||||
val publicKey = publicKey
|
||||
val emoji = emoji
|
||||
val react = react ?: true
|
||||
if (timestamp == null || publicKey == null || emoji == null) {
|
||||
Log.w(TAG, "Couldn't construct reaction proto from: $this")
|
||||
return null
|
||||
}
|
||||
val reactionProto = SignalServiceProtos.DataMessage.Reaction.newBuilder()
|
||||
reactionProto.id = timestamp
|
||||
reactionProto.author = publicKey
|
||||
reactionProto.emoji = emoji
|
||||
reactionProto.action = if (react) Action.REACT else Action.REMOVE
|
||||
// Build
|
||||
return try {
|
||||
reactionProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct reaction proto from: $this")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -23,6 +23,7 @@ class VisibleMessage : Message() {
|
||||
var linkPreview: LinkPreview? = null
|
||||
var profile: Profile? = null
|
||||
var openGroupInvitation: OpenGroupInvitation? = null
|
||||
var reaction: Reaction? = null
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
@@ -31,9 +32,9 @@ class VisibleMessage : Message() {
|
||||
if (!super.isValid()) return false
|
||||
if (attachmentIDs.isNotEmpty()) return true
|
||||
if (openGroupInvitation != null) return true
|
||||
if (reaction != null) return true
|
||||
val text = text?.trim() ?: return false
|
||||
if (text.isNotEmpty()) return true
|
||||
return false
|
||||
return text.isNotEmpty()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -65,6 +66,11 @@ class VisibleMessage : Message() {
|
||||
// TODO Contact
|
||||
val profile = Profile.fromProto(dataMessage)
|
||||
if (profile != null) { result.profile = profile }
|
||||
val reactionProto = if (dataMessage.hasReaction()) dataMessage.reaction else null
|
||||
if (reactionProto != null) {
|
||||
val reaction = Reaction.fromProto(reactionProto)
|
||||
result.reaction = reaction
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -74,10 +80,10 @@ class VisibleMessage : Message() {
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
// Profile
|
||||
val profileProto = profile?.toProto()
|
||||
if (profileProto != null) {
|
||||
dataMessage = profileProto.toBuilder()
|
||||
dataMessage = if (profileProto != null) {
|
||||
profileProto.toBuilder()
|
||||
} else {
|
||||
dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||
SignalServiceProtos.DataMessage.newBuilder()
|
||||
}
|
||||
// Text
|
||||
if (text != null) { dataMessage.body = text }
|
||||
@@ -86,6 +92,11 @@ class VisibleMessage : Message() {
|
||||
if (quoteProto != null) {
|
||||
dataMessage.quote = quoteProto
|
||||
}
|
||||
// Reaction
|
||||
val reactionProto = reaction?.toProto()
|
||||
if (reactionProto != null) {
|
||||
dataMessage.reaction = reactionProto
|
||||
}
|
||||
// Link preview
|
||||
val linkPreviewProto = linkPreview?.toProto()
|
||||
if (linkPreviewProto != null) {
|
||||
@@ -132,12 +143,12 @@ class VisibleMessage : Message() {
|
||||
dataMessage.syncTarget = syncTarget
|
||||
}
|
||||
// Build
|
||||
try {
|
||||
return try {
|
||||
proto.dataMessage = dataMessage.build()
|
||||
return proto.build()
|
||||
proto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -151,6 +162,6 @@ class VisibleMessage : Message() {
|
||||
}
|
||||
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || reaction != null
|
||||
}
|
||||
}
|
@@ -33,6 +33,15 @@ sealed class Endpoint(val value: String) {
|
||||
data class RoomDeleteMessages(val roomToken: String, val sessionId: String) :
|
||||
Endpoint("room/$roomToken/all/$sessionId")
|
||||
|
||||
data class Reactors(val roomToken: String, val messageId: Long, val emoji: String):
|
||||
Endpoint("room/$roomToken/reactors/$messageId/$emoji")
|
||||
|
||||
data class Reaction(val roomToken: String, val messageId: Long, val emoji: String):
|
||||
Endpoint("room/$roomToken/reaction/$messageId/$emoji")
|
||||
|
||||
data class ReactionDelete(val roomToken: String, val messageId: Long, val emoji: String):
|
||||
Endpoint("room/$roomToken/reactions/$messageId/$emoji")
|
||||
|
||||
// Pinning
|
||||
|
||||
data class RoomPinMessage(val roomToken: String, val messageId: Long) :
|
||||
|
@@ -7,5 +7,5 @@ data class GroupMember(
|
||||
)
|
||||
|
||||
enum class GroupMemberRole {
|
||||
STANDARD, ZOOMBIE, MODERATOR, ADMIN
|
||||
STANDARD, ZOOMBIE, MODERATOR, ADMIN, HIDDEN_MODERATOR, HIDDEN_ADMIN
|
||||
}
|
||||
|
@@ -55,9 +55,16 @@ object OpenGroupApi {
|
||||
now - lastOpenDate
|
||||
}
|
||||
|
||||
const val defaultServerPublicKey =
|
||||
"a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val defaultServer = "http://116.203.70.33"
|
||||
const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val legacyServerIP = "116.203.70.33"
|
||||
const val legacyDefaultServer = "http://116.203.70.33" // TODO: migrate all references to use new value
|
||||
|
||||
/** For migration purposes only, don't use this value in joining groups */
|
||||
const val httpDefaultServer = "http://open.getsession.org"
|
||||
|
||||
const val defaultServer = "https://open.getsession.org"
|
||||
|
||||
val pendingReactions = mutableListOf<PendingReaction>()
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object Generic : Error("An error occurred.")
|
||||
@@ -114,6 +121,7 @@ object OpenGroupApi {
|
||||
data class BatchRequestInfo<T>(
|
||||
val request: BatchRequest,
|
||||
val endpoint: Endpoint,
|
||||
val queryParameters: Map<String, String> = mapOf(),
|
||||
val responseType: TypeReference<T>
|
||||
)
|
||||
|
||||
@@ -139,6 +147,10 @@ object OpenGroupApi {
|
||||
val missing: List<String> = emptyList()
|
||||
)
|
||||
|
||||
enum class Capability {
|
||||
BLIND, REACTIONS
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
data class RoomPollInfo(
|
||||
val token: String = "",
|
||||
@@ -179,7 +191,39 @@ object OpenGroupApi {
|
||||
val whisperMods: String = "",
|
||||
val whisperTo: String = "",
|
||||
val data: String? = null,
|
||||
val signature: String? = null
|
||||
val signature: String? = null,
|
||||
val reactions: Map<String, Reaction>? = null,
|
||||
)
|
||||
|
||||
data class Reaction(
|
||||
val count: Long = 0,
|
||||
val reactors: List<String> = emptyList(),
|
||||
val you: Boolean = false,
|
||||
val index: Long = 0
|
||||
)
|
||||
|
||||
data class AddReactionResponse(
|
||||
val seqNo: Long,
|
||||
val added: Boolean
|
||||
)
|
||||
|
||||
data class DeleteReactionResponse(
|
||||
val seqNo: Long,
|
||||
val removed: Boolean
|
||||
)
|
||||
|
||||
data class DeleteAllReactionsResponse(
|
||||
val seqNo: Long,
|
||||
val removed: Boolean
|
||||
)
|
||||
|
||||
data class PendingReaction(
|
||||
val server: String,
|
||||
val room: String,
|
||||
val messageId: Long,
|
||||
val emoji: String,
|
||||
val add: Boolean,
|
||||
var seqNo: Long? = null
|
||||
)
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
@@ -240,15 +284,12 @@ object OpenGroupApi {
|
||||
}
|
||||
|
||||
private fun send(request: Request): Promise<OnionResponse, Exception> {
|
||||
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
.port(url.port())
|
||||
.addPathSegments(request.endpoint.value)
|
||||
if (request.verb == GET) {
|
||||
HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||
val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}")
|
||||
if (request.verb == GET && request.queryParameters.isNotEmpty()) {
|
||||
urlBuilder.append("?")
|
||||
for ((key, value) in request.queryParameters) {
|
||||
urlBuilder.addQueryParameter(key, value)
|
||||
urlBuilder.append("$key=$value")
|
||||
}
|
||||
}
|
||||
fun execute(): Promise<OnionResponse, Exception> {
|
||||
@@ -258,7 +299,7 @@ object OpenGroupApi {
|
||||
?: return Promise.ofFail(Error.NoPublicKey)
|
||||
val ed25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
|
||||
?: return Promise.ofFail(Error.NoEd25519KeyPair)
|
||||
val urlRequest = urlBuilder.build()
|
||||
val urlRequest = urlBuilder.toString()
|
||||
val headers = request.headers.toMutableMap()
|
||||
if (request.isAuthRequired) {
|
||||
val nonce = sodium.nonce(16)
|
||||
@@ -294,9 +335,9 @@ object OpenGroupApi {
|
||||
.plus(nonce)
|
||||
.plus("$timestamp".toByteArray(Charsets.US_ASCII))
|
||||
.plus(request.verb.rawValue.toByteArray())
|
||||
.plus(urlRequest.encodedPath().toByteArray())
|
||||
.plus("/${request.endpoint.value}".toByteArray())
|
||||
.plus(bodyHash)
|
||||
if (serverCapabilities.contains("blind")) {
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
|
||||
pubKey = SessionId(
|
||||
IdPrefix.BLINDED,
|
||||
@@ -404,12 +445,19 @@ object OpenGroupApi {
|
||||
fileIds: List<String>? = null
|
||||
): Promise<OpenGroupMessage, Exception> {
|
||||
val signedMessage = message.sign(room, server, fallbackSigningType = IdPrefix.STANDARD) ?: return Promise.ofFail(Error.SigningFailed)
|
||||
val parameters = signedMessage.toJSON().toMutableMap()
|
||||
|
||||
// add file IDs if there are any (from attachments)
|
||||
if (!fileIds.isNullOrEmpty()) {
|
||||
parameters += "files" to fileIds
|
||||
}
|
||||
|
||||
val request = Request(
|
||||
verb = POST,
|
||||
room = room,
|
||||
server = server,
|
||||
endpoint = Endpoint.RoomMessage(room),
|
||||
parameters = signedMessage.toJSON()
|
||||
parameters = parameters
|
||||
)
|
||||
return getResponseBodyJson(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessage = json as? Map<String, Any>
|
||||
@@ -470,6 +518,63 @@ object OpenGroupApi {
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
fun getReactors(room: String, server: String, messageId: Long, emoji: String): Promise<Map<*, *>, Exception> {
|
||||
val request = Request(
|
||||
verb = GET,
|
||||
room = room,
|
||||
server = server,
|
||||
endpoint = Endpoint.Reactors(room, messageId, emoji)
|
||||
)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, Map::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun addReaction(room: String, server: String, messageId: Long, emoji: String): Promise<AddReactionResponse, Exception> {
|
||||
val request = Request(
|
||||
verb = PUT,
|
||||
room = room,
|
||||
server = server,
|
||||
endpoint = Endpoint.Reaction(room, messageId, emoji),
|
||||
parameters = emptyMap<String, String>()
|
||||
)
|
||||
val pendingReaction = PendingReaction(server, room, messageId, emoji, true)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, AddReactionResponse::class.java).also {
|
||||
val index = pendingReactions.indexOf(pendingReaction)
|
||||
pendingReactions[index].seqNo = it.seqNo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): Promise<DeleteReactionResponse, Exception> {
|
||||
val request = Request(
|
||||
verb = DELETE,
|
||||
room = room,
|
||||
server = server,
|
||||
endpoint = Endpoint.Reaction(room, messageId, emoji)
|
||||
)
|
||||
val pendingReaction = PendingReaction(server, room, messageId, emoji, true)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, DeleteReactionResponse::class.java).also {
|
||||
val index = pendingReactions.indexOf(pendingReaction)
|
||||
pendingReactions[index].seqNo = it.seqNo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): Promise<DeleteAllReactionsResponse, Exception> {
|
||||
val request = Request(
|
||||
verb = DELETE,
|
||||
room = room,
|
||||
server = server,
|
||||
endpoint = Endpoint.ReactionDelete(room, messageId, emoji)
|
||||
)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Message Deletion
|
||||
@@ -608,7 +713,7 @@ object OpenGroupApi {
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(
|
||||
method = GET,
|
||||
path = "/room/$room/messages/recent"
|
||||
path = "/room/$room/messages/recent?t=r&reactors=5"
|
||||
),
|
||||
endpoint = Endpoint.RoomMessagesRecent(room),
|
||||
responseType = object : TypeReference<List<Message>>(){}
|
||||
@@ -617,7 +722,7 @@ object OpenGroupApi {
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(
|
||||
method = GET,
|
||||
path = "/room/$room/messages/since/$lastMessageServerId"
|
||||
path = "/room/$room/messages/since/$lastMessageServerId?t=r&reactors=5"
|
||||
),
|
||||
endpoint = Endpoint.RoomMessagesSince(room, lastMessageServerId),
|
||||
responseType = object : TypeReference<List<Message>>(){}
|
||||
@@ -626,7 +731,7 @@ object OpenGroupApi {
|
||||
)
|
||||
}
|
||||
val serverCapabilities = storage.getServerCapabilities(server)
|
||||
if (serverCapabilities.contains("blind")) {
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
requests.add(
|
||||
if (lastInboxMessageId == null) {
|
||||
BatchRequestInfo(
|
||||
@@ -689,14 +794,16 @@ object OpenGroupApi {
|
||||
|
||||
private fun sequentialBatch(
|
||||
server: String,
|
||||
requests: MutableList<BatchRequestInfo<*>>
|
||||
requests: MutableList<BatchRequestInfo<*>>,
|
||||
authRequired: Boolean = true
|
||||
): Promise<List<BatchResponse<*>>, Exception> {
|
||||
val request = Request(
|
||||
verb = POST,
|
||||
room = null,
|
||||
server = server,
|
||||
endpoint = Endpoint.Sequence,
|
||||
parameters = requests.map { it.request }
|
||||
parameters = requests.map { it.request },
|
||||
isAuthRequired = authRequired
|
||||
)
|
||||
return getBatchResponseJson(request, requests)
|
||||
}
|
||||
@@ -803,7 +910,11 @@ object OpenGroupApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCapabilitiesAndRoomInfo(room: String, server: String): Promise<Pair<Capabilities, RoomInfo>, Exception> {
|
||||
fun getCapabilitiesAndRoomInfo(
|
||||
room: String,
|
||||
server: String,
|
||||
authRequired: Boolean = true
|
||||
): Promise<Pair<Capabilities, RoomInfo>, Exception> {
|
||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(
|
||||
@@ -822,7 +933,7 @@ object OpenGroupApi {
|
||||
responseType = object : TypeReference<RoomInfo>(){}
|
||||
)
|
||||
)
|
||||
return sequentialBatch(server, requests).map {
|
||||
return sequentialBatch(server, requests, authRequired).map {
|
||||
val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed
|
||||
val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed
|
||||
capabilities to roomInfo
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
@@ -19,12 +20,13 @@ data class OpenGroupMessage(
|
||||
/**
|
||||
* The serialized protobuf in base64 encoding.
|
||||
*/
|
||||
val base64EncodedData: String,
|
||||
val base64EncodedData: String?,
|
||||
/**
|
||||
* When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||
* a receiving user can verify that the message wasn't tampered with.
|
||||
*/
|
||||
val base64EncodedSignature: String? = null
|
||||
val base64EncodedSignature: String? = null,
|
||||
val reactions: Map<String, OpenGroupApi.Reaction>? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -47,12 +49,12 @@ data class OpenGroupMessage(
|
||||
}
|
||||
|
||||
fun sign(room: String, server: String, fallbackSigningType: IdPrefix): OpenGroupMessage? {
|
||||
if (base64EncodedData.isEmpty()) return null
|
||||
if (base64EncodedData.isNullOrEmpty()) return null
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(room, server) ?: return null
|
||||
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(server)
|
||||
val signature = when {
|
||||
serverCapabilities.contains("blind") -> {
|
||||
serverCapabilities.contains(Capability.BLIND.name.lowercase()) -> {
|
||||
val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.publicKey, userEdKeyPair) ?: return null
|
||||
SodiumUtilities.sogsSignature(
|
||||
decode(base64EncodedData),
|
||||
@@ -78,7 +80,7 @@ data class OpenGroupMessage(
|
||||
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||
}
|
||||
|
||||
fun toJSON(): Map<String, Any> {
|
||||
fun toJSON(): Map<String, Any?> {
|
||||
val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
|
||||
serverID?.let { json["server_id"] = it }
|
||||
sender?.let { json["public_key"] = it }
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
fun String.migrateLegacyServerUrl() = if (contains(OpenGroupApi.legacyServerIP)) {
|
||||
OpenGroupApi.defaultServer
|
||||
} else if (contains(OpenGroupApi.httpDefaultServer)) {
|
||||
OpenGroupApi.defaultServer
|
||||
} else {
|
||||
this
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@@ -19,6 +18,7 @@ import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@@ -243,7 +243,7 @@ object MessageSender {
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
val messageSender = if (serverCapabilities.contains("blind") && blindedPublicKey != null) {
|
||||
val messageSender = if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && blindedPublicKey != null) {
|
||||
SessionId(IdPrefix.BLINDED, blindedPublicKey!!).hexString
|
||||
} else {
|
||||
SessionId(IdPrefix.UN_BLINDED, userEdKeyPair.publicKey.asBytes).hexString
|
||||
@@ -338,8 +338,23 @@ object MessageSender {
|
||||
storage.setMessageServerHash(messageID, it)
|
||||
}
|
||||
// Track the open group server message ID
|
||||
if (message.openGroupServerMessageID != null && destination is Destination.LegacyOpenGroup) {
|
||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.roomToken}".toByteArray())
|
||||
if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
|
||||
val server: String
|
||||
val room: String
|
||||
when (destination) {
|
||||
is Destination.LegacyOpenGroup -> {
|
||||
server = destination.server
|
||||
room = destination.roomToken
|
||||
}
|
||||
is Destination.OpenGroup -> {
|
||||
server = destination.server
|
||||
room = destination.roomToken
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Destination was a different destination than we were expecting")
|
||||
}
|
||||
}
|
||||
val encoded = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
||||
if (threadID != null && threadID >= 0) {
|
||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
@@ -352,6 +367,8 @@ object MessageSender {
|
||||
if (message is VisibleMessage && !isSyncMessage) {
|
||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, userPublicKey)
|
||||
}
|
||||
} ?: run {
|
||||
storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp)
|
||||
}
|
||||
// Sync the message if:
|
||||
// • it's a visible message
|
||||
@@ -432,4 +449,5 @@ object MessageSender {
|
||||
fun explicitLeave(groupPublicKey: String, notifyUser: Boolean): Promise<Unit, Exception> {
|
||||
return leave(groupPublicKey, notifyUser)
|
||||
}
|
||||
|
||||
}
|
@@ -16,7 +16,9 @@ import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
@@ -47,6 +49,7 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import java.security.MessageDigest
|
||||
import java.util.LinkedList
|
||||
import kotlin.math.min
|
||||
|
||||
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
@@ -157,7 +160,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
}
|
||||
}
|
||||
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
for (openGroup in message.openGroups.map {
|
||||
it.replace(OpenGroupApi.legacyDefaultServer, OpenGroupApi.defaultServer)
|
||||
.replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer)
|
||||
}) {
|
||||
if (allV2OpenGroups.contains(openGroup)) continue
|
||||
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
||||
if (!storage.hasBackgroundGroupAddJob(openGroup)) {
|
||||
@@ -256,11 +262,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
val author = Address.fromSerialized(quote.author)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
if (messageInfo != null) {
|
||||
quoteModel = if (messageInfo != null) {
|
||||
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
quoteModel = QuoteModel(quote.id, author,null,false, attachments)
|
||||
QuoteModel(quote.id, author,null,false, attachments)
|
||||
} else {
|
||||
quoteModel = QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
||||
QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
||||
}
|
||||
}
|
||||
// Parse link preview if needed
|
||||
@@ -288,22 +294,104 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
return@mapNotNull attachment
|
||||
}
|
||||
}
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID = storage.persist(
|
||||
message, quoteModel, linkPreviews,
|
||||
message.groupPublicKey, openGroupID,
|
||||
attachments, runIncrement, runThreadUpdate
|
||||
) ?: return null
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
// Parse reaction if needed
|
||||
message.reaction?.let { reaction ->
|
||||
if (reaction.react == true) {
|
||||
reaction.serverId = message.openGroupServerMessageID?.toString() ?: message.serverHash.orEmpty()
|
||||
reaction.dateSent = message.sentTimestamp ?: 0
|
||||
reaction.dateReceived = message.receivedTimestamp ?: 0
|
||||
storage.addReaction(reaction)
|
||||
} else {
|
||||
storage.removeReaction(reaction.emoji!!, reaction.timestamp!!, reaction.publicKey!!)
|
||||
}
|
||||
} ?: run {
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID =
|
||||
storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID,
|
||||
attachments, runIncrement, runThreadUpdate
|
||||
) ?: return null
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
}
|
||||
return messageID
|
||||
}
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
return messageID
|
||||
return null
|
||||
}
|
||||
|
||||
fun MessageReceiver.handleOpenGroupReactions(
|
||||
threadId: Long,
|
||||
openGroupMessageServerID: Long,
|
||||
reactions: Map<String, OpenGroupApi.Reaction>?
|
||||
) {
|
||||
if (reactions.isNullOrEmpty()) return
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val (messageId, isSms) = MessagingModuleConfiguration.shared.messageDataProvider.getMessageID(openGroupMessageServerID, threadId) ?: return
|
||||
storage.deleteReactions(messageId, !isSms)
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val openGroup = storage.getOpenGroup(threadId)
|
||||
val blindedPublicKey = openGroup?.publicKey?.let { serverPublicKey ->
|
||||
SodiumUtilities.blindedKeyPair(serverPublicKey, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!)
|
||||
?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||
}
|
||||
for ((emoji, reaction) in reactions) {
|
||||
val pendingUserReaction = OpenGroupApi.pendingReactions
|
||||
.filter { it.server == openGroup?.server && it.room == openGroup.room && it.messageId == openGroupMessageServerID && it.add }
|
||||
.sortedByDescending { it.seqNo }
|
||||
.any { it.emoji == emoji }
|
||||
val shouldAddUserReaction = pendingUserReaction || reaction.you || reaction.reactors.contains(userPublicKey)
|
||||
val reactorIds = reaction.reactors.filter { it != blindedPublicKey && it != userPublicKey }
|
||||
val count = if (reaction.you) reaction.count - 1 else reaction.count
|
||||
// Add the first reaction (with the count)
|
||||
reactorIds.firstOrNull()?.let {
|
||||
storage.addReaction(Reaction(
|
||||
localId = messageId,
|
||||
isMms = !isSms,
|
||||
publicKey = it,
|
||||
emoji = emoji,
|
||||
react = true,
|
||||
serverId = "$openGroupMessageServerID",
|
||||
count = count,
|
||||
index = reaction.index
|
||||
))
|
||||
}
|
||||
|
||||
// Add all other reactions
|
||||
val maxAllowed = if (shouldAddUserReaction) 4 else 5
|
||||
val lastIndex = min(maxAllowed, reactorIds.size)
|
||||
reactorIds.slice(1 until lastIndex).map { reactor ->
|
||||
storage.addReaction(Reaction(
|
||||
localId = messageId,
|
||||
isMms = !isSms,
|
||||
publicKey = reactor,
|
||||
emoji = emoji,
|
||||
react = true,
|
||||
serverId = "$openGroupMessageServerID",
|
||||
count = 0, // Only want this on the first reaction
|
||||
index = reaction.index
|
||||
))
|
||||
}
|
||||
|
||||
// Add the current user reaction (if applicable and not already included)
|
||||
if (shouldAddUserReaction) {
|
||||
storage.addReaction(Reaction(
|
||||
localId = messageId,
|
||||
isMms = !isSms,
|
||||
publicKey = userPublicKey,
|
||||
emoji = emoji,
|
||||
react = true,
|
||||
serverId = "$openGroupMessageServerID",
|
||||
count = 1,
|
||||
index = reaction.index
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
// region Closed Groups
|
||||
|
@@ -22,6 +22,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
@@ -136,10 +137,16 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
pollInfo.details?.moderators?.forEach {
|
||||
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.MODERATOR))
|
||||
}
|
||||
pollInfo.details?.hiddenModerators?.forEach {
|
||||
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR))
|
||||
}
|
||||
// - Admins
|
||||
pollInfo.details?.admins?.forEach {
|
||||
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.ADMIN))
|
||||
}
|
||||
pollInfo.details?.hiddenAdmins?.forEach {
|
||||
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessages(
|
||||
@@ -147,22 +154,23 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
roomToken: String,
|
||||
messages: List<OpenGroupApi.Message>
|
||||
) {
|
||||
val openGroupId = "$server.$roomToken"
|
||||
val sortedMessages = messages.sortedBy { it.seqno }
|
||||
sortedMessages.maxOfOrNull { it.seqno }?.let {
|
||||
MessagingModuleConfiguration.shared.storage.setLastMessageServerID(roomToken, server, it)
|
||||
sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo ->
|
||||
MessagingModuleConfiguration.shared.storage.setLastMessageServerID(roomToken, server, seqNo)
|
||||
OpenGroupApi.pendingReactions.removeAll { !(it.seqNo == null || it.seqNo!! > seqNo) }
|
||||
}
|
||||
val (deletions, additions) = sortedMessages.partition { it.deleted || it.data.isNullOrBlank() }
|
||||
handleNewMessages(openGroupId, additions.map {
|
||||
val (deletions, additions) = sortedMessages.partition { it.deleted }
|
||||
handleNewMessages(server, roomToken, additions.map {
|
||||
OpenGroupMessage(
|
||||
serverID = it.id,
|
||||
sender = it.sessionId,
|
||||
sentTimestamp = (it.posted * 1000).toLong(),
|
||||
base64EncodedData = it.data!!,
|
||||
base64EncodedSignature = it.signature
|
||||
base64EncodedData = it.data,
|
||||
base64EncodedSignature = it.signature,
|
||||
reactions = it.reactions
|
||||
)
|
||||
})
|
||||
handleDeletedMessages(openGroupId, deletions.map { it.id })
|
||||
handleDeletedMessages(server, roomToken, deletions.map { it.id })
|
||||
}
|
||||
|
||||
private fun handleDirectMessages(
|
||||
@@ -219,43 +227,52 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessages(openGroupID: String, messages: List<OpenGroupMessage>) {
|
||||
private fun handleNewMessages(server: String, roomToken: String, messages: List<OpenGroupMessage>) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val openGroupID = "$server.$roomToken"
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
// check thread still exists
|
||||
val threadId = storage.getThreadId(Address.fromSerialized(groupID)) ?: -1
|
||||
val threadExists = threadId >= 0
|
||||
if (!hasStarted || !threadExists) { return }
|
||||
val envelopes = messages.sortedBy { it.serverID!! }.map { message ->
|
||||
val senderPublicKey = message.sender!!
|
||||
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.content = message.toProto().toByteString()
|
||||
builder.timestamp = message.sentTimestamp
|
||||
builder.build() to message.serverID
|
||||
val envelopes = mutableListOf<Triple<Long?, SignalServiceProtos.Envelope, Map<String, OpenGroupApi.Reaction>?>>()
|
||||
messages.sortedBy { it.serverID!! }.forEach { message ->
|
||||
if (!message.base64EncodedData.isNullOrEmpty()) {
|
||||
val envelope = SignalServiceProtos.Envelope.newBuilder()
|
||||
.setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE)
|
||||
.setSource(message.sender!!)
|
||||
.setSourceDevice(1)
|
||||
.setContent(message.toProto().toByteString())
|
||||
.setTimestamp(message.sentTimestamp)
|
||||
.build()
|
||||
envelopes.add(Triple( message.serverID, envelope, message.reactions))
|
||||
} else if (!message.reactions.isNullOrEmpty()) {
|
||||
message.serverID?.let {
|
||||
MessageReceiver.handleOpenGroupReactions(threadId, it, message.reactions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list ->
|
||||
val parameters = list.map { (message, serverId) ->
|
||||
MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId)
|
||||
val parameters = list.map { (serverId, message, reactions) ->
|
||||
MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions)
|
||||
}
|
||||
JobQueue.shared.add(BatchMessageReceiveJob(parameters, openGroupID))
|
||||
}
|
||||
|
||||
if (envelopes.isNotEmpty()) {
|
||||
JobQueue.shared.add(TrimThreadJob(threadId,openGroupID))
|
||||
JobQueue.shared.add(TrimThreadJob(threadId, openGroupID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeletedMessages(openGroupID: String, serverIds: List<Long>) {
|
||||
private fun handleDeletedMessages(server: String, roomToken: String, serverIds: List<Long>) {
|
||||
val openGroupId = "$server.$roomToken"
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupId.toByteArray())
|
||||
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
|
||||
|
||||
if (serverIds.isNotEmpty()) {
|
||||
val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupID)
|
||||
val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupId)
|
||||
JobQueue.shared.add(deleteJob)
|
||||
}
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@ object SodiumUtilities {
|
||||
}
|
||||
|
||||
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */
|
||||
@JvmStatic
|
||||
fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
|
||||
if (edKeyPair.publicKey.asBytes.size != PUBLIC_KEY_LENGTH || edKeyPair.secretKey.asBytes.size != SECRET_KEY_LENGTH) return null
|
||||
val kBytes = generateBlindingFactor(serverPublicKey) ?: return null
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.open_groups.migrateLegacyServerUrl
|
||||
|
||||
object OpenGroupUrlParser {
|
||||
|
||||
@@ -20,7 +21,7 @@ object OpenGroupUrlParser {
|
||||
// If the URL is malformed, throw an exception
|
||||
val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL
|
||||
// Parse components
|
||||
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix)
|
||||
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix).migrateLegacyServerUrl()
|
||||
val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
|
||||
val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey
|
||||
if (publicKey.length != 64) throw Error.InvalidPublicKey
|
||||
@@ -33,4 +34,6 @@ object OpenGroupUrlParser {
|
||||
}
|
||||
}
|
||||
|
||||
class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String)
|
||||
class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String) {
|
||||
fun joinUrl() = "$server/$room?public_key=$serverPublicKey"
|
||||
}
|
@@ -3,6 +3,7 @@ package org.session.libsession.utilities;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@@ -10,7 +11,9 @@ import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
@@ -24,6 +27,17 @@ public class ThemeUtil {
|
||||
return getAttributeText(context, R.attr.theme_type, "light").equals("dark");
|
||||
}
|
||||
|
||||
public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
|
||||
if (theme.resolveAttribute(attr, typedValue, true)) {
|
||||
return typedValue.data != 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getThemedColor(@NonNull Context context, @AttrRes int attr) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
@@ -50,6 +64,18 @@ public class ThemeUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable
|
||||
Drawable getThemedDrawable(@NonNull Context context, @AttrRes int attr) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
|
||||
if (theme.resolveAttribute(attr, typedValue, true)) {
|
||||
return AppCompatResources.getDrawable(context, typedValue.resourceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static LayoutInflater getThemedInflater(@NonNull Context context, @NonNull LayoutInflater inflater, @StyleRes int theme) {
|
||||
Context contextThemeWrapper = new ContextThemeWrapper(context, theme);
|
||||
return inflater.cloneInContext(contextThemeWrapper);
|
||||
|
@@ -69,6 +69,7 @@
|
||||
<attr name="emoji_category_flags" format="reference"/>
|
||||
<attr name="emoji_category_emoticons" format="reference"/>
|
||||
<attr name="emoji_variation_selector_background" format="reference|color" />
|
||||
<attr name="emoji_show_less_icon" format="reference|color" />
|
||||
|
||||
<attr name="quick_camera_icon" format="reference"/>
|
||||
<attr name="quick_mic_icon" format="reference"/>
|
||||
@@ -108,6 +109,7 @@
|
||||
|
||||
<attr name="menu_popup_expand" format="reference"/>
|
||||
<attr name="menu_trash_icon" format="reference" />
|
||||
<attr name="menu_select_icon" format="reference" />
|
||||
<attr name="menu_selectall_icon" format="reference" />
|
||||
<attr name="menu_split_icon" format="reference" />
|
||||
<attr name="menu_accept_icon" format="reference" />
|
||||
@@ -303,4 +305,13 @@
|
||||
<enum name="bottom" value="3" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="KeyboardPageSearchView">
|
||||
<attr name="show_always" format="boolean" />
|
||||
<attr name="search_bar_tint" format="color|reference" />
|
||||
<attr name="search_icon_tint" format="color|reference" />
|
||||
<attr name="search_hint" format="string|reference" />
|
||||
<attr name="click_only" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user