SES-1156 - Ban and delete functionality fix (#1428)

* WIP

* Investigation in progress

* End of day push

* WIP

* Fixes #1416

* Cleanup

* Added code to remove zombie messages caught in limbo during a ban & delete - still chock full o' debug while finding root cause

* Root cause debug WIP

* Push prior to cleanup

* Cleaned up for PR

* fix: mms delete, remove unnecessary values from sms

* Addressed PR feedback

* fix: fix unit tests

* Added '.run' folder with test setup

* Update README.md

Test commit for CI

* Re-added accidentally removed closing brace

---------

Co-authored-by: alansley <aclansley@gmail.com>
Co-authored-by: Al Lansley <alansley@users.noreply.github.com>
Co-authored-by: 0x330a <92654767+0x330a@users.noreply.github.com>
This commit is contained in:
AL-Session 2024-04-03 09:30:15 +11:00 committed by GitHub
parent 9ad5bd2374
commit a8a257a1a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 350 additions and 142 deletions

24
.run/Run Tests.run.xml Normal file
View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="testPlayDebugUnitTestCoverageReport" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@ -184,16 +184,21 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
override fun deleteMessage(messageID: Long, isSms: Boolean) { override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).mmsDatabase()
messagingDatabase.deleteMessage(messageID) messagingDatabase.deleteMessage(messageID)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
} }
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) { override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).mmsDatabase()
// Perform local delete
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId) messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
// Perform online delete
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
} }

View File

@ -132,7 +132,8 @@ class ProfilePictureView @JvmOverloads constructor(
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop() .circleCrop()
.into(imageView) .into(imageView)
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) { } else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
glide.clear(imageView)
glide.load(unknownOpenGroupDrawable) glide.load(unknownOpenGroupDrawable)
.centerCrop() .centerCrop()
.circleCrop() .circleCrop()

View File

@ -58,7 +58,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) { return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
it.address.isOpenGroup it.address.isCommunity
} }
} }

View File

@ -116,7 +116,7 @@ class ConversationActionBarView @JvmOverloads constructor(
) )
} }
if (recipient.isGroupRecipient) { if (recipient.isGroupRecipient) {
val title = if (recipient.isOpenGroupRecipient) { val title = if (recipient.isCommunityRecipient) {
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0 val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
context.getString(R.string.ConversationActivity_active_member_count, userCount) context.getString(R.string.ConversationActivity_active_member_count, userCount)
} else { } else {

View File

@ -395,7 +395,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
val recipient = viewModel.recipient val recipient = viewModel.recipient
val openGroup = recipient.let { viewModel.openGroup } val openGroup = recipient.let { viewModel.openGroup }
if (recipient == null || (recipient.isOpenGroupRecipient && openGroup == null)) { if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish() return finish()
} }
@ -976,11 +976,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
view.glide = glide view.glide = glide
view.onCandidateSelected = { handleMentionSelected(it) } view.onCandidateSelected = { handleMentionSelected(it) }
additionalContentContainer.addView(view) additionalContentContainer.addView(view)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient) val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView = view this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId) view.show(candidates, viewModel.threadId)
} else { } else {
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient) val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView!!.setMentionCandidates(candidates) this.mentionCandidatesView!!.setMentionCandidates(candidates)
} }
isShowingMentionCandidatesView = true isShowingMentionCandidatesView = true
@ -1197,7 +1197,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun copyOpenGroupUrl(thread: Recipient) { override fun copyOpenGroupUrl(thread: Recipient) {
if (!thread.isOpenGroupRecipient) { return } if (!thread.isCommunityRecipient) { return }
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
@ -1361,7 +1361,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address } else originalMessage.individualRecipient.address
// Send it // Send it
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true) reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
if (recipient.isOpenGroupRecipient) { if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let { viewModel.openGroup?.let {
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji) OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
@ -1385,7 +1385,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address } else originalMessage.individualRecipient.address
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false) message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
if (recipient.isOpenGroupRecipient) { if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let { viewModel.openGroup?.let {
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji) OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
@ -1782,7 +1782,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendAttachments(slideDeck.asAttachments(), body) sendAttachments(slideDeck.asAttachments(), body)
} }
INVITE_CONTACTS -> { INVITE_CONTACTS -> {
if (viewModel.recipient?.isOpenGroupRecipient != true) { return } if (viewModel.recipient?.isCommunityRecipient != true) { return }
val extras = intent?.extras ?: return val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return } if (!intent.hasExtra(selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!! val selectedContacts = extras.getStringArray(selectedContactsKey)!!
@ -1848,19 +1848,62 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
handleLongPress(messages.first(), 0) //TODO: begin selection mode handleLongPress(messages.first(), 0) //TODO: begin selection mode
} }
// The option to "Delete just for me" or "Delete for everyone"
private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set<MessageRecord>) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = viewModel.recipient!!
bottomSheet.onDeleteForMeTapped = {
messages.forEach(viewModel::deleteLocally)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onDeleteForEveryoneTapped = {
messages.forEach(viewModel::deleteForEveryone)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onCancelTapped = {
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
val messageCount = 1
showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode)
}
}
// Note: The messages in the provided set may be a single message, or multiple if there are a
// group of selected messages.
override fun deleteMessages(messages: Set<MessageRecord>) { override fun deleteMessages(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient ?: return val recipient = viewModel.recipient
if (recipient == null) {
Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
return
}
val allSentByCurrentUser = messages.all { it.isOutgoing } val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null } val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
if (recipient.isOpenGroupRecipient) {
val messageCount = 1 // If the recipient is a community then we delete the message for everyone
if (recipient.isCommunityRecipient) {
val messageCount = 1 // Only used for plurals string
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() } button(R.string.delete) {
messages.forEach(viewModel::deleteForEveryone); endActionMode()
}
cancelButton { endActionMode() } cancelButton { endActionMode() }
} }
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
} else if (allSentByCurrentUser && allHasHash) { } else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet() val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = recipient bottomSheet.recipient = recipient
@ -1879,13 +1922,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode() endActionMode()
} }
bottomSheet.show(supportFragmentManager, bottomSheet.tag) bottomSheet.show(supportFragmentManager, bottomSheet.tag)
} else { }
else // Finally, if this is a closed group and you are deleting someone else's message(s)
// then we can only delete locally.
{
val messageCount = 1 val messageCount = 1
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() } button(R.string.delete) {
messages.forEach(viewModel::deleteLocally); endActionMode()
}
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
@ -1904,7 +1951,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
showSessionDialog { showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user) title(R.string.ConversationFragment_ban_selected_user)
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.") text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first().individualRecipient); endActionMode() } button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }

