From 206505abe8eb769107a00ef59705b603bf9ca28d Mon Sep 17 00:00:00 2001 From: ceokot Date: Fri, 4 Mar 2022 08:46:39 +0200 Subject: [PATCH] feat: Add conversation filtering for message requests (#830) * feat: Message requests * Apply contact sync message * Filter based on message requests toggle * Add message requests screen * Implement message requests screen * Handle message request buttons * Handle approval syncing * Display message request response * Display pending message request * Display pending message request * Add approval migrations * Send message request response * Fix conversation filters * Add approval migration * Handle message request response * Update message request response proto * Update message request response handling * Refresh message requests * Show message request banner on new message request * Message request item layout tweaks * Fix latest unapproved conversation query * Handle sent message request responses on restore * QA feedback tweaks * Remove send limit on message requests * Config message handling tweaks * Reverse conversation upon message request approval * Remove read receipts, delete declined conversations * Fix contact filtering in config messages * Fix message request order and handle deletion * Fix message request snippet on home screen * Refresh message request list after decline or clearing all * Fix message request reversal * Fix message request notifications * Disable media buttons for message requests * Hide message request banner after reading * Refresh message request banner --- app/src/main/AndroidManifest.xml | 5 + .../contacts/SelectContactsLoader.kt | 6 +- .../conversation/v2/ConversationActivityV2.kt | 64 +- .../conversation/v2/ConversationLoader.kt | 8 +- .../conversation/v2/ConversationViewModel.kt | 28 +- .../conversation/v2/input_bar/InputBar.kt | 10 + .../v2/input_bar/InputBarEditText.kt | 6 +- .../v2/messages/ControlMessageView.kt | 31 +- .../v2/utilities/MentionManagerUtilities.kt | 2 +- .../securesms/database/MmsDatabase.java | 26 +- .../securesms/database/MmsSmsColumns.java | 7 + .../securesms/database/MmsSmsDatabase.java | 15 +- .../securesms/database/RecipientDatabase.java | 37 +- .../securesms/database/Storage.kt | 82 +- .../securesms/database/ThreadDatabase.java | 95 +- .../database/helpers/SQLCipherOpenHelper.java | 13 +- .../database/model/DisplayRecord.java | 4 +- .../securesms/home/HomeActivity.kt | 68 +- .../thoughtcrime/securesms/home/HomeLoader.kt | 2 +- .../messagerequests/MessageRequestView.kt | 64 + .../MessageRequestsActivity.kt | 116 ++ .../messagerequests/MessageRequestsAdapter.kt | 94 ++ .../messagerequests/MessageRequestsLoader.kt | 13 + .../MessageRequestsViewModel.kt | 24 + .../notifications/DefaultMessageNotifier.java | 71 +- .../notifications/MarkReadReceiver.java | 3 +- .../securesms/preferences/SettingsActivity.kt | 7 + .../repository/ConversationRepository.kt | 63 +- .../service/ExpiringMessageManager.java | 1 + .../util/ConfigurationMessageUtilities.kt | 24 +- .../securesms/util/SessionMetaProtocol.kt | 4 +- ...ctive_outline_button_medium_background.xml | 11 + app/src/main/res/drawable/ic_delete_24.xml | 10 + .../ic_outline_message_requests_24.xml | 9 + .../res/layout/activity_conversation_v2.xml | 56 +- .../res/layout/activity_message_requests.xml | 63 + app/src/main/res/layout/activity_settings.xml | 16 + .../main/res/layout/view_control_message.xml | 4 +- .../main/res/layout/view_message_request.xml | 77 ++ .../layout/view_message_request_banner.xml | 81 ++ .../main/res/menu/menu_message_request.xml | 9 + app/src/main/res/values/strings.xml | 16 +- app/src/main/res/values/styles.xml | 6 + .../v2/ConversationViewModelTest.kt | 14 + .../MessageRequestsViewModelTest.kt | 34 + .../libsession/database/StorageProtocol.kt | 2 + .../messages/control/ConfigurationMessage.kt | 23 +- .../control/MessageRequestResponse.kt | 33 + .../messages/signal/IncomingMediaMessage.java | 9 +- .../sending_receiving/MessageReceiver.kt | 1 + .../ReceivedMessageHandler.kt | 28 +- .../utilities/TextSecurePreferences.kt | 21 + .../utilities/recipients/Recipient.java | 44 +- .../recipients/RecipientProvider.java | 4 + libsignal/protobuf/SignalService.proto | 9 + .../libsignal/protos/SignalServiceProtos.java | 1059 +++++++++++++++-- 56 files changed, 2427 insertions(+), 205 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt create mode 100644 app/src/main/res/drawable/destructive_outline_button_medium_background.xml create mode 100644 app/src/main/res/drawable/ic_delete_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_message_requests_24.xml create mode 100644 app/src/main/res/layout/activity_message_requests.xml create mode 100644 app/src/main/res/layout/view_message_request.xml create mode 100644 app/src/main/res/layout/view_message_request_banner.xml create mode 100644 app/src/main/res/menu/menu_message_request.xml create mode 100644 app/src/test/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModelTest.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f1b256749..54ec5ea7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -124,6 +124,11 @@ android:screenOrientation="portrait" android:launchMode="singleTask" android:theme="@style/Theme.Session.DayNight.NoActionBar" /> + ) : AsyncLoader>(context) { +class SelectContactsLoader(context: Context, private val usersToExclude: Set) : AsyncLoader>(context) { override fun loadInBackground(): List { val contacts = ContactUtilities.getAllContacts(context) - return contacts.filter { contact -> - !contact.isGroupRecipient && !usersToExclude.contains(contact.address.toString()) + return contacts.filter { + !it.isGroupRecipient && !usersToExclude.contains(it.address.toString()) && it.hasApprovedMe() }.map { it.address.toString() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 78aaa2145..a693e954f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -44,6 +44,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.annimon.stream.Stream import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding import network.loki.messenger.databinding.ActivityConversationV2Binding @@ -126,6 +128,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SaveAttachmentTask @@ -220,7 +223,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private val adapter by lazy { - val cursor = mmsSmsDb.getConversation(viewModel.threadId) + val cursor = mmsSmsDb.getConversation(viewModel.threadId, !isIncomingMessageRequestThread()) val adapter = ConversationAdapter( this, cursor, @@ -310,6 +313,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe setUpSearchResultObserver() scrollToFirstUnreadMessageIfNeeded() showOrHideInputIfNeeded() + setUpMessageRequestsBar() if (viewModel.recipient.isOpenGroupRecipient) { val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) if (openGroup == null) { @@ -349,13 +353,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // called from onCreate private fun setUpRecyclerView() { binding!!.conversationRecyclerView.adapter = adapter - val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) + val reverseLayout = !isIncomingMessageRequestThread() + val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, reverseLayout) binding!!.conversationRecyclerView.layoutManager = layoutManager // Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will) LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { - return ConversationLoader(viewModel.threadId, this@ConversationActivityV2) + return ConversationLoader(viewModel.threadId, reverseLayout, this@ConversationActivityV2) } override fun onLoadFinished(loader: Loader, cursor: Cursor?) { @@ -539,6 +544,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe viewModel.messageShown(it.id) } addOpenGroupGuidelinesIfNeeded(uiState.isOxenHostedOpenGroup) + if (uiState.isMessageRequestAccepted == true) { + binding?.messageRequestBar?.visibility = View.GONE + } } } } @@ -551,7 +559,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun onPrepareOptionsMenu(menu: Menu): Boolean { - ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, viewModel.recipient, viewModel.threadId, this) { onOptionsItemSelected(it) } + if (!isMessageRequestThread()) { + ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, viewModel.recipient, viewModel.threadId, this) { onOptionsItemSelected(it) } + } super.onPrepareOptionsMenu(menu) return true } @@ -587,6 +597,49 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } + private fun setUpMessageRequestsBar() { + binding?.inputBar?.showMediaControls = !isOutgoingMessageRequestThread() + binding?.messageRequestBar?.isVisible = isIncomingMessageRequestThread() + binding?.acceptMessageRequestButton?.setOnClickListener { + acceptMessageRequest() + } + binding?.declineMessageRequestButton?.setOnClickListener { + viewModel.declineMessageRequest() + lifecycleScope.launch(Dispatchers.IO) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2) + } + finish() + } + } + + private fun acceptMessageRequest() { + binding?.messageRequestBar?.isVisible = false + binding?.conversationRecyclerView?.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) + viewModel.acceptMessageRequest() + lifecycleScope.launch(Dispatchers.IO) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2) + } + } + + private fun isMessageRequestThread(): Boolean { + val hasSent = threadDb.getLastSeenAndHasSent(viewModel.threadId).second() + return (!viewModel.recipient.isGroupRecipient && !hasSent) || + (!viewModel.recipient.isGroupRecipient && hasSent && !(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived())) + } + + private fun isOutgoingMessageRequestThread(): Boolean { + return !viewModel.recipient.isGroupRecipient && + !(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived()) + } + + private fun isIncomingMessageRequestThread(): Boolean { + return !viewModel.recipient.isGroupRecipient && + !viewModel.recipient.isApproved && + !threadDb.getLastSeenAndHasSent(viewModel.threadId).second() && + threadDb.getMessageCount(viewModel.threadId) > 0 + } + override fun inputBarEditTextContentChanged(newContent: CharSequence) { val inputBarText = binding?.inputBar?.text ?: return // TODO check if we should be referencing newContent here instead if (textSecurePreferences.isLinkPreviewsEnabled()) { @@ -946,6 +999,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun sendMessage() { + if (isIncomingMessageRequestThread()) { + acceptMessageRequest() + } if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) { BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog") return diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt index a78b9b3a8..4692bf786 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt @@ -5,9 +5,13 @@ import android.database.Cursor import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.AbstractCursorLoader -class ConversationLoader(private val threadID: Long, context: Context) : AbstractCursorLoader(context) { +class ConversationLoader( + private val threadID: Long, + private val reverse: Boolean, + context: Context +) : AbstractCursorLoader(context) { override fun getCursor(): Cursor { - return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID) + return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 852d0a998..dbe35ee96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -22,9 +22,8 @@ class ConversationViewModel( private val _uiState = MutableStateFlow(ConversationUiState()) val uiState: StateFlow = _uiState - val recipient: Recipient by lazy { - repository.getRecipientForThreadId(threadId) - } + val recipient: Recipient + get() = repository.getRecipientForThreadId(threadId) init { _uiState.update { @@ -88,6 +87,22 @@ class ConversationViewModel( } } + fun acceptMessageRequest() = viewModelScope.launch { + repository.acceptMessageRequest(threadId, recipient) + .onSuccess { + _uiState.update { + it.copy(isMessageRequestAccepted = true) + } + } + .onFailure { + showMessage("Couldn't accept message request due to error: $it") + } + } + + fun declineMessageRequest() { + repository.declineMessageRequest(threadId, recipient) + } + private fun showMessage(message: String) { _uiState.update { currentUiState -> val messages = currentUiState.uiMessages + UiMessage( @@ -105,6 +120,10 @@ class ConversationViewModel( } } + fun hasReceived(): Boolean { + return repository.hasReceived(threadId) + } + @dagger.assisted.AssistedFactory interface AssistedFactory { fun create(threadId: Long): Factory @@ -126,5 +145,6 @@ data class UiMessage(val id: Long, val message: String) data class ConversationUiState( val isOxenHostedOpenGroup: Boolean = false, - val uiMessages: List = emptyList() + val uiMessages: List = emptyList(), + val isMessageRequestAccepted: Boolean? = null ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index cd18725e0..260b5d265 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -37,6 +37,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li var linkPreview: LinkPreview? = null var showInput: Boolean = true set(value) { field = value; showOrHideInputIfNeeded() } + var showMediaControls: Boolean = true + set(value) { + field = value + showOrHideMediaControlsIfNeeded() + binding.inputBarEditText.showMediaControls = value + } var text: String get() { return binding.inputBarEditText.text?.toString() ?: "" } @@ -162,6 +168,10 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } } + private fun showOrHideMediaControlsIfNeeded() { + setOf(attachmentsButton, microphoneButton).forEach { it.snIsEnabled = showMediaControls } + } + fun addTextChangedListener(textWatcher: TextWatcher) { binding.inputBarEditText.addTextChangedListener(textWatcher) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt index 90afabb80..a42222f41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt @@ -21,6 +21,8 @@ class InputBarEditText : AppCompatEditText { private val screenWidth get() = Resources.getSystem().displayMetrics.widthPixels var delegate: InputBarEditTextDelegate? = null + var showMediaControls: Boolean = true + private val snMinHeight = toPx(40.0f, resources) private val snMaxHeight = toPx(80.0f, resources) @@ -47,7 +49,9 @@ class InputBarEditText : AppCompatEditText { override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { val ic = super.onCreateInputConnection(editorInfo) ?: return null - EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg")) + EditorInfoCompat.setContentMimeTypes(editorInfo, + if (showMediaControls) arrayOf("image/png", "image/gif", "image/jpg") else null + ) val callback = InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 73e5ac338..e4247b86f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -29,19 +29,26 @@ class ControlMessageView : LinearLayout { // region Updating fun bind(message: MessageRecord, previous: MessageRecord?) { binding.dateBreakTextView.showDateBreak(message, previous) - binding.iconImageView.visibility = View.GONE - if (message.isExpirationTimerUpdate) { - binding.iconImageView.setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme) - ) - binding.iconImageView.visibility = View.VISIBLE - } else if (message.isMediaSavedNotification) { - binding.iconImageView.setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme) - ) - binding.iconImageView.visibility = View.VISIBLE + var messageBody: CharSequence = message.getDisplayBody(context) + when { + message.isExpirationTimerUpdate -> { + binding.iconImageView.setImageDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme) + ) + binding.iconImageView.visibility = View.VISIBLE + } + message.isMediaSavedNotification -> { + binding.iconImageView.setImageDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme) + ) + binding.iconImageView.visibility = View.VISIBLE + } + message.isMessageRequestResponse -> { + messageBody = context.getString(R.string.message_requests_accepted) + } } - binding.textView.text = message.getDisplayBody(context) + + binding.textView.text = messageBody } fun recycle() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt index 8dc9d2b5d..48ce85de1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt @@ -19,7 +19,7 @@ object MentionManagerUtilities { result.addAll(members) } else { val messageDatabase = DatabaseComponent.get(context).mmsSmsDatabase() - val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID, 0, 200)) + val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID, true, 0, 200)) var record: MessageRecord? = reader.next while (record != null) { result.add(record.individualRecipient.address.serialize()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 31ecbcb99..9d200971c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -178,6 +178,11 @@ public class MmsDatabase extends MessagingDatabase { private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); + public static String getCreateMessageRequestResponseCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + MESSAGE_REQUEST_RESPONSE + " INTEGER DEFAULT 0;"; + } + public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } @@ -664,6 +669,7 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(EXPIRES_IN, retrieved.getExpiresIn()); contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0); contentValues.put(UNIDENTIFIED, retrieved.isUnidentified()); + contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse()); if (!contentValues.containsKey(DATE_SENT)) { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); @@ -680,7 +686,8 @@ public class MmsDatabase extends MessagingDatabase { quoteAttachments = retrieved.getQuote().getAttachments(); } - if (retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) { + if ((retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) || + retrieved.isMessageRequestResponse() && isDuplicateMessageRequestResponse(retrieved, threadId)) { Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")"); return Optional.absent(); } @@ -750,6 +757,10 @@ public class MmsDatabase extends MessagingDatabase { type |= Types.MEDIA_SAVED_EXTRACTION_BIT; } + if (retrieved.isMessageRequestResponse()) { + type |= Types.MESSAGE_REQUEST_RESPONSE_BIT; + } + return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp); } @@ -1000,6 +1011,19 @@ public class MmsDatabase extends MessagingDatabase { return linkPreviewJson.toString(); } + private boolean isDuplicateMessageRequestResponse(IncomingMediaMessage message, long threadId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = database.query(TABLE_NAME, null, MESSAGE_REQUEST_RESPONSE + " = 1 AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", + new String[]{message.getFrom().serialize(), String.valueOf(threadId)}, + null, null, null, "1"); + + try { + return cursor != null && cursor.moveToFirst(); + } finally { + if (cursor != null) cursor.close(); + } + } + private boolean isDuplicate(IncomingMediaMessage message, long threadId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 52642b5d1..92f9e970d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -20,6 +20,7 @@ public interface MmsSmsColumns { public static final String EXPIRE_STARTED = "expire_started"; public static final String NOTIFIED = "notified"; public static final String UNIDENTIFIED = "unidentified"; + public static final String MESSAGE_REQUEST_RESPONSE = "message_request_response"; public static class Types { protected static final long TOTAL_MASK = 0xFFFFFFFF; @@ -97,6 +98,8 @@ public interface MmsSmsColumns { protected static final long ENCRYPTION_LOKI_SESSION_RESTORE_SENT_BIT = 0x01000000; protected static final long ENCRYPTION_LOKI_SESSION_RESTORE_DONE_BIT = 0x00100000; + protected static final long MESSAGE_REQUEST_RESPONSE_BIT = 0x010000; + public static boolean isDraftMessageType(long type) { return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE; } @@ -274,6 +277,10 @@ public interface MmsSmsColumns { (type & ENCRYPTION_REMOTE_BIT) != 0; } + public static boolean isMessageRequestResponse(long type) { + return (type & MESSAGE_REQUEST_RESPONSE_BIT) != 0; + } + public static long translateFromSystemBaseType(long theirType) { switch ((int)theirType) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index f9d524010..a0d8715fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; +import java.io.Closeable; import java.util.HashSet; import java.util.Set; @@ -111,8 +112,8 @@ public class MmsSmsDatabase extends Database { return getMessageFor(timestamp, author.serialize()); } - public Cursor getConversation(long threadId, long offset, long limit) { - String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC"); String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null; @@ -122,8 +123,8 @@ public class MmsSmsDatabase extends Database { return cursor; } - public Cursor getConversation(long threadId) { - return getConversation(threadId, 0, 0); + public Cursor getConversation(long threadId, boolean reverse) { + return getConversation(threadId, reverse, 0, 0); } public Cursor getConversationSnippet(long threadId) { @@ -406,7 +407,7 @@ public class MmsSmsDatabase extends Database { return new Reader(cursor); } - public class Reader { + public class Reader implements Closeable { private final Cursor cursor; private SmsDatabase.Reader smsReader; @@ -448,7 +449,9 @@ public class MmsSmsDatabase extends Database { } public void close() { - cursor.close(); + if (cursor != null) { + cursor.close(); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index e1a3383d9..70db93a79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.database; +import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -34,7 +36,9 @@ public class RecipientDatabase extends Database { static final String TABLE_NAME = "recipient_preferences"; private static final String ID = "_id"; public static final String ADDRESS = "recipient_ids"; - private static final String BLOCK = "block"; + static final String BLOCK = "block"; + static final String APPROVED = "approved"; + private static final String APPROVED_ME = "approved_me"; private static final String NOTIFICATION = "notification"; private static final String VIBRATE = "vibrate"; private static final String MUTE_UNTIL = "mute_until"; @@ -59,7 +63,7 @@ public class RecipientDatabase extends Database { private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none private static final String[] RECIPIENT_PROJECTION = new String[] { - BLOCK, 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, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, UNIDENTIFIED_ACCESS_MODE, @@ -102,6 +106,22 @@ public class RecipientDatabase extends Database { "ADD COLUMN " + NOTIFY_TYPE + " INTEGER DEFAULT 0;"; } + public static String getCreateApprovedCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + APPROVED + " INTEGER DEFAULT 0;"; + } + + public static String getCreateApprovedMeCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + APPROVED_ME + " INTEGER DEFAULT 0;"; + } + + public static String getUpdateApprovedCommand() { + return "UPDATE "+ TABLE_NAME + " " + + "SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " + + "WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'"; + } + public static final int NOTIFY_TYPE_ALL = 0; public static final int NOTIFY_TYPE_MENTIONS = 1; public static final int NOTIFY_TYPE_NONE = 2; @@ -137,6 +157,8 @@ public class RecipientDatabase extends Database { Optional getRecipientSettings(@NonNull Cursor cursor) { boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; + boolean approved = cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1; + boolean approvedMe = cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED_ME)) == 1; String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE)); int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); @@ -178,7 +200,7 @@ public class RecipientDatabase extends Database { } } - return Optional.of(new RecipientSettings(blocked, muteUntil, + return Optional.of(new RecipientSettings(blocked, approved, approvedMe, muteUntil, notifyType, Recipient.VibrateState.fromId(messageVibrateState), Recipient.VibrateState.fromId(callVibrateState), @@ -213,6 +235,15 @@ public class RecipientDatabase extends Database { recipient.resolve().setForceSmsSelection(forceSmsSelection); } + public void setApproved(@NonNull Recipient recipient, boolean approved) { + ContentValues values = new ContentValues(); + values.put(APPROVED, approved ? 1 : 0); + values.put(APPROVED_ME, approved ? 1 : 0); + updateOrInsert(recipient.getAddress(), values); + recipient.resolve().setApproved(approved); + recipient.resolve().setHasApprovedMe(approved); + } + public void setBlocked(@NonNull Recipient recipient, boolean blocked) { ContentValues values = new ContentValues(); values.put(BLOCK, blocked ? 1 : 0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 1dfc1f0b2..d6b5e824a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -6,6 +6,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.* import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.signal.* import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.visible.Attachment @@ -25,6 +26,7 @@ import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -581,7 +583,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, recipientDatabase.setProfileSharing(recipient, true) recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED) // create Thread if needed - threadDatabase.getOrCreateThreadIdFor(recipient) + val threadId = threadDatabase.getOrCreateThreadIdFor(recipient) + if (contact.didApproveMe == true) { + recipientDatabase.setApproved(recipient, true) + threadDatabase.setHasSent(threadId, true) + } + if (contact.isApproved == true) { + recipientDatabase.setApproved(recipient, true) + threadDatabase.setHasSent(threadId, true) + } + if (contact.isBlocked == true) { + recipientDatabase.setBlocked(recipient, true) + threadDatabase.deleteConversation(threadId) + } } if (contacts.isNotEmpty()) { threadDatabase.notifyConversationListListeners() @@ -613,17 +627,63 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, if (recipient.isBlocked) return - val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1, - 0, false, - false, - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.of(message)) + val mediaMessage = IncomingMediaMessage( + address, + sentTimestamp, + -1, + 0, + false, + false, + false, + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.of(message) + ) database.insertSecureDecryptedMessageInbox(mediaMessage, -1) } + + override fun insertMessageRequestResponse(response: MessageRequestResponse) { + val userPublicKey = getUserPublicKey() + val senderPublicKey = response.sender!! + val recipientPublicKey = response.recipient!! + if (userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)) return + val recipientDb = DatabaseComponent.get(context).recipientDatabase() + val threadDB = DatabaseComponent.get(context).threadDatabase() + if (userPublicKey == senderPublicKey) { + val requestRecipient = Recipient.from(context, fromSerialized(recipientPublicKey), false) + recipientDb.setApproved(requestRecipient, true) + val threadId = threadDB.getOrCreateThreadIdFor(requestRecipient) + threadDB.setHasSent(threadId, true) + } else { + val mmsDb = DatabaseComponent.get(context).mmsDatabase() + val senderAddress = fromSerialized(senderPublicKey) + val requestSender = Recipient.from(context, senderAddress, false) + recipientDb.setApproved(requestSender, true) + + val message = IncomingMediaMessage( + senderAddress, + response.sentTimestamp!!, + -1, + 0, + false, + false, + true, + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent() + ) + val threadId = getOrCreateThreadIdFor(senderAddress) + mmsDb.insertSecureDecryptedMessageInbox(message, threadId) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 84c7de34e..2b0b23167 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -253,7 +253,7 @@ public class ThreadDatabase extends Database { Cursor cursor = null; try { - cursor = DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadId); + cursor = DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadId, true); if (cursor != null && length > 0 && cursor.getCount() > length) { Log.w("ThreadDatabase", "Cursor count is greater than length!"); @@ -388,20 +388,88 @@ public class ThreadDatabase extends Database { return db.rawQuery(query, null); } + public int getUnapprovedConversationCount() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + + try { + String query = "SELECT COUNT (*) FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + + " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + MESSAGE_COUNT + " = " + UNREAD_COUNT + " AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL"; + cursor = db.rawQuery(query, null); + + if (cursor != null && cursor.moveToFirst()) + return cursor.getInt(0); + } finally { + if (cursor != null) + cursor.close(); + } + + return 0; + } + + public long getLatestUnapprovedConversationTimestamp() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + + try { + String where = "SELECT " + DATE + " FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + + " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + DATE + " DESC LIMIT 1"; + cursor = db.rawQuery(where, null); + + if (cursor != null && cursor.moveToFirst()) + return cursor.getLong(0); + } finally { + if (cursor != null) + cursor.close(); + } + + return 0; + } + public Cursor getConversationList() { - return getConversationList("0"); + String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + + "AND " + ARCHIVED + " = 0 "; + return getConversationList(where); + } + + public Cursor getApprovedConversationList() { + String where = "((" + MESSAGE_COUNT + " != 0 AND (" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1)) OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + + "AND " + ARCHIVED + " = 0 "; + return getConversationList(where); + } + + public Cursor getUnapprovedConversationList() { + String where = MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL"; + return getConversationList(where); } public Cursor getArchivedConversationList() { - return getConversationList("1"); + String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + + "AND " + ARCHIVED + " = 1 "; + return getConversationList(where); } - private Cursor getConversationList(String archived) { + private Cursor getConversationList(String where) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " + - "AND " + ARCHIVED + " = ?"; String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{archived}); + Cursor cursor = db.rawQuery(query, null); setNotifyConverationListListeners(cursor); @@ -454,6 +522,19 @@ public class ThreadDatabase extends Database { } } + public int getMessageCount(long threadId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String[] columns = new String[]{MESSAGE_COUNT}; + String[] args = new String[]{String.valueOf(threadId)}; + try (Cursor cursor = db.query(TABLE_NAME, columns, ID_WHERE, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + + return 0; + } + } + public void deleteConversation(long threadId) { DatabaseComponent.get(context).smsDatabase().deleteThread(threadId); DatabaseComponent.get(context).mmsDatabase().deleteThread(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index b0a02041a..3afcd66b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -62,9 +62,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV28 = 49; private static final int lokiV29 = 50; private static final int lokiV30 = 51; + private static final int lokiV31 = 52; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV30; + private static final int DATABASE_VERSION = lokiV31; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -138,6 +139,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand()); db.execSQL(ThreadDatabase.getCreatePinnedCommand()); db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); + db.execSQL(RecipientDatabase.getCreateApprovedCommand()); + db.execSQL(RecipientDatabase.getCreateApprovedMeCommand()); + db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -320,6 +324,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); } + if (oldVersion < lokiV31) { + db.execSQL(RecipientDatabase.getCreateApprovedCommand()); + db.execSQL(RecipientDatabase.getCreateApprovedMeCommand()); + db.execSQL(RecipientDatabase.getUpdateApprovedCommand()); + db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 10e4cb753..141c77791 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -118,8 +118,10 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isMissedCall(type); } public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); } + public boolean isMessageRequestResponse() { return MmsSmsColumns.Types.isMessageRequestResponse(type); } public boolean isControlMessage() { - return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification(); + return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification() + || isMessageRequestResponse(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index f6508b766..0522dbcac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.lifecycleScope import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect @@ -26,6 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding +import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -58,18 +60,21 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel +import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.IOException +import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -93,10 +98,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), private val globalSearchViewModel by viewModels() private val publicKey: String - get() = TextSecurePreferences.getLocalNumber(this)!! + get() = textSecurePreferences.getLocalNumber()!! private val homeAdapter: HomeAdapter by lazy { - HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this) + HomeAdapter(context = this, cursor = threadDb.approvedConversationList, listener = this) } private val globalSearchAdapter = GlobalSearchAdapter { model -> @@ -157,7 +162,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } binding.sessionToolbar.disableClipping() // Set up seed reminder view - val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) + val hasViewedSeed = textSecurePreferences.getHasViewedSeed() if (!hasViewedSeed) { binding.seedReminderView.isVisible = true binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated @@ -167,6 +172,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else { binding.seedReminderView.isVisible = false } + setupMessageRequestsBanner() setupHeaderImage() // Set up recycler view binding.globalSearchInputLayout.listener = this @@ -208,8 +214,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // Set up remaining components if needed val application = ApplicationContext.getInstance(this@HomeActivity) application.registerForFCMIfNeeded(false) - val userPublicKey = TextSecurePreferences.getLocalNumber(this@HomeActivity) - if (userPublicKey != null) { + if (textSecurePreferences.getLocalNumber() != null) { OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() } @@ -293,12 +298,35 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.newConversationButtonSet.isVisible = !isShown } + private fun setupMessageRequestsBanner() { + val messageRequestCount = threadDb.unapprovedConversationCount + // Set up message requests + if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) { + with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) { + unreadCountTextView.text = messageRequestCount.toString() + timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString( + this@HomeActivity, + Locale.getDefault(), + threadDb.latestUnapprovedConversationTimestamp + ) + root.setOnClickListener { showMessageRequests() } + root.setOnLongClickListener { hideMessageRequests(); true } + root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + homeAdapter.headerView = root + homeAdapter.notifyItemChanged(0) + } + } else { + homeAdapter.headerView = null + } + } + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { return HomeLoader(this@HomeActivity) } override fun onLoadFinished(loader: Loader, cursor: Cursor?) { homeAdapter.changeCursor(cursor) + setupMessageRequestsBanner() updateEmptyState() } @@ -309,15 +337,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), override fun onResume() { super.onResume() ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) - if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared + if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared IdentityKeyUtil.checkUpdate(this) binding.profileButton.recycle() // clear cached image before update tje profilePictureView binding.profileButton.update() - val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) - if (hasViewedSeed) { + if (textSecurePreferences.getHasViewedSeed()) { binding.seedReminderView.isVisible = false } - if (TextSecurePreferences.getConfigurationMessageSynced(this)) { + if (textSecurePreferences.getConfigurationMessageSynced()) { lifecycleScope.launch(Dispatchers.IO) { ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) } @@ -361,7 +388,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), private fun updateProfileButton() { binding.profileButton.publicKey = publicKey - binding.profileButton.displayName = TextSecurePreferences.getProfileName(this) + binding.profileButton.displayName = textSecurePreferences.getProfileName() binding.profileButton.recycle() binding.profileButton.update() } @@ -522,7 +549,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val recipient = thread.recipient val message = if (recipient.isGroupRecipient) { val group = groupDatabase.getGroup(recipient.address.toString()).orNull() - if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { + if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) { "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." } else { resources.getString(R.string.activity_home_leave_group_dialog_message) @@ -584,6 +611,25 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), show(intent) } + private fun showMessageRequests() { + val intent = Intent(this, MessageRequestsActivity::class.java) + push(intent) + } + + private fun hideMessageRequests() { + AlertDialog.Builder(this) + .setMessage("Hide message requests?") + .setPositiveButton(R.string.yes) { _, _ -> + textSecurePreferences.setHasHiddenMessageRequests() + setupMessageRequestsBanner() + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + .setNegativeButton(R.string.no) { _, _ -> + // Do nothing + } + .create().show() + } + override fun createNewPrivateChat() { val intent = Intent(this, CreatePrivateChatActivity::class.java) show(intent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt index 921ecfffa..a748189b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt @@ -8,6 +8,6 @@ import org.thoughtcrime.securesms.util.AbstractCursorLoader class HomeLoader(context: Context) : AbstractCursorLoader(context) { override fun getCursor(): Cursor { - return DatabaseComponent.get(context).threadDatabase().conversationList + return DatabaseComponent.get(context).threadDatabase().approvedConversationList } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt new file mode 100644 index 000000000..d8d12938f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.messagerequests + +import android.content.Context +import android.content.res.Resources +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import network.loki.messenger.R +import network.loki.messenger.databinding.ViewMessageRequestBinding +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.DateUtils +import java.util.Locale + +class MessageRequestView : LinearLayout { + private lateinit var binding: ViewMessageRequestBinding + private val screenWidth = Resources.getSystem().displayMetrics.widthPixels + var thread: ThreadRecord? = null + + // region Lifecycle + constructor(context: Context) : super(context) { initialize() } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } + + private fun initialize() { + binding = ViewMessageRequestBinding.inflate(LayoutInflater.from(context), this, true) + layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) + } + // endregion + + // region Updating + fun bind(thread: ThreadRecord, glide: GlideRequests) { + this.thread = thread + binding.profilePictureView.glide = glide + val senderDisplayName = getUserDisplayName(thread.recipient) + ?: thread.recipient.address.toString() + binding.displayNameTextView.text = senderDisplayName + binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) + val rawSnippet = thread.getDisplayBody(context) + val snippet = highlightMentions(rawSnippet, thread.threadId, context) + binding.snippetTextView.text = snippet + + post { + binding.profilePictureView.update(thread.recipient) + } + } + + fun recycle() { + binding.profilePictureView.recycle() + } + + private fun getUserDisplayName(recipient: Recipient): String? { + return if (recipient.isLocalNumber) { + context.getString(R.string.note_to_self) + } else { + recipient.name // Internally uses the Contact API + } + } + // endregion +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt new file mode 100644 index 000000000..6881e8e8a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.messagerequests + +import android.app.AlertDialog +import android.content.Intent +import android.database.Cursor +import android.os.Bundle +import androidx.activity.viewModels +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityMessageRequestsBinding +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.push +import javax.inject.Inject + +@AndroidEntryPoint +class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, LoaderManager.LoaderCallbacks { + + private lateinit var binding: ActivityMessageRequestsBinding + private lateinit var glide: GlideRequests + + @Inject lateinit var threadDb: ThreadDatabase + + private val viewModel: MessageRequestsViewModel by viewModels() + + private val adapter: MessageRequestsAdapter by lazy { + MessageRequestsAdapter(context = this, cursor = threadDb.unapprovedConversationList, listener = this) + } + + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + super.onCreate(savedInstanceState, ready) + binding = ActivityMessageRequestsBinding.inflate(layoutInflater) + setContentView(binding.root) + + glide = GlideApp.with(this) + + adapter.setHasStableIds(true) + adapter.glide = glide + binding.recyclerView.adapter = adapter + + binding.clearAllMessageRequestsButton.setOnClickListener { deleteAllAndBlock() } + } + + override fun onResume() { + super.onResume() + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { + return MessageRequestsLoader(this@MessageRequestsActivity) + } + + override fun onLoadFinished(loader: Loader, cursor: Cursor?) { + adapter.changeCursor(cursor) + updateEmptyState() + } + + override fun onLoaderReset(cursor: Loader) { + adapter.changeCursor(null) + } + + override fun onConversationClick(thread: ThreadRecord) { + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) + push(intent) + } + + override fun onLongConversationClick(thread: ThreadRecord) { + val dialog = AlertDialog.Builder(this) + dialog.setMessage(resources.getString(R.string.message_requests_delete_message)) + dialog.setPositiveButton(R.string.yes) { _, _ -> + viewModel.deleteMessageRequest(thread) + LoaderManager.getInstance(this).restartLoader(0, null, this) + lifecycleScope.launch(Dispatchers.IO) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) + } + } + dialog.setNegativeButton(R.string.no) { _, _ -> + // Do nothing + } + dialog.create().show() + } + + private fun updateEmptyState() { + val threadCount = (binding.recyclerView.adapter as MessageRequestsAdapter).itemCount + binding.emptyStateContainer.isVisible = threadCount == 0 + binding.clearAllMessageRequestsButton.isVisible = threadCount != 0 + } + + private fun deleteAllAndBlock() { + val dialog = AlertDialog.Builder(this) + dialog.setMessage(resources.getString(R.string.message_requests_clear_all_message)) + dialog.setPositiveButton(R.string.yes) { _, _ -> + viewModel.clearAllMessageRequests() + LoaderManager.getInstance(this).restartLoader(0, null, this) + lifecycleScope.launch(Dispatchers.IO) { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) + } + } + dialog.setNegativeButton(R.string.no) { _, _ -> + // Do nothing + } + dialog.create().show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt new file mode 100644 index 000000000..17f4440e4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt @@ -0,0 +1,94 @@ +package org.thoughtcrime.securesms.messagerequests + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.os.Build +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.Log +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.recyclerview.widget.RecyclerView +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.mms.GlideRequests + + +class MessageRequestsAdapter( + context: Context, + cursor: Cursor?, + val listener: ConversationClickListener +) : CursorRecyclerViewAdapter(context, cursor) { + private val threadDatabase = DatabaseComponent.get(context).threadDatabase() + lateinit var glide: GlideRequests + + class ViewHolder(val view: MessageRequestView) : RecyclerView.ViewHolder(view) + + override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = MessageRequestView(context) + view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } } + view.setOnLongClickListener { + view.thread?.let { showPopupMenu(view) } + true + } + return ViewHolder(view) + } + + override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) { + val thread = getThread(cursor)!! + viewHolder.view.bind(thread, glide) + } + + override fun onItemViewRecycled(holder: ViewHolder?) { + super.onItemViewRecycled(holder) + holder?.view?.recycle() + } + + private fun showPopupMenu(view: MessageRequestView) { + val popupMenu = PopupMenu(context, view) + popupMenu.menuInflater.inflate(R.menu.menu_message_request, popupMenu.menu) + popupMenu.setOnMenuItemClickListener { menuItem -> + if (menuItem.itemId == R.id.menu_delete_message_request) { + listener.onLongConversationClick(view.thread!!) + } + true + } + for (i in 0 until popupMenu.menu.size()) { + val item = popupMenu.menu.getItem(i) + val s = SpannableString(item.title) + s.setSpan(ForegroundColorSpan(context.getColor(R.color.destructive)), 0, s.length, 0) + item.title = s + } + popupMenu.forceShowIcon() //TODO: call setForceShowIcon(true) after update to appcompat 1.4.1+ + popupMenu.show() + } + + private fun getThread(cursor: Cursor): ThreadRecord? { + return threadDatabase.readerFor(cursor).current + } +} + +interface ConversationClickListener { + fun onConversationClick(thread: ThreadRecord) + fun onLongConversationClick(thread: ThreadRecord) +} + +@SuppressLint("PrivateApi") +private fun PopupMenu.forceShowIcon() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.setForceShowIcon(true) + } else { + try { + val popupField = PopupMenu::class.java.getDeclaredField("mPopup") + popupField.isAccessible = true + val menu = popupField.get(this) + menu.javaClass.getDeclaredMethod("setForceShowIcon", Boolean::class.java) + .invoke(menu, true) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't show message request popupmenu due to error: $exception.") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt new file mode 100644 index 000000000..16d83be6d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsLoader.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.messagerequests + +import android.content.Context +import android.database.Cursor +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.util.AbstractCursorLoader + +class MessageRequestsLoader(context: Context) : AbstractCursorLoader(context) { + + override fun getCursor(): Cursor { + return DatabaseComponent.get(context).threadDatabase().unapprovedConversationList + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt new file mode 100644 index 000000000..c07fd5ce6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.messagerequests + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.repository.ConversationRepository +import javax.inject.Inject + +@HiltViewModel +class MessageRequestsViewModel @Inject constructor( + private val repository: ConversationRepository +) : ViewModel() { + + fun deleteMessageRequest(thread: ThreadRecord) = viewModelScope.launch { + repository.deleteMessageRequest(thread) + } + + fun clearAllMessageRequests() = viewModelScope.launch { + repository.clearAllMessageRequests() + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index eeffdd834..efe75fef8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -171,35 +171,33 @@ public class DefaultMessageNotifier implements MessageNotifier { } private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { - if (Build.VERSION.SDK_INT >= 23) { - try { - NotificationManager notifications = ServiceUtil.getNotificationManager(context); - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + try { + NotificationManager notifications = ServiceUtil.getNotificationManager(context); + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - for (StatusBarNotification notification : activeNotifications) { - boolean validNotification = false; + for (StatusBarNotification notification : activeNotifications) { + boolean validNotification = false; - if (notification.getId() != SUMMARY_NOTIFICATION_ID && - notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && - notification.getId() != FOREGROUND_ID && - notification.getId() != PENDING_MESSAGES_ID) - { - for (NotificationItem item : notificationState.getNotifications()) { - if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) { - validNotification = true; - break; - } - } - - if (!validNotification) { - notifications.cancel(notification.getId()); + if (notification.getId() != SUMMARY_NOTIFICATION_ID && + notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && + notification.getId() != FOREGROUND_ID && + notification.getId() != PENDING_MESSAGES_ID) + { + for (NotificationItem item : notificationState.getNotifications()) { + if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) { + validNotification = true; + break; } } + + if (!validNotification) { + notifications.cancel(notification.getId()); + } } - } catch (Throwable e) { - // XXX Android ROM Bug, see #6043 - Log.w(TAG, e); } + } catch (Throwable e) { + // XXX Android ROM Bug, see #6043 + Log.w(TAG, e); } } @@ -229,15 +227,19 @@ public class DefaultMessageNotifier implements MessageNotifier { boolean isVisible = visibleThread == threadId; ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase(); - Recipient recipients = threads.getRecipientForThreadId(threadId); + Recipient recipient = threads.getRecipientForThreadId(threadId); - if (isVisible && recipients != null) { + if (!recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 && + !(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) { + TextSecurePreferences.removeHasHiddenMessageRequests(context); + } + if (isVisible && recipient != null) { List messageIds = threads.setRead(threadId, false); - if (SessionMetaProtocol.shouldSendReadReceipt(recipients.getAddress())) { MarkReadReceiver.process(context, messageIds); } + if (SessionMetaProtocol.shouldSendReadReceipt(recipient)) { MarkReadReceiver.process(context, messageIds); } } if (!TextSecurePreferences.isNotificationsEnabled(context) || - (recipients != null && recipients.isMuted())) + (recipient != null && recipient.isMuted())) { return; } @@ -484,7 +486,7 @@ public class DefaultMessageNotifier implements MessageNotifier { { NotificationState notificationState = new NotificationState(); MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor); - + ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase(); MessageRecord record; while ((record = reader.getNext()) != null) { @@ -497,13 +499,20 @@ public class DefaultMessageNotifier implements MessageNotifier { Recipient threadRecipients = null; SlideDeck slideDeck = null; long timestamp = record.getTimestamp(); + boolean messageRequest = false; if (threadId != -1) { - threadRecipients = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadId); + threadRecipients = threadDatabase.getRecipientForThreadId(threadId); + messageRequest = threadRecipients != null && !threadRecipients.isGroupRecipient() && + !threadRecipients.isApproved() && !threadDatabase.getLastSeenAndHasSent(threadId).second(); + if (messageRequest && (threadDatabase.getMessageCount(threadId) > 1 || !TextSecurePreferences.hasHiddenMessageRequests(context))) { + continue; + } } - - if (KeyCachingService.isLocked(context)) { + if (messageRequest) { + body = SpanUtil.italic(context.getString(R.string.message_requests_notification)); + } else if (KeyCachingService.isLocked(context)) { body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 90be1fc3d..f1ec6d188 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -16,6 +16,7 @@ import org.session.libsession.messaging.messages.control.ReadReceipt; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo; @@ -83,7 +84,7 @@ public class MarkReadReceiver extends BroadcastReceiver { for (Address address : addressMap.keySet()) { List timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); - if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; } + if (!SessionMetaProtocol.shouldSendReadReceipt(Recipient.from(context, address, false))) { continue; } ReadReceipt readReceipt = new ReadReceipt(timestamps); readReceipt.setSentTimestamp(System.currentTimeMillis()); MessageSender.send(readReceipt, address); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index d5c7747e0..eaac7aaa4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -35,6 +35,7 @@ import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.home.PathActivity +import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions @@ -91,6 +92,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { pathContainer.disableClipping() privacyButton.setOnClickListener { showPrivacySettings() } notificationsButton.setOnClickListener { showNotificationSettings() } + messageRequestsButton.setOnClickListener { showMessageRequests() } chatsButton.setOnClickListener { showChatSettings() } sendInvitationButton.setOnClickListener { sendInvitation() } faqButton.setOnClickListener { showFAQ() } @@ -283,6 +285,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { push(intent) } + private fun showMessageRequests() { + val intent = Intent(this, MessageRequestsActivity::class.java) + push(intent) + } + private fun showChatSettings() { val intent = Intent(this, ChatSettingsActivity::class.java) push(intent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 4225dabf4..ad118e0ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.repository import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.OpenGroupInvitation @@ -17,10 +19,13 @@ import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.SessionJobDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.ThreadRecord import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -51,6 +56,17 @@ interface ConversationRepository { suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf + + suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf + + suspend fun clearAllMessageRequests(): ResultOf + + suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf + + fun declineMessageRequest(threadId: Long, recipient: Recipient) + + fun hasReceived(threadId: Long): Boolean + } class DefaultConversationRepository @Inject constructor( @@ -61,8 +77,10 @@ class DefaultConversationRepository @Inject constructor( private val lokiThreadDb: LokiThreadDatabase, private val smsDb: SmsDatabase, private val mmsDb: MmsDatabase, + private val mmsSmsDb: MmsSmsDatabase, private val recipientDb: RecipientDatabase, - private val lokiMessageDb: LokiMessageDatabase + private val lokiMessageDb: LokiMessageDatabase, + private val sessionJobDb: SessionJobDatabase ) : ConversationRepository { override fun isOxenHostedOpenGroup(threadId: Long): Boolean { @@ -226,4 +244,47 @@ class DefaultConversationRepository @Inject constructor( } } + override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf { + sessionJobDb.cancelPendingMessageSendJobs(thread.threadId) + recipientDb.setBlocked(thread.recipient, true) + return ResultOf.Success(Unit) + } + + override suspend fun clearAllMessageRequests(): ResultOf { + threadDb.readerFor(threadDb.unapprovedConversationList).use { reader -> + while (reader.next != null) { + deleteMessageRequest(reader.current) + } + } + return ResultOf.Success(Unit) + } + + override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf = suspendCoroutine { continuation -> + recipientDb.setApproved(recipient, true) + val message = MessageRequestResponse(true) + MessageSender.send(message, Destination.from(recipient.address)) + .success { + threadDb.setHasSent(threadId, true) + continuation.resume(ResultOf.Success(Unit)) + }.fail { error -> + continuation.resumeWithException(error) + } + } + + override fun declineMessageRequest(threadId: Long, recipient: Recipient) { + recipientDb.setBlocked(recipient, true) + } + + override fun hasReceived(threadId: Long): Boolean { + val cursor = mmsSmsDb.getConversation(threadId, true) + mmsSmsDb.readerFor(cursor).use { reader -> + while (reader.next != null) { + if (!reader.current.isOutgoing) { + return true + } + } + } + return false + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index 6bf41032b..d571ad1d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -110,6 +110,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1, duration * 1000L, true, false, + false, Optional.absent(), groupInfo, Optional.absent(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index 0031bcc1b..fd462417d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -17,9 +17,17 @@ object ConfigurationMessageUtilities { val now = System.currentTimeMillis() if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> - !recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() + !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() }.map { recipient -> - ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) + ConfigurationMessage.Contact( + publicKey = recipient.address.serialize(), + name = recipient.name!!, + profilePicture = recipient.profileAvatar, + profileKey = recipient.profileKey, + isApproved = recipient.isApproved, + isBlocked = recipient.isBlocked, + didApproveMe = recipient.hasApprovedMe() + ) } val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) @@ -29,9 +37,17 @@ object ConfigurationMessageUtilities { fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> - !recipient.isGroupRecipient && !recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() + !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() }.map { recipient -> - ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) + ConfigurationMessage.Contact( + publicKey = recipient.address.serialize(), + name = recipient.name!!, + profilePicture = recipient.profileAvatar, + profileKey = recipient.profileKey, + isApproved = recipient.isApproved, + isBlocked = recipient.isBlocked, + didApproveMe = recipient.hasApprovedMe() + ) } val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt index fcfa1d082..9d81ed56e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt @@ -48,8 +48,8 @@ object SessionMetaProtocol { } @JvmStatic - fun shouldSendReadReceipt(address: Address): Boolean { - return !address.isGroup + fun shouldSendReadReceipt(recipient: Recipient): Boolean { + return !recipient.isGroupRecipient && recipient.isApproved } @JvmStatic diff --git a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml new file mode 100644 index 000000000..7db4da2ec --- /dev/null +++ b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete_24.xml b/app/src/main/res/drawable/ic_delete_24.xml new file mode 100644 index 000000000..178806738 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_message_requests_24.xml b/app/src/main/res/drawable/ic_outline_message_requests_24.xml new file mode 100644 index 000000000..e01bceed1 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_message_requests_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index b4c5183c9..79d115554 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -25,7 +25,7 @@ android:layout_width="match_parent" android:layout_height="36dp" android:visibility="gone" - android:layout_above="@+id/inputBar" + android:layout_above="@+id/messageRequestBar" /> + + + + + + +