Merge remote-tracking branch 'upstream/dev' into closed_groups

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
#	libsession-util/libsession-util
#	libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
#	libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt
#	libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
#	libsignal/protobuf/SignalService.proto
#	libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java
pull/1403/head
0x330a 2 years ago
commit b6ff1deb64
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C

@ -10,7 +10,6 @@ import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding import network.loki.messenger.databinding.ViewProfilePictureBinding
import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.avatars.ContactColors import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.avatars.ProfileContactPhoto
@ -74,7 +73,7 @@ class ProfilePictureView @JvmOverloads constructor(
additionalDisplayName = getUserDisplayName(apk) additionalDisplayName = getUserDisplayName(apk)
} }
} else if(recipient.isOpenGroupInboxRecipient) { } else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize()) val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
this.publicKey = publicKey this.publicKey = publicKey
displayName = getUserDisplayName(publicKey) displayName = getUserDisplayName(publicKey)
additionalPublicKey = null additionalPublicKey = null

@ -40,6 +40,7 @@ import androidx.annotation.DimenRes
import androidx.core.text.set import androidx.core.text.set
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.drawToBitmap import androidx.core.view.drawToBitmap
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -79,7 +80,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
@ -112,9 +112,9 @@ import org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActi
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
@ -177,6 +177,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -250,11 +251,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) { val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let { storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
fromSerialized(it) fromSerialized(it)
} ?: run { } ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
val openGroupInboxId =
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
}
} else { } else {
it it
} }
@ -263,7 +260,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} ?: finish() } ?: finish()
} }
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver) viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
} }
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var unreadCount = 0 private var unreadCount = 0
@ -322,8 +319,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
handleSwipeToReply(message) handleSwipeToReply(message)
}, },
onItemLongPress = { message, position, view -> onItemLongPress = { message, position, view ->
if (!isMessageRequestThread() && if (!viewModel.isMessageRequestThread &&
(viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities) viewModel.canReactToMessages
) { ) {
showEmojiPicker(message, view) showEmojiPicker(message, view)
} else { } else {
@ -606,26 +603,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate // called from onCreate
private fun setUpInputBar() { private fun setUpInputBar() {
binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true val binding = binding ?: return
binding!!.inputBar.delegate = this binding.inputBar.isGone = viewModel.hidesInputBar()
binding!!.inputBarRecordingView.delegate = this binding.inputBar.delegate = this
binding.inputBarRecordingView.delegate = this
// GIF button // GIF button
binding!!.gifButtonContainer.addView(gifButton) binding.gifButtonContainer.addView(gifButton)
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
gifButton.onUp = { showGIFPicker() } gifButton.onUp = { showGIFPicker() }
gifButton.snIsEnabled = false gifButton.snIsEnabled = false
// Document button // Document button
binding!!.documentButtonContainer.addView(documentButton) binding.documentButtonContainer.addView(documentButton)
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
documentButton.onUp = { showDocumentPicker() } documentButton.onUp = { showDocumentPicker() }
documentButton.snIsEnabled = false documentButton.snIsEnabled = false
// Library button // Library button
binding!!.libraryButtonContainer.addView(libraryButton) binding.libraryButtonContainer.addView(libraryButton)
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
libraryButton.onUp = { pickFromLibrary() } libraryButton.onUp = { pickFromLibrary() }
libraryButton.snIsEnabled = false libraryButton.snIsEnabled = false
// Camera button // Camera button
binding!!.cameraButtonContainer.addView(cameraButton) binding.cameraButtonContainer.addView(cameraButton)
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
cameraButton.onUp = { showCamera() } cameraButton.onUp = { showCamera() }
cameraButton.snIsEnabled = false cameraButton.snIsEnabled = false
@ -774,7 +772,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val recipient = viewModel.recipient ?: return false val recipient = viewModel.recipient ?: return false
if (!isMessageRequestThread()) { if (!viewModel.isMessageRequestThread) {
ConversationMenuHelper.onPrepareOptionsMenu( ConversationMenuHelper.onPrepareOptionsMenu(
menu, menu,
menuInflater, menuInflater,
@ -860,11 +858,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
private fun isMessageRequestThread(): Boolean {
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient && !recipient.isApproved
}
private fun isOutgoingMessageRequestThread(): Boolean { private fun isOutgoingMessageRequestThread(): Boolean {
val recipient = viewModel.recipient ?: return false val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient && return !recipient.isGroupRecipient &&
@ -1079,11 +1072,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun updatePlaceholder() { private fun updatePlaceholder() {
val recipient = viewModel.recipient val recipient = viewModel.recipient
?: return Log.w("Loki", "recipient was null in placeholder update") ?: return Log.w("Loki", "recipient was null in placeholder update")
val blindedRecipient = viewModel.blindedRecipient
val binding = binding ?: return val binding = binding ?: return
val openGroup = viewModel.openGroup val openGroup = viewModel.openGroup
val (textResource, insertParam) = when { val (textResource, insertParam) = when {
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString() openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString() else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
} }
val showPlaceholder = adapter.itemCount == 0 val showPlaceholder = adapter.itemCount == 0

@ -1,10 +1,8 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.content.ContentResolver
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.cash.copper.flow.observeQuery
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -22,7 +20,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
@ -31,7 +28,6 @@ import java.util.UUID
class ConversationViewModel( class ConversationViewModel(
val threadId: Long, val threadId: Long,
val edKeyPair: KeyPair?, val edKeyPair: KeyPair?,
private val contentResolver: ContentResolver,
private val repository: ConversationRepository, private val repository: ConversationRepository,
private val storage: StorageProtocol private val storage: StorageProtocol
) : ViewModel() { ) : ViewModel() {
@ -48,6 +44,15 @@ class ConversationViewModel(
val recipient: Recipient? val recipient: Recipient?
get() = _recipient.value get() = _recipient.value
val blindedRecipient: Recipient?
get() = _recipient.value?.let { recipient ->
when {
recipient.isOpenGroupOutboxRecipient -> recipient
recipient.isOpenGroupInboxRecipient -> repository.maybeGetBlindedRecipient(recipient)
else -> null
}
}
private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce { private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce {
storage.getOpenGroup(threadId) storage.getOpenGroup(threadId)
} }
@ -63,12 +68,22 @@ class ConversationViewModel(
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
} }
val isMessageRequestThread : Boolean
get() {
val recipient = recipient ?: return false
return !recipient.isLocalNumber && !recipient.isGroupRecipient && !recipient.isApproved
}
val canReactToMessages: Boolean
// allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions
get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities)
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)) repository.recipientUpdateFlow(threadId)
.collect { .collect { recipient ->
val recipientExists = storage.getRecipientForThread(threadId) != null if (recipient == null && _uiState.value.conversationExists) {
if (!recipientExists && _uiState.value.conversationExists) {
_uiState.update { it.copy(conversationExists = false) } _uiState.update { it.copy(conversationExists = false) }
} }
} }
@ -200,22 +215,25 @@ class ConversationViewModel(
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
} }
fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
blindedRecipient?.blocksCommunityMessageRequests == true
@dagger.assisted.AssistedFactory @dagger.assisted.AssistedFactory
interface AssistedFactory { interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory fun create(threadId: Long, edKeyPair: KeyPair?): Factory
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor( class Factory @AssistedInject constructor(
@Assisted private val threadId: Long, @Assisted private val threadId: Long,
@Assisted private val edKeyPair: KeyPair?, @Assisted private val edKeyPair: KeyPair?,
@Assisted private val contentResolver: ContentResolver,
private val repository: ConversationRepository, private val repository: ConversationRepository,
private val storage: StorageProtocol private val storage: StorageProtocol
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T return ConversationViewModel(threadId, edKeyPair, repository, storage) as T
} }
} }
} }

@ -63,14 +63,15 @@ public class RecipientDatabase extends Database {
private static final String FORCE_SMS_SELECTION = "force_sms_selection"; private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
private static final String WRAPPER_HASH = "wrapper_hash"; private static final String WRAPPER_HASH = "wrapper_hash";
private static final String BLOCKS_COMMUNITY_MESSAGE_REQUESTS = "blocks_community_message_requests";
private static final String AUTO_DOWNLOAD = "auto_download"; // 1 / 0 / -1 flag for whether to auto-download in a conversation, or if the user hasn't selected a preference private static final String AUTO_DOWNLOAD = "auto_download"; // 1 / 0 / -1 flag for whether to auto-download in a conversation, or if the user hasn't selected a preference
private static final String[] RECIPIENT_PROJECTION = new String[] { private static final String[] RECIPIENT_PROJECTION = new String[] {
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI, PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE, UNIDENTIFIED_ACCESS_MODE, FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH,
FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH, AUTO_DOWNLOAD, BLOCKS_COMMUNITY_MESSAGE_REQUESTS, AUTO_DOWNLOAD,
}; };
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@ -154,6 +155,11 @@ public class RecipientDatabase extends Database {
"ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;"; "ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
} }
public static String getAddBlocksCommunityMessageRequests() {
return "ALTER TABLE "+TABLE_NAME+" "+
"ADD COLUMN "+BLOCKS_COMMUNITY_MESSAGE_REQUESTS+" INT DEFAULT 0;";
}
public static final int NOTIFY_TYPE_ALL = 0; public static final int NOTIFY_TYPE_ALL = 0;
public static final int NOTIFY_TYPE_MENTIONS = 1; public static final int NOTIFY_TYPE_MENTIONS = 1;
public static final int NOTIFY_TYPE_NONE = 2; public static final int NOTIFY_TYPE_NONE = 2;
@ -210,6 +216,7 @@ public class RecipientDatabase extends Database {
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1; boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH)); String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1;
MaterialColor color; MaterialColor color;
byte[] profileKey = null; byte[] profileKey = null;
@ -241,7 +248,7 @@ public class RecipientDatabase extends Database {
systemPhoneLabel, systemContactUri, systemPhoneLabel, systemContactUri,
signalProfileName, signalProfileAvatar, profileSharing, signalProfileName, signalProfileAvatar, profileSharing,
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode), notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection, wrapperHash)); forceSmsSelection, wrapperHash, blocksCommunityMessageRequests));
} }
public boolean isAutoDownloadFlagSet(Recipient recipient) { public boolean isAutoDownloadFlagSet(Recipient recipient) {
@ -439,6 +446,14 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners(); notifyRecipientListeners();
} }
public void setBlocksCommunityMessageRequests(@NonNull Recipient recipient, boolean isBlocked) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, isBlocked ? 1 : 0);
updateOrInsert(recipient.getAddress(), contentValues);
recipient.resolve().setBlocksCommunityMessageRequests(isBlocked);
notifyRecipientListeners();
}
private void updateOrInsert(Address address, ContentValues contentValues) { private void updateOrInsert(Address address, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();

@ -197,6 +197,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
db.setProfileKey(recipient, newProfileKey) db.setProfileKey(recipient, newProfileKey)
} }
override fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) {
val db = DatabaseComponent.get(context).recipientDatabase()
db.setBlocksCommunityMessageRequests(recipient, blocksMessageRequests)
}
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) { override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
val ourRecipient = fromSerialized(getUserPublicKey()!!).let { val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false) Recipient.from(context, it, false)
@ -438,6 +443,10 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
return configFactory.canPerformChange(variant, publicKey, changeTimestampMs) return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
} }
override fun isCheckingCommunityRequests(): Boolean {
return configFactory.user?.getCommunityMessageRequests() == true
}
fun notifyUpdates(forConfigObject: ConfigBase) { fun notifyUpdates(forConfigObject: ConfigBase) {
when (forConfigObject) { when (forConfigObject) {
is UserProfile -> updateUser(forConfigObject) is UserProfile -> updateUser(forConfigObject)
@ -1459,7 +1468,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
val blindedId = when { val blindedId = when {
recipient.isGroupRecipient -> null recipient.isGroupRecipient -> null
recipient.isOpenGroupInboxRecipient -> { recipient.isOpenGroupInboxRecipient -> {
GroupUtil.getDecodedOpenGroupInbox(address) GroupUtil.getDecodedOpenGroupInboxSessionId(address)
} }
else -> { else -> {
if (SessionId(address).prefix == IdPrefix.BLINDED) { if (SessionId(address).prefix == IdPrefix.BLINDED) {
@ -1578,16 +1587,12 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
if (mapping.sessionId != null) { if (mapping.sessionId != null) {
return mapping return mapping
} }
val threadDb = DatabaseComponent.get(context).threadDatabase() getAllContacts().forEach { contact ->
threadDb.readerFor(threadDb.conversationList).use { reader -> val sessionId = SessionId(contact.sessionID)
while (reader.next != null) { if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) {
val recipient = reader.current.recipient val contactMapping = mapping.copy(sessionId = sessionId.hexString)
val sessionId = recipient.address.serialize() db.addBlindedIdMapping(contactMapping)
if (!recipient.isGroupRecipient && SodiumUtilities.sessionId(sessionId, blindedId, serverPublicKey)) { return contactMapping
val contactMapping = mapping.copy(sessionId = sessionId)
db.addBlindedIdMapping(contactMapping)
return contactMapping
}
} }
} }
db.getBlindedIdMappingsExceptFor(server).forEach { db.getBlindedIdMappingsExceptFor(server).forEach {

@ -89,9 +89,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV41 = 62; private static final int lokiV41 = 62;
private static final int lokiV42 = 63; private static final int lokiV42 = 63;
private static final int lokiV43 = 64; private static final int lokiV43 = 64;
private static final int lokiV44 = 65;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV43; private static final int DATABASE_VERSION = lokiV44;
private static final int MIN_DATABASE_VERSION = lokiV7; private static final int MIN_DATABASE_VERSION = lokiV7;
private static final String CIPHER3_DATABASE_NAME = "signal.db"; private static final String CIPHER3_DATABASE_NAME = "signal.db";
public static final String DATABASE_NAME = "signal_v4.db"; public static final String DATABASE_NAME = "signal_v4.db";
@ -357,6 +358,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
db.execSQL(RecipientDatabase.getAddWrapperHash()); db.execSQL(RecipientDatabase.getAddWrapperHash());
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand()); db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand());
db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand()); db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand());
@ -603,6 +605,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
} }
if (oldVersion < lokiV43) { if (oldVersion < lokiV43) {
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
}
if (oldVersion < lokiV44) {
db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand()); db.execSQL(RecipientDatabase.getCreateAutoDownloadCommand());
db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand()); db.execSQL(RecipientDatabase.getUpdateAutoDownloadValuesCommand());
} }

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.dependencies
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object ContentModule {
@Provides
fun providesContentResolver(@ApplicationContext context: Context) =context.contentResolver
}

@ -72,8 +72,8 @@ import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
@ -299,12 +299,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
EventBus.getDefault().register(this@HomeActivity) EventBus.getDefault().register(this@HomeActivity)
if (intent.hasExtra(FROM_ONBOARDING) if (intent.hasExtra(FROM_ONBOARDING)
&& intent.getBooleanExtra(FROM_ONBOARDING, false) && intent.getBooleanExtra(FROM_ONBOARDING, false)) {
&& !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled() if ((getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
) { Permissions.with(this)
Permissions.with(this) .request(Manifest.permission.POST_NOTIFICATIONS)
.request(Manifest.permission.POST_NOTIFICATIONS) .execute()
.execute() }
configFactory.user?.let { user ->
if (!user.isBlockCommunityMessageRequestsSet()) {
user.setCommunityMessageRequests(false)
}
}
} }
} }

@ -1,9 +1,11 @@
package org.thoughtcrime.securesms.preferences package org.thoughtcrime.securesms.preferences
import android.os.Bundle import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
@AndroidEntryPoint
class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() { class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {

@ -8,6 +8,9 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceDataStore
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.BuildConfig import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -15,13 +18,19 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswo
import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled
import org.thoughtcrime.securesms.util.IntentUtils import org.thoughtcrime.securesms.util.IntentUtils
import javax.inject.Inject
@AndroidEntryPoint
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() { class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
@Inject lateinit var configFactory: ConfigFactory
override fun onCreate(paramBundle: Bundle?) { override fun onCreate(paramBundle: Bundle?) {
super.onCreate(paramBundle) super.onCreate(paramBundle)
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!! findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
@ -30,6 +39,33 @@ class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
.onPreferenceChangeListener = TypingIndicatorsToggleListener() .onPreferenceChangeListener = TypingIndicatorsToggleListener()
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!! findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) } .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
findPreference<PreferenceCategory>(getString(R.string.preferences__message_requests_category))?.let { category ->
when (val user = configFactory.user) {
null -> category.isVisible = false
else -> SwitchPreferenceCompat(requireContext()).apply {
key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS
preferenceDataStore = object : PreferenceDataStore() {
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
return user.getCommunityMessageRequests()
}
return super.getBoolean(key, defValue)
}
override fun putBoolean(key: String?, value: Boolean) {
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
user.setCommunityMessageRequests(value)
return
}
super.putBoolean(key, value)
}
}
title = getString(R.string.preferences__message_requests_title)
summary = getString(R.string.preferences__message_requests_summary)
}.let(category::addPreference)
}
}
initializeVisibility() initializeVisibility()
} }