View File

@ -541,7 +541,7 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) }) items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
} }
// Copy Session ID // Copy Session ID
if (recipient.isGroupRecipient && !recipient.isOpenGroupRecipient && message.recipient.address.toString() != userPublicKey) { if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) }) items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
} }
// Delete message // Delete message

View File

@ -1,18 +1,23 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
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 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
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
@ -22,9 +27,12 @@ import org.session.libsession.utilities.Address
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.MmsSmsDatabase
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 java.util.UUID import java.util.UUID
class ConversationViewModel( class ConversationViewModel(
@ -144,9 +152,14 @@ class ConversationViewModel(
} }
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch { fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
val recipient = recipient ?: return@launch val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
repository.deleteForEveryone(threadId, recipient, message) repository.deleteForEveryone(threadId, recipient, message)
.onSuccess {
Log.d("Loki", "Deleted message ${message.id} ")
}
.onFailure { .onFailure {
Log.w("Loki", "FAILED TO delete message ${message.id} ")
showMessage("Couldn't delete message due to error: $it") showMessage("Couldn't delete message due to error: $it")
} }
} }
@ -168,10 +181,15 @@ class ConversationViewModel(
} }
} }
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch { fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch {
repository.banAndDeleteAll(threadId, recipient)
repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
.onSuccess { .onSuccess {
// At this point the server side messages have been successfully deleted..
showMessage("Successfully banned user and deleted all their messages") showMessage("Successfully banned user and deleted all their messages")
// ..so we can now delete all their messages in this thread from local storage & remove the views.
repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
} }
.onFailure { .onFailure {
showMessage("Couldn't execute request due to error: $it") showMessage("Couldn't execute request due to error: $it")

View File

@ -65,7 +65,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
// Copy Session ID // Copy Session ID
menu.findItem(R.id.menu_context_copy_public_key).isVisible = menu.findItem(R.id.menu_context_copy_public_key).isVisible =
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey) (thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// Message detail // Message detail
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1 menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
// Resend // Resend

View File

@ -50,7 +50,7 @@ object ConversationMenuHelper {
) { ) {
// Prepare // Prepare
menu.clear() menu.clear()
val isOpenGroup = thread.isOpenGroupRecipient val isOpenGroup = thread.isCommunityRecipient
// Base menu (options that should always be present) // Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu) inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages // Expiring messages
@ -253,7 +253,7 @@ object ConversationMenuHelper {
} }
private fun copyOpenGroupUrl(context: Context, thread: Recipient) { private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
if (!thread.isOpenGroupRecipient) { return } if (!thread.isCommunityRecipient) { return }
val listener = context as? ConversationMenuListener ?: return val listener = context as? ConversationMenuListener ?: return
listener.copyOpenGroupUrl(thread) listener.copyOpenGroupUrl(thread)
} }
@ -300,7 +300,7 @@ object ConversationMenuHelper {
} }
private fun inviteContacts(context: Context, thread: Recipient) { private fun inviteContacts(context: Context, thread: Recipient) {
if (!thread.isOpenGroupRecipient) { return } if (!thread.isCommunityRecipient) { return }
val intent = Intent(context, SelectContactsActivity::class.java) val intent = Intent(context, SelectContactsActivity::class.java)
val activity = context as AppCompatActivity val activity = context as AppCompatActivity
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS) activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)

View File

@ -166,7 +166,7 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.publicKey = senderSessionID binding.profilePictureView.publicKey = senderSessionID
binding.profilePictureView.update(message.individualRecipient) binding.profilePictureView.update(message.individualRecipient)
binding.profilePictureView.setOnClickListener { binding.profilePictureView.setOnClickListener {
if (thread.isOpenGroupRecipient) { if (thread.isCommunityRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) { if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
// TODO: support v2 soon // TODO: support v2 soon
@ -179,7 +179,7 @@ class VisibleMessageView : LinearLayout {
maybeShowUserDetails(senderSessionID, threadID) maybeShowUserDetails(senderSessionID, threadID)
} }
} }
if (thread.isOpenGroupRecipient) { if (thread.isCommunityRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
var standardPublicKey = "" var standardPublicKey = ""
var blindedPublicKey: String? = null var blindedPublicKey: String? = null
@ -195,7 +195,7 @@ class VisibleMessageView : LinearLayout {
} }
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected)) binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
val contactContext = val contactContext =
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
// Unread marker // Unread marker

View File

@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.session.libsession.utilities.WindowDebouncer; import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -77,11 +78,11 @@ public abstract class Database {
notifyConversationListListeners(); notifyConversationListListeners();
} }
protected void setNotifyConverationListeners(Cursor cursor, long threadId) { protected void setNotifyConversationListeners(Cursor cursor, long threadId) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId)); cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId));
} }
protected void setNotifyConverationListListeners(Cursor cursor) { protected void setNotifyConversationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI); cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
} }

