diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f1b256749f..54ec5ea7ab 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 78aaa21453..a693e954f4 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 a78b9b3a8b..4692bf7862 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 852d0a998c..dbe35ee96a 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 cd18725e0f..260b5d2656 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 90afabb80d..a42222f412 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 73e5ac338c..e4247b86ff 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 8dc9d2b5d6..48ce85de19 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 31ecbcb99b..9d200971c7 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 52642b5d1f..92f9e970d7 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 f9d524010a..a0d8715fac 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 e1a3383d98..70db93a792 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 1dfc1f0b25..d6b5e824aa 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 84c7de34e9..2b0b231678 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 b0a02041ac..3afcd66b18 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 10e4cb753e..141c77791e 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 f6508b766d..0522dbcac2 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 921ecfffa5..a748189b2e 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 0000000000..d8d12938fe
--- /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 0000000000..6881e8e8af
--- /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 0000000000..17f4440e40
--- /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 0000000000..16d83be6db
--- /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 0000000000..c07fd5ce68
--- /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 eeffdd8344..efe75fef8e 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 90be1fc3dc..f1ec6d1887 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 d5c7747e0d..eaac7aaa43 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 4225dabf41..ad118e0eac 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 6bf41032b5..d571ad1d44 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 0031bcc1bf..fd462417d9 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 fcfa1d082b..9d81ed56ee 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 0000000000..7db4da2ec4
--- /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 0000000000..178806738a
--- /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 0000000000..e01bceed1c
--- /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 b4c5183c98..79d115554c 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"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_message_requests.xml b/app/src/main/res/layout/activity_message_requests.xml
new file mode 100644
index 0000000000..522ba5147f
--- /dev/null
+++ b/app/src/main/res/layout/activity_message_requests.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 5556182b31..ba8baca1bf 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -179,6 +179,22 @@
android:layout_height="1px"
android:background="?android:dividerHorizontal" />
+
+
+
+
+ tools:src="@drawable/ic_timer"
+ tools:visibility="visible"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_message_request_banner.xml b/app/src/main/res/layout/view_message_request_banner.xml
new file mode 100644
index 0000000000..e3121aaa5a
--- /dev/null
+++ b/app/src/main/res/layout/view_message_request_banner.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_message_request.xml b/app/src/main/res/menu/menu_message_request.xml
new file mode 100644
index 0000000000..a840ffb36d
--- /dev/null
+++ b/app/src/main/res/menu/menu_message_request.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 07a9b68d06..3205620dde 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -730,6 +730,7 @@
Please pick a shorter display name
Privacy
Notifications
+ Message Requests
Chats
Devices
Invite a Friend
@@ -795,7 +796,7 @@
Select Contacts
Secure session reset done
-
+
Theme
Day
Night
@@ -882,8 +883,21 @@
Mark all as read
Contacts and Groups
Messages
+ Message Requests
+ Sending a message to this user will automatically accept their message request and reveal your Session ID.
+ Accept
+ Decline
+ Clear All
+ Are you sure you want to delete this message request?
+ Message request deleted
+ Are you sure you want to clear all message requests?
+ Message requests deleted
+ Your message request has been accepted.
+ Your message request is currently pending.
+ No pending message requests
Direct Message
Closed Group
Open Group
+ You have a new message request
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b6c72f867d..1622da9e32 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -95,6 +95,12 @@
- ?android:textColorPrimary
+
+