@ -1,5 +1,11 @@
package org.thoughtcrime.securesms.repository package org.thoughtcrime.securesms.repository
import android.content.ContentResolver
import android.content.Context
import app.cash.copper.flow.observeQuery
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
@ -15,6 +21,7 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase
@ -35,6 +42,8 @@ import kotlin.coroutines.suspendCoroutine
interface ConversationRepository { interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient? fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
fun maybeGetBlindedRecipient(recipient: Recipient): Recipient?
fun recipientUpdateFlow(threadId: Long): Flow<Recipient?>
fun saveDraft(threadId: Long, text: String) fun saveDraft(threadId: Long, text: String)
fun getDraft(threadId: Long): String? fun getDraft(threadId: Long): String?
fun clearDrafts(threadId: Long) fun clearDrafts(threadId: Long)
@ -75,6 +84,7 @@ interface ConversationRepository {
} }
class DefaultConversationRepository @Inject constructor( class DefaultConversationRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
private val messageDataProvider: MessageDataProvider, private val messageDataProvider: MessageDataProvider,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
@ -87,13 +97,29 @@ class DefaultConversationRepository @Inject constructor(
private val storage: Storage, private val storage: Storage,
private val lokiMessageDb: LokiMessageDatabase, private val lokiMessageDb: LokiMessageDatabase,
private val sessionJobDb: SessionJobDatabase, private val sessionJobDb: SessionJobDatabase,
private val configFactory: ConfigFactory private val configFactory: ConfigFactory,
private val contentResolver: ContentResolver,
) : ConversationRepository { ) : ConversationRepository {
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? { override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
return threadDb.getRecipientForThreadId(threadId) return threadDb.getRecipientForThreadId(threadId)
} }
override fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? {
if (!recipient.isOpenGroupInboxRecipient) return null
return Recipient.from(
context,
Address.fromSerialized(GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())),
false
)
}
override fun recipientUpdateFlow(threadId: Long): Flow<Recipient?> {
return contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)).map {
maybeGetRecipientForThreadId(threadId)
}
}
override fun saveDraft(threadId: Long, text: String) { override fun saveDraft(threadId: Long, text: String) {
if (text.isEmpty()) return if (text.isEmpty()) return
val drafts = DraftDatabase.Drafts() val drafts = DraftDatabase.Drafts()

@ -628,6 +628,9 @@
<string name="preferences_notifications__priority">Priority</string> <string name="preferences_notifications__priority">Priority</string>
<string name="preferences_app_protection__screenshot_notifications">Screenshot Notifications</string> <string name="preferences_app_protection__screenshot_notifications">Screenshot Notifications</string>
<string name="preferences_app_protected__screenshot_notifications_summary">Receive a notification when a contact takes a screenshot of a one-to-one chat.</string> <string name="preferences_app_protected__screenshot_notifications_summary">Receive a notification when a contact takes a screenshot of a one-to-one chat.</string>
<string name="preferences__message_requests_category">Message Requests</string>
<string name="preferences__message_requests_title">Community Message Requests</string>
<string name="preferences__message_requests_summary">Allow message requests from Community conversations</string>
<!-- **************************************** --> <!-- **************************************** -->
<!-- menus --> <!-- menus -->
<!-- **************************************** --> <!-- **************************************** -->
@ -1036,6 +1039,7 @@
<string name="activity_home_outdated_client_config">Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.</string> <string name="activity_home_outdated_client_config">Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.</string>
<string name="activity_conversation_empty_state_read_only">There are no messages in <b>%s</b>.</string> <string name="activity_conversation_empty_state_read_only">There are no messages in <b>%s</b>.</string>
<string name="activity_conversation_empty_state_blocks_community_requests"><b>%s</b> has message requests from Community conversations turned off, so you cannot send them a message.</string>
<string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string> <string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string>
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string> <string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>

@ -20,6 +20,12 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:title="@string/preferences__message_requests_category"
android:key="@string/preferences__message_requests_category"
android:persistent="false">
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences__read_receipts"> <PreferenceCategory android:title="@string/preferences__read_receipts">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat <org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"

@ -1,42 +1,46 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.hamcrest.CoreMatchers.endsWith import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet import org.mockito.Mockito.anySet
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.ResultOf import org.thoughtcrime.securesms.repository.ResultOf
import org.mockito.Mockito.`when` as whenever
class ConversationViewModelTest: BaseViewModelTest() { class ConversationViewModelTest: BaseViewModelTest() {
private val repository = mock(ConversationRepository::class.java) private val repository = mock<ConversationRepository>()
private val storage = mock(Storage::class.java) private val storage = mock<Storage>()
private val threadId = 123L private val threadId = 123L
private val edKeyPair = mock(KeyPair::class.java) private val edKeyPair = mock<KeyPair>()
private lateinit var recipient: Recipient private lateinit var recipient: Recipient
private val viewModel: ConversationViewModel by lazy { private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage) ConversationViewModel(threadId, edKeyPair, repository, storage)
} }
@Before @Before
fun setUp() { fun setUp() {
recipient = mock(Recipient::class.java) recipient = mock()
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient) whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
} }
@Test @Test
@ -79,7 +83,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should delete locally`() { fun `should delete locally`() {
val message = mock(MessageRecord::class.java) val message = mock<MessageRecord>()
viewModel.deleteLocally(message) viewModel.deleteLocally(message)
@ -88,7 +92,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest { fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
val message = mock(MessageRecord::class.java) val message = mock<MessageRecord>()
val error = Throwable() val error = Throwable()
whenever(repository.deleteForEveryone(anyLong(), any(), any())) whenever(repository.deleteForEveryone(anyLong(), any(), any()))
.thenReturn(ResultOf.Failure(error)) .thenReturn(ResultOf.Failure(error))
@ -101,7 +105,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `should emit error message on failure to delete messages without unsend request`() = fun `should emit error message on failure to delete messages without unsend request`() =
runBlockingTest { runBlockingTest {
val message = mock(MessageRecord::class.java) val message = mock<MessageRecord>()
val error = Throwable() val error = Throwable()
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet())) whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
.thenReturn(ResultOf.Failure(error)) .thenReturn(ResultOf.Failure(error))
@ -181,4 +185,30 @@ class ConversationViewModelTest: BaseViewModelTest() {
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0)) assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0))
} }
@Test
fun `open group recipient should have no blinded recipient`() {
whenever(recipient.isOpenGroupRecipient).thenReturn(true)
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
assertThat(viewModel.blindedRecipient, nullValue())
}
@Test
fun `local recipient should have input and no blinded recipient`() {
whenever(recipient.isLocalNumber).thenReturn(true)
assertThat(viewModel.hidesInputBar(), equalTo(false))
assertThat(viewModel.blindedRecipient, nullValue())
}
@Test
fun `contact recipient should hide input bar if not accepting requests`() {
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(true)
val blinded = mock<Recipient> {
whenever(it.blocksCommunityMessageRequests).thenReturn(true)
}
whenever(repository.maybeGetBlindedRecipient(recipient)).thenReturn(blinded)
assertThat(viewModel.blindedRecipient, notNullValue())
assertThat(viewModel.hidesInputBar(), equalTo(true))
}
} }

@ -95,4 +95,33 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv *
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto profile = ptrToProfile(env, thiz); auto profile = ptrToProfile(env, thiz);
return profile->get_nts_priority(); return profile->get_nts_priority();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_getCommunityMessageRequests(
JNIEnv *env, jobject thiz) {
std::lock_guard lock{util::util_mutex_};
auto profile = ptrToProfile(env, thiz);
auto blinded_msg_requests = profile->get_blinded_msgreqs();
if (blinded_msg_requests.has_value()) {
return *blinded_msg_requests;
}
return true;
}
extern "C"
JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_setCommunityMessageRequests(
JNIEnv *env, jobject thiz, jboolean blocks) {
std::lock_guard lock{util::util_mutex_};
auto profile = ptrToProfile(env, thiz);
profile->set_blinded_msgreqs(std::optional{(bool)blocks});
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_isBlockCommunityMessageRequestsSet(
JNIEnv *env, jobject thiz) {
std::lock_guard lock{util::util_mutex_};
auto profile = ptrToProfile(env, thiz);
return profile->get_blinded_msgreqs().has_value();
} }

@ -127,6 +127,9 @@ class UserProfile(pointer: Long) : ConfigBase(pointer) {
external fun setPic(userPic: UserPic) external fun setPic(userPic: UserPic)
external fun setNtsPriority(priority: Int) external fun setNtsPriority(priority: Int)
external fun getNtsPriority(): Int external fun getNtsPriority(): Int
external fun getCommunityMessageRequests(): Boolean
external fun setCommunityMessageRequests(blocks: Boolean)
external fun isBlockCommunityMessageRequestsSet(): Boolean
} }
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {

@ -42,6 +42,7 @@ interface StorageProtocol {
fun getUserProfile(): Profile fun getUserProfile(): Profile
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
fun clearUserPic() fun clearUserPic()
// Signal // Signal
@ -233,4 +234,5 @@ interface StorageProtocol {
fun notifyConfigUpdates(forConfigObject: ConfigBase) fun notifyConfigUpdates(forConfigObject: ConfigBase)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
fun isCheckingCommunityRequests(): Boolean
} }

@ -25,7 +25,8 @@ class VisibleMessage(
var profile: Profile? = null, var profile: Profile? = null,
var openGroupInvitation: OpenGroupInvitation? = null, var openGroupInvitation: OpenGroupInvitation? = null,
var reaction: Reaction? = null, var reaction: Reaction? = null,
var hasMention: Boolean = false var hasMention: Boolean = false,
var blocksMessageRequests: Boolean = false
) : Message() { ) : Message() {
override val isSelfSendValid: Boolean = true override val isSelfSendValid: Boolean = true
@ -74,6 +75,9 @@ class VisibleMessage(
val reaction = Reaction.fromProto(reactionProto) val reaction = Reaction.fromProto(reactionProto)
result.reaction = reaction result.reaction = reaction
} }
result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests }
return result return result
} }
} }
@ -132,6 +136,8 @@ class VisibleMessage(
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
} }
dataMessage.expireTimer = expiration dataMessage.expireTimer = expiration
// Community blocked message requests flag
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
// Sync target // Sync target
if (syncTarget != null) { if (syncTarget != null) {
dataMessage.syncTarget = syncTarget dataMessage.syncTarget = syncTarget

@ -753,7 +753,8 @@ object OpenGroupApi {
) )
} }
val serverCapabilities = storage.getServerCapabilities(server) val serverCapabilities = storage.getServerCapabilities(server)
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) { val isAcceptingCommunityRequests = storage.isCheckingCommunityRequests()
if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && isAcceptingCommunityRequests) {
requests.add( requests.add(
if (lastInboxMessageId == null) { if (lastInboxMessageId == null) {
BatchRequestInfo( BatchRequestInfo(

@ -242,9 +242,16 @@ object MessageSender {
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> { private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val configFactory = MessagingModuleConfiguration.shared.configFactory
if (message.sentTimestamp == null) { if (message.sentTimestamp == null) {
message.sentTimestamp = SnodeAPI.nowWithOffset message.sentTimestamp = SnodeAPI.nowWithOffset
} }
// Attach the blocks message requests info
configFactory.user?.let { user ->
if (message is VisibleMessage) {
message.blocksMessageRequests = !user.getCommunityMessageRequests()
}
}
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
var serverCapabilities = listOf<String>() var serverCapabilities = listOf<String>()
var blindedPublicKey: ByteArray? = null var blindedPublicKey: ByteArray? = null

@ -305,6 +305,10 @@ fun MessageReceiver.handleVisibleMessage(
profileManager.setProfilePicture(context, recipient, null, null) profileManager.setProfilePicture(context, recipient, null, null)
} }
} }
if (userPublicKey != messageSender && !isUserBlindedSender) {
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
}
} }
// Parse quote if needed // Parse quote if needed
var quoteModel: QuoteModel? = null var quoteModel: QuoteModel? = null

@ -1,5 +1,7 @@
package org.session.libsession.utilities package org.session.libsession.utilities
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import java.io.IOException import java.io.IOException
@ -15,8 +17,15 @@ object GroupUtil {
} }
@JvmStatic @JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String { fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address {
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID) val openGroupInboxId =
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
return getEncodedOpenGroupInboxID(openGroupInboxId)
}
@JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
} }
@JvmStatic @JvmStatic
@ -51,7 +60,7 @@ object GroupUtil {
} }
@JvmStatic @JvmStatic
fun getDecodedOpenGroupInbox(groupID: String): String { fun getDecodedOpenGroupInboxSessionId(groupID: String): String {
val decodedGroupId = getDecodedGroupID(groupID) val decodedGroupId = getDecodedGroupID(groupID)
if (decodedGroupId.split("!").count() > 2) { if (decodedGroupId.split("!").count() > 2) {
return decodedGroupId.split("!", limit = 3)[2] return decodedGroupId.split("!", limit = 3)[2]

@ -287,6 +287,8 @@ interface TextSecurePreferences {
const val OCEAN_DARK = "ocean.dark" const val OCEAN_DARK = "ocean.dark"
const val OCEAN_LIGHT = "ocean.light" const val OCEAN_LIGHT = "ocean.light"
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
@JvmStatic @JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long { fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0) return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)

@ -101,6 +101,7 @@ public class Recipient implements RecipientModifiedListener {
private String notificationChannel; private String notificationChannel;
private boolean forceSmsSelection; private boolean forceSmsSelection;
private String wrapperHash; private String wrapperHash;
private boolean blocksCommunityMessageRequests;
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED; private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
@ -195,6 +196,7 @@ public class Recipient implements RecipientModifiedListener {
this.forceSmsSelection = details.get().forceSmsSelection; this.forceSmsSelection = details.get().forceSmsSelection;
this.notifyType = details.get().notifyType; this.notifyType = details.get().notifyType;
this.autoDownloadAttachments = details.get().autoDownloadAttachments; this.autoDownloadAttachments = details.get().autoDownloadAttachments;
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
this.participants.clear(); this.participants.clear();
this.participants.addAll(details.get().participants); this.participants.addAll(details.get().participants);
@ -232,6 +234,8 @@ public class Recipient implements RecipientModifiedListener {
Recipient.this.forceSmsSelection = result.forceSmsSelection; Recipient.this.forceSmsSelection = result.forceSmsSelection;
Recipient.this.notifyType = result.notifyType; Recipient.this.notifyType = result.notifyType;
Recipient.this.autoDownloadAttachments = result.autoDownloadAttachments; Recipient.this.autoDownloadAttachments = result.autoDownloadAttachments;
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
Recipient.this.participants.clear(); Recipient.this.participants.clear();
Recipient.this.participants.addAll(result.participants); Recipient.this.participants.addAll(result.participants);
@ -286,6 +290,7 @@ public class Recipient implements RecipientModifiedListener {
this.unidentifiedAccessMode = details.unidentifiedAccessMode; this.unidentifiedAccessMode = details.unidentifiedAccessMode;
this.forceSmsSelection = details.forceSmsSelection; this.forceSmsSelection = details.forceSmsSelection;
this.wrapperHash = details.wrapperHash; this.wrapperHash = details.wrapperHash;
this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests;
this.participants.addAll(details.participants); this.participants.addAll(details.participants);
this.resolving = false; this.resolving = false;
@ -326,7 +331,7 @@ public class Recipient implements RecipientModifiedListener {
return this.name; return this.name;
} }
} else if (isOpenGroupInboxRecipient()){ } else if (isOpenGroupInboxRecipient()){
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID); String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID);
Contact contact = storage.getContactWithSessionID(inboxID); Contact contact = storage.getContactWithSessionID(inboxID);
if (contact == null) { return sessionID; } if (contact == null) { return sessionID; }
return contact.displayName(Contact.ContactContext.REGULAR); return contact.displayName(Contact.ContactContext.REGULAR);
@ -350,6 +355,18 @@ public class Recipient implements RecipientModifiedListener {
if (notify) notifyListeners(); if (notify) notifyListeners();
} }
public boolean getBlocksCommunityMessageRequests() {
return blocksCommunityMessageRequests;
}
public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) {
synchronized (this) {
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
}
notifyListeners();
}
public synchronized @NonNull MaterialColor getColor() { public synchronized @NonNull MaterialColor getColor() {
if (isGroupRecipient()) return MaterialColor.GROUP; if (isGroupRecipient()) return MaterialColor.GROUP;
else if (color != null) return color; else if (color != null) return color;
@ -777,12 +794,43 @@ public class Recipient implements RecipientModifiedListener {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Recipient recipient = (Recipient) o; 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) && Objects.equals(wrapperHash, recipient.wrapperHash); 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)
&& Objects.equals(wrapperHash, recipient.wrapperHash)
&& blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests;
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash); int result = Objects.hash(
address,
name,
customLabel,
resolving,
groupAvatarId,
mutedUntil,
notifyType,
blocked,
approved,
approvedMe,
expireMessages,
profileName,
profileAvatar,
wrapperHash,
blocksCommunityMessageRequests
);
result = 31 * result + Arrays.hashCode(profileKey); result = 31 * result + Arrays.hashCode(profileKey);
return result; return result;
} }
@ -888,10 +936,11 @@ public class Recipient implements RecipientModifiedListener {
private final UnidentifiedAccessMode unidentifiedAccessMode; private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection; private final boolean forceSmsSelection;
private final String wrapperHash; private final String wrapperHash;
private final boolean blocksCommunityMessageRequests;
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil, public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
int notifyType, int notifyType,
boolean autoDownloadAttachments, boolean autoDownloadAttachments,
@NonNull VibrateState messageVibrateState, @NonNull VibrateState messageVibrateState,
@NonNull VibrateState callVibrateState, @NonNull VibrateState callVibrateState,
@Nullable Uri messageRingtone, @Nullable Uri messageRingtone,
@ -911,7 +960,9 @@ public class Recipient implements RecipientModifiedListener {
@Nullable String notificationChannel, @Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode, @NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection, boolean forceSmsSelection,
String wrapperHash) String wrapperHash,
boolean blocksCommunityMessageRequests
)
{ {
this.blocked = blocked; this.blocked = blocked;
this.approved = approved; this.approved = approved;
@ -938,7 +989,7 @@ public class Recipient implements RecipientModifiedListener {
this.notificationChannel = notificationChannel; this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection; this.forceSmsSelection = forceSmsSelection;
this.wrapperHash = wrapperHash; this.wrapperHash = wrapperHash;this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
} }
public @Nullable MaterialColor getColor() { public @Nullable MaterialColor getColor() {
@ -1045,6 +1096,10 @@ public class Recipient implements RecipientModifiedListener {
return wrapperHash; return wrapperHash;
} }
public boolean getBlocksCommunityMessageRequests() {
return blocksCommunityMessageRequests;
}
} }

@ -179,6 +179,7 @@ class RecipientProvider {
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode; @NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
final boolean forceSmsSelection; final boolean forceSmsSelection;
final String wrapperHash; final String wrapperHash;
final boolean blocksCommunityMessageRequests;
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings, boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
@ -213,6 +214,7 @@ class RecipientProvider {
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED; this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
this.forceSmsSelection = settings != null && settings.isForceSmsSelection(); this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
this.wrapperHash = settings != null ? settings.getWrapperHash() : null; this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests();
if (name == null && settings != null) this.name = settings.getSystemDisplayName(); if (name == null && settings != null) this.name = settings.getSystemDisplayName();
else this.name = name; else this.name = name;

@ -177,19 +177,20 @@ message DataMessage {
required Action action = 4; required Action action = 4;
} }
optional string body = 1; optional string body = 1;
repeated AttachmentPointer attachments = 2; repeated AttachmentPointer attachments = 2;
optional uint32 flags = 4; optional uint32 flags = 4;
optional uint32 expireTimer = 5; optional uint32 expireTimer = 5;
optional bytes profileKey = 6; optional bytes profileKey = 6;
optional uint64 timestamp = 7; optional uint64 timestamp = 7;
optional Quote quote = 8; optional Quote quote = 8;
repeated Preview preview = 10; repeated Preview preview = 10;
optional Reaction reaction = 11; optional Reaction reaction = 11;
optional LokiProfile profile = 101; optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102; optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104; optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105; optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
optional GroupMessage groupMessage = 120; optional GroupMessage groupMessage = 120;
} }

Loading…
Cancel
Save