View File

@ -6,8 +6,8 @@ import android.database.Cursor
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_INBOX_PREFIX import org.session.libsession.utilities.GroupUtil.COMMUNITY_INBOX_PREFIX
import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX import org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@ -38,8 +38,8 @@ class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHel
INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID} INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME} FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%' WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%'
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_PREFIX%' AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_PREFIX%'
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_INBOX_PREFIX%' AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_INBOX_PREFIX%'
AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0) AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0)
""".trimIndent() """.trimIndent()

View File

@ -4,6 +4,7 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
import org.session.libsignal.database.LokiMessageDatabaseProtocol import org.session.libsignal.database.LokiMessageDatabaseProtocol
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@ -72,7 +73,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
"${Companion.messageID} = ? AND $messageType = ?", "${Companion.messageID} = ? AND $messageType = ?",
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor -> arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
cursor.getInt(serverID).toLong() cursor.getInt(serverID).toLong()
} ?: return }
if (serverID == null) {
Log.w(this::class.simpleName, "Could not get server ID to delete message with ID: $messageID")
return
}
database.beginTransaction() database.beginTransaction()

View File

@ -68,7 +68,7 @@ public class MediaDatabase extends Database {
public Cursor getGalleryMediaForThread(long threadId) { public Cursor getGalleryMediaForThread(long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""}); Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
setNotifyConverationListeners(cursor, threadId); setNotifyConversationListeners(cursor, threadId);
return cursor; return cursor;
} }
@ -83,7 +83,7 @@ public class MediaDatabase extends Database {
public Cursor getDocumentMediaForThread(long threadId) { public Cursor getDocumentMediaForThread(long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""}); Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
setNotifyConverationListeners(cursor, threadId); setNotifyConversationListeners(cursor, threadId);
return cursor; return cursor;
} }

View File

@ -19,9 +19,9 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.provider.ContactsContract.CommonDataKinds.BaseTypes
import com.annimon.stream.Stream import com.annimon.stream.Stream
import com.google.android.mms.pdu_alt.PduHeaders import com.google.android.mms.pdu_alt.PduHeaders
import org.apache.commons.lang3.StringUtils
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -214,7 +214,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
fun getMessage(messageId: Long): Cursor { fun getMessage(messageId: Long): Cursor {
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId)) setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId))
return cursor return cursor
} }
@ -859,8 +859,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
*/ */
private fun deleteMessages(messageIds: Array<String?>) { private fun deleteMessages(messageIds: Array<String?>) {
if (messageIds.isEmpty()) { if (messageIds.isEmpty()) {
Log.w(TAG, "No message Ids provided to MmsDatabase.deleteMessages - aborting delete operation!")
return return
} }
// don't need thread IDs // don't need thread IDs
val queryBuilder = StringBuilder() val queryBuilder = StringBuilder()
for (i in messageIds.indices) { for (i in messageIds.indices) {
@ -883,6 +885,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
notifyStickerPackListeners() notifyStickerPackListeners()
} }
// Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
// - it is "Was the thread deleted because removing that message resulted in an empty thread"!
override fun deleteMessage(messageId: Long): Boolean { override fun deleteMessage(messageId: Long): Boolean {
val threadId = getThreadIdForMessage(messageId) val threadId = getThreadIdForMessage(messageId)
val attachmentDatabase = get(context).attachmentDatabase() val attachmentDatabase = get(context).attachmentDatabase()
@ -899,14 +903,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean { override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
val attachmentDatabase = get(context).attachmentDatabase() val argsArray = messageIds.map { "?" }
val groupReceiptDatabase = get(context).groupReceiptDatabase() val argValues = messageIds.map { it.toString() }.toTypedArray()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) }) val db = databaseHelper.writableDatabase
groupReceiptDatabase.deleteRowsForMessages(messageIds) db.delete(
TABLE_NAME,
val database = databaseHelper.writableDatabase ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(","))) argValues
)
val threadDeleted = get(context).threadDatabase().update(threadId, false, true) val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId) notifyConversationListeners(threadId)

View File

@ -183,7 +183,7 @@ public class MmsSmsDatabase extends Database {
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null; String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr); Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
setNotifyConverationListeners(cursor, threadId); setNotifyConversationListeners(cursor, threadId);
return cursor; return cursor;
} }
@ -209,6 +209,44 @@ public class MmsSmsDatabase extends Database {
} }
} }
// Builds up and returns a list of all all the messages sent by this user in the given thread.
// Used to do a pass through our local database to remove records when a user has "Ban & Delete"
// called on them in a Community.
public Set<MessageRecord> getAllMessageRecordsFromSenderInThread(long threadId, String serializedAuthor) {
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
Set<MessageRecord> identifiedMessages = new HashSet<MessageRecord>();
// Try everything with resources so that they auto-close on end of scope
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
identifiedMessages.add(messageRecord);
}
}
}
return identifiedMessages;
}
// Version of the above `getAllMessageRecordsFromSenderInThread` method that returns the message
// Ids rather than the set of MessageRecords - currently unused by potentially useful in the future.
public Set<Long> getAllMessageIdsFromSenderInThread(long threadId, String serializedAuthor) {
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
Set<Long> identifiedMessages = new HashSet<Long>();
// Try everything with resources so that they auto-close on end of scope
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
identifiedMessages.add(messageRecord.id);
}
}
}
return identifiedMessages;
}
public long getLastSentMessageFromSender(long threadId, String serializedAuthor) { public long getLastSentMessageFromSender(long threadId, String serializedAuthor) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;

View File

@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX; import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
@ -123,18 +123,18 @@ public class RecipientDatabase extends Database {
public static String getUpdateApprovedCommand() { public static String getUpdateApprovedCommand() {
return "UPDATE "+ TABLE_NAME + " " + return "UPDATE "+ TABLE_NAME + " " +
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " + "SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'"; "WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
} }
public static String getUpdateResetApprovedCommand() { public static String getUpdateResetApprovedCommand() {
return "UPDATE "+ TABLE_NAME + " " + return "UPDATE "+ TABLE_NAME + " " +
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " + "SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'"; "WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
} }
public static String getUpdateApprovedSelectConversations() { public static String getUpdateApprovedSelectConversations() {
return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+ return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+
"WHERE "+ADDRESS+ " NOT LIKE '"+OPEN_GROUP_PREFIX+"%' " + "WHERE "+ADDRESS+ " NOT LIKE '"+ COMMUNITY_PREFIX +"%' " +
"AND ("+ADDRESS+" IN (SELECT "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" FROM "+ThreadDatabase.TABLE_NAME+" WHERE ("+ThreadDatabase.MESSAGE_COUNT+" != 0) "+ "AND ("+ADDRESS+" IN (SELECT "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" FROM "+ThreadDatabase.TABLE_NAME+" WHERE ("+ThreadDatabase.MESSAGE_COUNT+" != 0) "+
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))"; "OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
} }

View File

@ -119,7 +119,7 @@ public class SearchDatabase extends Database {
int queryLimit = Math.min(query.length()*50,500); int queryLimit = Math.min(query.length()*50,500);
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) }); Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
setNotifyConverationListListeners(cursor); setNotifyConversationListListeners(cursor);
return cursor; return cursor;
} }
@ -128,7 +128,7 @@ public class SearchDatabase extends Database {
String prefixQuery = adjustQuery(query); String prefixQuery = adjustQuery(query);
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) }); Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
setNotifyConverationListListeners(cursor); setNotifyConversationListListeners(cursor);
return cursor; return cursor;
} }

View File

@ -621,10 +621,12 @@ public class SmsDatabase extends MessagingDatabase {
public Cursor getMessageCursor(long messageId) { public Cursor getMessageCursor(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null); Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId)); setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId));
return cursor; return cursor;
} }
// Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
// - it is "Was the thread deleted because removing that message resulted in an empty thread"!
@Override @Override
public boolean deleteMessage(long messageId) { public boolean deleteMessage(long messageId) {
Log.i("MessageDatabase", "Deleting: " + messageId); Log.i("MessageDatabase", "Deleting: " + messageId);
@ -645,9 +647,6 @@ public class SmsDatabase extends MessagingDatabase {
argValues[i] = (messageIds[i] + ""); argValues[i] = (messageIds[i] + "");
} }
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
String combinedMessageIds = StringUtils.join(messageIds, ',');
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete( db.delete(
TABLE_NAME, TABLE_NAME,

View File

@ -121,7 +121,7 @@ open class Storage(
) )
volatile.set(newVolatileParams) volatile.set(newVolatileParams)
} }
} else if (address.isOpenGroup) { } else if (address.isCommunity) {
// these should be added on the group join / group info fetch // these should be added on the group join / group info fetch
Log.w("Loki", "Thread created called for open group address, not adding any extra information") Log.w("Loki", "Thread created called for open group address, not adding any extra information")
} }
@ -152,7 +152,7 @@ open class Storage(
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
volatile.eraseLegacyClosedGroup(sessionId) volatile.eraseLegacyClosedGroup(sessionId)
groups.eraseLegacyGroup(sessionId) groups.eraseLegacyGroup(sessionId)
} else if (address.isOpenGroup) { } else if (address.isCommunity) {
// these should be removed in the group leave / handling new configs // these should be removed in the group leave / handling new configs
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
} }
@ -257,7 +257,7 @@ open class Storage(
// recipient closed group // recipient closed group
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize())) recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
// recipient is open group // recipient is open group
recipient.isOpenGroupRecipient -> { recipient.isCommunityRecipient -> {
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
config.getOrConstructCommunity(base, room, pubKey) config.getOrConstructCommunity(base, room, pubKey)
@ -327,7 +327,7 @@ open class Storage(
setRecipientApprovedMe(targetRecipient, true) setRecipientApprovedMe(targetRecipient, true)
} }
} }
if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) { if (message.threadID == null && !targetRecipient.isCommunityRecipient) {
// open group recipients should explicitly create threads // open group recipients should explicitly create threads
message.threadID = getOrCreateThreadIdFor(targetAddress) message.threadID = getOrCreateThreadIdFor(targetAddress)
} }
@ -1289,7 +1289,7 @@ open class Storage(
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
) )
groups.set(newGroupInfo) groups.set(newGroupInfo)
} else if (threadRecipient.isOpenGroupRecipient) { } else if (threadRecipient.isCommunityRecipient) {
val openGroup = getOpenGroup(threadID) ?: return val openGroup = getOpenGroup(threadID) ?: return
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (

View File

@ -18,7 +18,7 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX; import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX;
import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX; import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID; import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID;
import android.content.ContentValues; import android.content.ContentValues;
@ -427,7 +427,7 @@ public class ThreadDatabase extends Database {
} }
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0); Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
setNotifyConverationListListeners(cursor); setNotifyConversationListListeners(cursor);
return cursor; return cursor;
} }
@ -491,7 +491,7 @@ public class ThreadDatabase extends Database {
} }
public Cursor getConversationList() { public Cursor getConversationList() {
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 "; "AND " + ARCHIVED + " = 0 ";
return getConversationList(where); return getConversationList(where);
} }
@ -502,7 +502,7 @@ public class ThreadDatabase extends Database {
} }
public Cursor getApprovedConversationList() { public Cursor getApprovedConversationList() {
String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 "; "AND " + ARCHIVED + " = 0 ";
return getConversationList(where); return getConversationList(where);
} }
@ -516,7 +516,7 @@ public class ThreadDatabase extends Database {
} }
public Cursor getArchivedConversationList() { public Cursor getArchivedConversationList() {
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
"AND " + ARCHIVED + " = 1 "; "AND " + ARCHIVED + " = 1 ";
return getConversationList(where); return getConversationList(where);
} }
@ -526,7 +526,7 @@ public class ThreadDatabase extends Database {
String query = createQuery(where, 0); String query = createQuery(where, 0);
Cursor cursor = db.rawQuery(query, null); Cursor cursor = db.rawQuery(query, null);
setNotifyConverationListListeners(cursor); setNotifyConversationListListeners(cursor);
return cursor; return cursor;
} }
@ -547,7 +547,7 @@ public class ThreadDatabase extends Database {
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example) // edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
Recipient forThreadId = getRecipientForThreadId(threadId); Recipient forThreadId = getRecipientForThreadId(threadId);
if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false; if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isCommunityRecipient()) return false;
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
@ -822,7 +822,7 @@ public class ThreadDatabase extends Database {
private boolean deleteThreadOnEmpty(long threadId) { private boolean deleteThreadOnEmpty(long threadId) {
Recipient threadRecipient = getRecipientForThreadId(threadId); Recipient threadRecipient = getRecipientForThreadId(threadId);
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient(); return threadRecipient != null && !threadRecipient.isCommunityRecipient();
} }
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {

View File

@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.getConversationUnread import org.thoughtcrime.securesms.util.getConversationUnread
import javax.inject.Inject import javax.inject.Inject
@ -75,7 +74,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
} }
binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
binding.copyConversationId.setOnClickListener(this) binding.copyConversationId.setOnClickListener(this)
binding.copyCommunityUrl.visibility = if (recipient.isOpenGroupRecipient) View.VISIBLE else View.GONE binding.copyCommunityUrl.visibility = if (recipient.isCommunityRecipient) View.VISIBLE else View.GONE
binding.copyCommunityUrl.setOnClickListener(this) binding.copyCommunityUrl.setOnClickListener(this)
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber

View File

@ -496,7 +496,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
} }
else if (thread.recipient.isOpenGroupRecipient) { else if (thread.recipient.isCommunityRecipient) {
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit

View File

@ -21,6 +21,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityPathBinding import network.loki.messenger.databinding.ActivityPathBinding
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities

View File

@ -91,10 +91,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
&& !threadRecipient.isOpenGroupInboxRecipient && !threadRecipient.isOpenGroupInboxRecipient
&& !threadRecipient.isOpenGroupOutboxRecipient && !threadRecipient.isOpenGroupOutboxRecipient
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient publicKeyTextView.isVisible = !threadRecipient.isCommunityRecipient
&& !threadRecipient.isOpenGroupInboxRecipient && !threadRecipient.isOpenGroupInboxRecipient
&& !threadRecipient.isOpenGroupOutboxRecipient && !threadRecipient.isOpenGroupOutboxRecipient
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true messageButton.isVisible = !threadRecipient.isCommunityRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
publicKeyTextView.text = publicKey publicKeyTextView.text = publicKey
publicKeyTextView.setOnLongClickListener { publicKeyTextView.setOnLongClickListener {
val clipboard = val clipboard =

View File

@ -53,7 +53,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) { public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
String displayName = recipient.toShortString(); String displayName = recipient.toShortString();
if (threadRecipient.isGroupRecipient()) { if (threadRecipient.isGroupRecipient()) {
displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient()); displayName = getGroupDisplayName(recipient, threadRecipient.isCommunityRecipient());
} }
if (privacy.isDisplayContact()) { if (privacy.isDisplayContact()) {
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName)); setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
@ -79,7 +79,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) { public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
String displayName = sender.toShortString(); String displayName = sender.toShortString();
if (threadRecipient.isGroupRecipient()) { if (threadRecipient.isGroupRecipient()) {
displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient()); displayName = getGroupDisplayName(sender, threadRecipient.isCommunityRecipient());
} }
if (privacy.isDisplayMessage()) { if (privacy.isDisplayMessage()) {
SpannableStringBuilder builder = new SpannableStringBuilder(); SpannableStringBuilder builder = new SpannableStringBuilder();

View File

@ -125,7 +125,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) { if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient()); String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
stringBuilder.append(Util.getBoldedString(displayName + ": ")); stringBuilder.append(Util.getBoldedString(displayName + ": "));
} }
@ -215,7 +215,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) { if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient()); String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
stringBuilder.append(Util.getBoldedString(displayName + ": ")); stringBuilder.append(Util.getBoldedString(displayName + ": "));
} }

View File

@ -1,13 +1,22 @@
package org.thoughtcrime.securesms.repository package org.thoughtcrime.securesms.repository
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import app.cash.copper.Query import app.cash.copper.Query
import app.cash.copper.flow.observeQuery import app.cash.copper.flow.observeQuery
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map 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
@ -22,7 +31,10 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil 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.Log
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
@ -39,10 +51,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface ConversationRepository { interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient? fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
@ -55,37 +65,19 @@ interface ConversationRepository {
fun inviteContacts(threadId: Long, contacts: List<Recipient>) fun inviteContacts(threadId: Long, contacts: List<Recipient>)
fun setBlocked(recipient: Recipient, blocked: Boolean) fun setBlocked(recipient: Recipient, blocked: Boolean)
fun deleteLocally(recipient: Recipient, message: MessageRecord) fun deleteLocally(recipient: Recipient, message: MessageRecord)
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean) fun setApproved(recipient: Recipient, isApproved: Boolean)
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): ResultOf<Unit>
suspend fun deleteForEveryone(
threadId: Long,
recipient: Recipient,
message: MessageRecord
): ResultOf<Unit>
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): ResultOf<Unit>
suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long,
messages: Set<MessageRecord>
): ResultOf<Unit>
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
suspend fun deleteThread(threadId: Long): ResultOf<Unit> suspend fun deleteThread(threadId: Long): ResultOf<Unit>
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit> suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit> suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
fun declineMessageRequest(threadId: Long) fun declineMessageRequest(threadId: Long)
fun hasReceived(threadId: Long): Boolean fun hasReceived(threadId: Long): Boolean
} }
class DefaultConversationRepository @Inject constructor( class DefaultConversationRepository @Inject constructor(
@ -184,6 +176,15 @@ class DefaultConversationRepository @Inject constructor(
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
} }
override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
val threadId = messageRecord.threadId
val senderId = messageRecord.recipient.address.contactIdentifier()
val messageRecordsToRemoveFromLocalStorage = mmsSmsDb.getAllMessageRecordsFromSenderInThread(threadId, senderId)
for (message in messageRecordsToRemoveFromLocalStorage) {
messageDataProvider.deleteMessage(message.id, !message.isMms)
}
}
override fun setApproved(recipient: Recipient, isApproved: Boolean) { override fun setApproved(recipient: Recipient, isApproved: Boolean) {
storage.setRecipientApproved(recipient, isApproved) storage.setRecipientApproved(recipient, isApproved)
} }
@ -196,18 +197,38 @@ class DefaultConversationRepository @Inject constructor(
buildUnsendRequest(recipient, message)?.let { unsendRequest -> buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address) MessageSender.send(unsendRequest, recipient.address)
} }
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) { if (openGroup != null) {
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID -> val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server) OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success { .success {
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
}.fail { error -> }.fail { error ->
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
continuation.resumeWithException(error) continuation.resumeWithException(error)
} }
} }
} else {
// If the server ID is null then this message is stuck in limbo (it has likely been
// deleted remotely but that deletion did not occur locally) - so we'll delete the
// message locally to clean up.
if (serverId == null) {
Log.w("ConversationRepository","Found community message without a server ID - deleting locally.")
// Caution: The bool returned from `deleteMessage` is NOT "Was the message
// successfully deleted?" - it is "Was the thread itself also deleted because
// removing that message resulted in an empty thread?".
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
}
}
}
else // If this thread is NOT in a Community
{
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash -> messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
var publicKey = recipient.address.serialize() var publicKey = recipient.address.serialize()
@ -218,6 +239,7 @@ class DefaultConversationRepository @Inject constructor(
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
}.fail { error -> }.fail { error ->
Log.w("[onversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resumeWithException(error) continuation.resumeWithException(error)
} }
} }
@ -225,7 +247,7 @@ class DefaultConversationRepository @Inject constructor(
} }
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? { override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
if (recipient.isOpenGroupRecipient) return null if (recipient.isCommunityRecipient) return null
messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null
return UnsendRequest( return UnsendRequest(
author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(), author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(),
@ -279,8 +301,10 @@ class DefaultConversationRepository @Inject constructor(
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> = override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
// Note: This sessionId could be the blinded Id
val sessionID = recipient.address.toString() val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server) OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))

View File

@ -193,7 +193,7 @@ object ConfigurationMessageUtilities {
while (current != null) { while (current != null) {
val recipient = current.recipient val recipient = current.recipient
val contact = when { val contact = when {
recipient.isOpenGroupRecipient -> { recipient.isCommunityRecipient -> {
val openGroup = storage.getOpenGroup(current.threadId) ?: continue val openGroup = storage.getOpenGroup(current.threadId) ?: continue
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
convoConfig.getOrConstructCommunity(base, room, pubKey) convoConfig.getOrConstructCommunity(base, room, pubKey)
@ -279,7 +279,7 @@ object ConfigurationMessageUtilities {
@JvmField @JvmField
val DELETE_INACTIVE_ONE_TO_ONES: String = """ val DELETE_INACTIVE_ONE_TO_ONES: String = """
DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_INBOX_PREFIX}%'; DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_INBOX_PREFIX}%';
""".trimIndent() """.trimIndent()
} }

View File

@ -14,7 +14,7 @@ fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Bool
return getOneToOne(recipient.address.serialize())?.unread == true return getOneToOne(recipient.address.serialize())?.unread == true
} else if (recipient.isClosedGroupRecipient) { } else if (recipient.isClosedGroupRecipient) {
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
} else if (recipient.isOpenGroupRecipient) { } else if (recipient.isCommunityRecipient) {
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
return getCommunity(openGroup.server, openGroup.room)?.unread == true return getCommunity(openGroup.server, openGroup.room)?.unread == true
} }

View File

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms
import org.session.libsignal.utilities.Log.Logger
object NoOpLogger: Logger() {
override fun v(tag: String?, message: String?, t: Throwable?) {}
override fun d(tag: String?, message: String?, t: Throwable?) {}
override fun i(tag: String?, message: String?, t: Throwable?) {}
override fun w(tag: String?, message: String?, t: Throwable?) {}
override fun e(tag: String?, message: String?, t: Throwable?) {}
override fun wtf(tag: String?, message: String?, t: Throwable?) {}
override fun blockUntilAllWritesFinished() {}
}

View File

@ -1,10 +1,20 @@
package org.thoughtcrime.securesms package org.thoughtcrime.securesms
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.session.libsignal.utilities.Log
open class BaseViewModelTest: BaseCoroutineTest() { open class BaseViewModelTest: BaseCoroutineTest() {
companion object {
@BeforeClass
@JvmStatic
fun setupLogger() {
Log.initialize(NoOpLogger)
}
}
@get:Rule @get:Rule
var instantExecutorRule = InstantTaskExecutorRule() var instantExecutorRule = InstantTaskExecutorRule()

View File

@ -39,7 +39,7 @@ import kotlin.time.Duration.Companion.minutes
private const val THREAD_ID = 1L private const val THREAD_ID = 1L
private const val LOCAL_NUMBER = "05---local---address" private const val LOCAL_NUMBER = "05---local---address"
private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER) private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER)
private const val GROUP_NUMBER = "${GroupUtil.OPEN_GROUP_PREFIX}4133" private const val GROUP_NUMBER = "${GroupUtil.COMMUNITY_PREFIX}4133"
private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER) private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)

View File

@ -3,12 +3,14 @@ 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.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
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.notNullValue
import org.hamcrest.CoreMatchers.nullValue 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.BeforeClass
import org.junit.Test import org.junit.Test
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
@ -18,7 +20,9 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.NoOpLogger
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
@ -32,6 +36,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private val threadId = 123L private val threadId = 123L
private val edKeyPair = mock<KeyPair>() private val edKeyPair = mock<KeyPair>()
private lateinit var recipient: Recipient private lateinit var recipient: Recipient
private lateinit var messageRecord: MessageRecord
private val viewModel: ConversationViewModel by lazy { private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, repository, storage) ConversationViewModel(threadId, edKeyPair, repository, storage)
@ -40,6 +45,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Before @Before
fun setUp() { fun setUp() {
recipient = mock() recipient = mock()
messageRecord = mock { record ->
whenever(record.individualRecipient).thenReturn(recipient)
}
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient) whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow()) whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
} }
@ -144,7 +152,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
val error = Throwable() val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
viewModel.banAndDeleteAll(recipient) viewModel.banAndDeleteAll(messageRecord)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error")) assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
} }
@ -153,7 +161,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should emit a message on ban user and delete all success`() = runBlockingTest { fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
viewModel.banAndDeleteAll(recipient) viewModel.banAndDeleteAll(messageRecord)
assertThat( assertThat(
viewModel.uiState.first().uiMessages.first().message, viewModel.uiState.first().uiMessages.first().message,
@ -189,7 +197,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test @Test
fun `open group recipient should have no blinded recipient`() { fun `open group recipient should have no blinded recipient`() {
whenever(recipient.isOpenGroupRecipient).thenReturn(true) whenever(recipient.isCommunityRecipient).thenReturn(true)
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false) whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false) whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
assertThat(viewModel.blindedRecipient, nullValue()) assertThat(viewModel.blindedRecipient, nullValue())

View File

@ -77,7 +77,7 @@ class Contact(val sessionID: String) {
companion object { companion object {
fun contextForRecipient(recipient: Recipient): ContactContext { fun contextForRecipient(recipient: Recipient): ContactContext {
return if (recipient.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
} }
} }
} }

View File

@ -22,7 +22,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
override suspend fun execute(dispatcherName: String) { override suspend fun execute(dispatcherName: String) {
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val numberToDelete = messageServerIds.size val numberToDelete = messageServerIds.size
Log.d(TAG, "Deleting $numberToDelete messages") Log.d(TAG, "About to attempt to delete $numberToDelete messages")
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded) // FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
try { try {
@ -42,6 +42,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
delegate?.handleJobSucceeded(this, dispatcherName) delegate?.handleJobSucceeded(this, dispatcherName)
} }
catch (e: Exception) { catch (e: Exception) {
Log.w(TAG, "OpenGroupDeleteJob failed: $e")
delegate?.handleJobFailed(this, dispatcherName, e) delegate?.handleJobFailed(this, dispatcherName, e)
} }
} }

View File

@ -43,14 +43,14 @@ sealed class Destination {
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
ClosedGroup(groupPublicKey) ClosedGroup(groupPublicKey)
} }
address.isOpenGroup -> { address.isCommunity -> {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val threadID = storage.getThreadId(address)!! val threadID = storage.getThreadId(address)!!
storage.getOpenGroup(threadID)?.let { storage.getOpenGroup(threadID)?.let {
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds) OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
} ?: throw Exception("Missing open group for thread with ID: $threadID.") } ?: throw Exception("Missing open group for thread with ID: $threadID.")
} }
address.isOpenGroupInbox -> { address.isCommunityInbox -> {
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!") val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
OpenGroupInbox( OpenGroupInbox(
groupInboxId.dropLast(2).joinToString("!"), groupInboxId.dropLast(2).joinToString("!"),

View File

@ -602,8 +602,7 @@ object OpenGroupApi {
// region Message Deletion // region Message Deletion
@JvmStatic @JvmStatic
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> { fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
val request = val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
return send(request).map { return send(request).map {
Log.d("Loki", "Message deletion successful.") Log.d("Loki", "Message deletion successful.")
} }
@ -659,7 +658,9 @@ object OpenGroupApi {
} }
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val requests = mutableListOf<BatchRequestInfo<*>>( val requests = mutableListOf<BatchRequestInfo<*>>(
// Ban request
BatchRequestInfo( BatchRequestInfo(
request = BatchRequest( request = BatchRequest(
method = POST, method = POST,
@ -669,6 +670,7 @@ object OpenGroupApi {
endpoint = Endpoint.UserBan(publicKey), endpoint = Endpoint.UserBan(publicKey),
responseType = object: TypeReference<Any>(){} responseType = object: TypeReference<Any>(){}
), ),
// Delete request
BatchRequestInfo( BatchRequestInfo(
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"), request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
endpoint = Endpoint.RoomDeleteMessages(room, publicKey), endpoint = Endpoint.RoomDeleteMessages(room, publicKey),

View File

@ -22,17 +22,17 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
get() = GroupUtil.isEncodedGroup(address) get() = GroupUtil.isEncodedGroup(address)
val isClosedGroup: Boolean val isClosedGroup: Boolean
get() = GroupUtil.isClosedGroup(address) get() = GroupUtil.isClosedGroup(address)
val isOpenGroup: Boolean val isCommunity: Boolean
get() = GroupUtil.isOpenGroup(address) get() = GroupUtil.isCommunity(address)
val isOpenGroupInbox: Boolean val isCommunityInbox: Boolean
get() = GroupUtil.isOpenGroupInbox(address) get() = GroupUtil.isCommunityInbox(address)
val isOpenGroupOutbox: Boolean val isCommunityOutbox: Boolean
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value) get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
val isContact: Boolean val isContact: Boolean
get() = !(isGroup || isOpenGroupInbox) get() = !(isGroup || isCommunityInbox)
fun contactIdentifier(): String { fun contactIdentifier(): String {
if (!isContact && !isOpenGroup) { if (!isContact && !isCommunity) {
if (isGroup) throw AssertionError("Not e164, is group") if (isGroup) throw AssertionError("Not e164, is group")
throw AssertionError("Not e164, unknown") throw AssertionError("Not e164, unknown")
} }

View File

@ -22,7 +22,7 @@ class GroupRecord(
} }
val isOpenGroup: Boolean val isOpenGroup: Boolean
get() = Address.fromSerialized(encodedId).isOpenGroup get() = Address.fromSerialized(encodedId).isCommunity
val isClosedGroup: Boolean val isClosedGroup: Boolean
get() = Address.fromSerialized(encodedId).isClosedGroup get() = Address.fromSerialized(encodedId).isClosedGroup

View File

@ -8,12 +8,12 @@ import java.io.IOException
object GroupUtil { object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!" const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!" const val COMMUNITY_PREFIX = "__loki_public_chat_group__!"
const val OPEN_GROUP_INBOX_PREFIX = "__open_group_inbox__!" const val COMMUNITY_INBOX_PREFIX = "__open_group_inbox__!"
@JvmStatic @JvmStatic
fun getEncodedOpenGroupID(groupID: ByteArray): String { fun getEncodedOpenGroupID(groupID: ByteArray): String {
return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID) return COMMUNITY_PREFIX + Hex.toStringCondensed(groupID)
} }
@JvmStatic @JvmStatic
@ -25,7 +25,7 @@ object GroupUtil {
@JvmStatic @JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address { fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)) return Address.fromSerialized(COMMUNITY_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
} }
@JvmStatic @JvmStatic
@ -69,17 +69,17 @@ object GroupUtil {
} }
fun isEncodedGroup(groupId: String): Boolean { fun isEncodedGroup(groupId: String): Boolean {
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX) return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(COMMUNITY_PREFIX)
} }
@JvmStatic @JvmStatic
fun isOpenGroup(groupId: String): Boolean { fun isCommunity(groupId: String): Boolean {
return groupId.startsWith(OPEN_GROUP_PREFIX) return groupId.startsWith(COMMUNITY_PREFIX)
} }
@JvmStatic @JvmStatic
fun isOpenGroupInbox(groupId: String): Boolean { fun isCommunityInbox(groupId: String): Boolean {
return groupId.startsWith(OPEN_GROUP_INBOX_PREFIX) return groupId.startsWith(COMMUNITY_INBOX_PREFIX)
} }
@JvmStatic @JvmStatic

View File

@ -459,16 +459,16 @@ public class Recipient implements RecipientModifiedListener {
} }
public boolean is1on1() { return address.isContact() && !isLocalNumber; } public boolean is1on1() { return address.isContact() && !isLocalNumber; }
public boolean isOpenGroupRecipient() { public boolean isCommunityRecipient() {
return address.isOpenGroup(); return address.isCommunity();
} }
public boolean isOpenGroupOutboxRecipient() { public boolean isOpenGroupOutboxRecipient() {
return address.isOpenGroupOutbox(); return address.isCommunityOutbox();
} }
public boolean isOpenGroupInboxRecipient() { public boolean isOpenGroupInboxRecipient() {
return address.isOpenGroupInbox(); return address.isCommunityInbox();
} }
public boolean isClosedGroupRecipient() { public boolean isClosedGroupRecipient() {