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
This commit is contained in:
ceokot 2022-03-04 08:46:39 +02:00 committed by GitHub
parent 55aa266769
commit 206505abe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 2427 additions and 205 deletions

View File

@ -124,6 +124,11 @@
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/Theme.Session.DayNight.NoActionBar" /> android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
android:exported="false"
android:label="@string/activity_message_requests_title"
android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity" android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -4,12 +4,12 @@ import android.content.Context
import org.thoughtcrime.securesms.util.ContactUtilities import org.thoughtcrime.securesms.util.ContactUtilities
import org.thoughtcrime.securesms.util.AsyncLoader import org.thoughtcrime.securesms.util.AsyncLoader
class SelectContactsLoader(context: Context, val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) { class SelectContactsLoader(context: Context, private val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) {
override fun loadInBackground(): List<String> { override fun loadInBackground(): List<String> {
val contacts = ContactUtilities.getAllContacts(context) val contacts = ContactUtilities.getAllContacts(context)
return contacts.filter { contact -> return contacts.filter {
!contact.isGroupRecipient && !usersToExclude.contains(contact.address.toString()) !it.isGroupRecipient && !usersToExclude.contains(it.address.toString()) && it.hasApprovedMe()
}.map { }.map {
it.address.toString() it.address.toString()
} }

View File

@ -44,6 +44,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream import com.annimon.stream.Stream
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
import network.loki.messenger.databinding.ActivityConversationV2Binding 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.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
@ -220,7 +223,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private val adapter by lazy { private val adapter by lazy {
val cursor = mmsSmsDb.getConversation(viewModel.threadId) val cursor = mmsSmsDb.getConversation(viewModel.threadId, !isIncomingMessageRequestThread())
val adapter = ConversationAdapter( val adapter = ConversationAdapter(
this, this,
cursor, cursor,
@ -310,6 +313,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpSearchResultObserver() setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded() scrollToFirstUnreadMessageIfNeeded()
showOrHideInputIfNeeded() showOrHideInputIfNeeded()
setUpMessageRequestsBar()
if (viewModel.recipient.isOpenGroupRecipient) { if (viewModel.recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup == null) { if (openGroup == null) {
@ -349,13 +353,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate // called from onCreate
private fun setUpRecyclerView() { private fun setUpRecyclerView() {
binding!!.conversationRecyclerView.adapter = adapter 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 binding!!.conversationRecyclerView.layoutManager = layoutManager
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will) // 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<Cursor> { LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return ConversationLoader(viewModel.threadId, this@ConversationActivityV2) return ConversationLoader(viewModel.threadId, reverseLayout, this@ConversationActivityV2)
} }
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) { override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
@ -539,6 +544,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
viewModel.messageShown(it.id) viewModel.messageShown(it.id)
} }
addOpenGroupGuidelinesIfNeeded(uiState.isOxenHostedOpenGroup) 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 { 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) super.onPrepareOptionsMenu(menu)
return true 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) { override fun inputBarEditTextContentChanged(newContent: CharSequence) {
val inputBarText = binding?.inputBar?.text ?: return // TODO check if we should be referencing newContent here instead val inputBarText = binding?.inputBar?.text ?: return // TODO check if we should be referencing newContent here instead
if (textSecurePreferences.isLinkPreviewsEnabled()) { if (textSecurePreferences.isLinkPreviewsEnabled()) {
@ -946,6 +999,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun sendMessage() { override fun sendMessage() {
if (isIncomingMessageRequestThread()) {
acceptMessageRequest()
}
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) { if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog") BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
return return

View File

@ -5,9 +5,13 @@ import android.database.Cursor
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.AbstractCursorLoader 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 { override fun getCursor(): Cursor {
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID) return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
} }
} }

View File

@ -22,9 +22,8 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState()) private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> = _uiState val uiState: StateFlow<ConversationUiState> = _uiState
val recipient: Recipient by lazy { val recipient: Recipient
repository.getRecipientForThreadId(threadId) get() = repository.getRecipientForThreadId(threadId)
}
init { init {
_uiState.update { _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) { private fun showMessage(message: String) {
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
val messages = currentUiState.uiMessages + UiMessage( val messages = currentUiState.uiMessages + UiMessage(
@ -105,6 +120,10 @@ class ConversationViewModel(
} }
} }
fun hasReceived(): Boolean {
return repository.hasReceived(threadId)
}
@dagger.assisted.AssistedFactory @dagger.assisted.AssistedFactory
interface AssistedFactory { interface AssistedFactory {
fun create(threadId: Long): Factory fun create(threadId: Long): Factory
@ -126,5 +145,6 @@ data class UiMessage(val id: Long, val message: String)
data class ConversationUiState( data class ConversationUiState(
val isOxenHostedOpenGroup: Boolean = false, val isOxenHostedOpenGroup: Boolean = false,
val uiMessages: List<UiMessage> = emptyList() val uiMessages: List<UiMessage> = emptyList(),
val isMessageRequestAccepted: Boolean? = null
) )

View File

@ -37,6 +37,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
var linkPreview: LinkPreview? = null var linkPreview: LinkPreview? = null
var showInput: Boolean = true var showInput: Boolean = true
set(value) { field = value; showOrHideInputIfNeeded() } set(value) { field = value; showOrHideInputIfNeeded() }
var showMediaControls: Boolean = true
set(value) {
field = value
showOrHideMediaControlsIfNeeded()
binding.inputBarEditText.showMediaControls = value
}
var text: String var text: String
get() { return binding.inputBarEditText.text?.toString() ?: "" } 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) { fun addTextChangedListener(textWatcher: TextWatcher) {
binding.inputBarEditText.addTextChangedListener(textWatcher) binding.inputBarEditText.addTextChangedListener(textWatcher)
} }

View File

@ -21,6 +21,8 @@ class InputBarEditText : AppCompatEditText {
private val screenWidth get() = Resources.getSystem().displayMetrics.widthPixels private val screenWidth get() = Resources.getSystem().displayMetrics.widthPixels
var delegate: InputBarEditTextDelegate? = null var delegate: InputBarEditTextDelegate? = null
var showMediaControls: Boolean = true
private val snMinHeight = toPx(40.0f, resources) private val snMinHeight = toPx(40.0f, resources)
private val snMaxHeight = toPx(80.0f, resources) private val snMaxHeight = toPx(80.0f, resources)
@ -47,7 +49,9 @@ class InputBarEditText : AppCompatEditText {
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
val ic = super.onCreateInputConnection(editorInfo) ?: return null 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 = val callback =
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts -> InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->

View File

@ -29,19 +29,26 @@ class ControlMessageView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, previous: MessageRecord?) { fun bind(message: MessageRecord, previous: MessageRecord?) {
binding.dateBreakTextView.showDateBreak(message, previous) binding.dateBreakTextView.showDateBreak(message, previous)
binding.iconImageView.visibility = View.GONE var messageBody: CharSequence = message.getDisplayBody(context)
if (message.isExpirationTimerUpdate) { when {
binding.iconImageView.setImageDrawable( message.isExpirationTimerUpdate -> {
ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme) binding.iconImageView.setImageDrawable(
) ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
binding.iconImageView.visibility = View.VISIBLE )
} else if (message.isMediaSavedNotification) { binding.iconImageView.visibility = View.VISIBLE
binding.iconImageView.setImageDrawable( }
ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme) message.isMediaSavedNotification -> {
) binding.iconImageView.setImageDrawable(
binding.iconImageView.visibility = View.VISIBLE 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() { fun recycle() {

View File

@ -19,7 +19,7 @@ object MentionManagerUtilities {
result.addAll(members) result.addAll(members)
} else { } else {
val messageDatabase = DatabaseComponent.get(context).mmsSmsDatabase() 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 var record: MessageRecord? = reader.next
while (record != null) { while (record != null) {
result.add(record.individualRecipient.address.serialize()) result.add(record.individualRecipient.address.serialize())

View File

@ -178,6 +178,11 @@ public class MmsDatabase extends MessagingDatabase {
private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
private final EarlyReceiptCache earlyReadReceiptCache = 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) { public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -664,6 +669,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(EXPIRES_IN, retrieved.getExpiresIn()); contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0); contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0);
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified()); contentValues.put(UNIDENTIFIED, retrieved.isUnidentified());
contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse());
if (!contentValues.containsKey(DATE_SENT)) { if (!contentValues.containsKey(DATE_SENT)) {
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED));
@ -680,7 +686,8 @@ public class MmsDatabase extends MessagingDatabase {
quoteAttachments = retrieved.getQuote().getAttachments(); 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() + ")"); Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")");
return Optional.absent(); return Optional.absent();
} }
@ -750,6 +757,10 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.MEDIA_SAVED_EXTRACTION_BIT; type |= Types.MEDIA_SAVED_EXTRACTION_BIT;
} }
if (retrieved.isMessageRequestResponse()) {
type |= Types.MESSAGE_REQUEST_RESPONSE_BIT;
}
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp); return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp);
} }
@ -1000,6 +1011,19 @@ public class MmsDatabase extends MessagingDatabase {
return linkPreviewJson.toString(); 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) { private boolean isDuplicate(IncomingMediaMessage message, long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",

View File

@ -20,6 +20,7 @@ public interface MmsSmsColumns {
public static final String EXPIRE_STARTED = "expire_started"; public static final String EXPIRE_STARTED = "expire_started";
public static final String NOTIFIED = "notified"; public static final String NOTIFIED = "notified";
public static final String UNIDENTIFIED = "unidentified"; public static final String UNIDENTIFIED = "unidentified";
public static final String MESSAGE_REQUEST_RESPONSE = "message_request_response";
public static class Types { public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF; 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_SENT_BIT = 0x01000000;
protected static final long ENCRYPTION_LOKI_SESSION_RESTORE_DONE_BIT = 0x00100000; 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) { public static boolean isDraftMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE; return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
} }
@ -274,6 +277,10 @@ public interface MmsSmsColumns {
(type & ENCRYPTION_REMOTE_BIT) != 0; (type & ENCRYPTION_REMOTE_BIT) != 0;
} }
public static boolean isMessageRequestResponse(long type) {
return (type & MESSAGE_REQUEST_RESPONSE_BIT) != 0;
}
public static long translateFromSystemBaseType(long theirType) { public static long translateFromSystemBaseType(long theirType) {
switch ((int)theirType) { switch ((int)theirType) {

View File

@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.io.Closeable;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -111,8 +112,8 @@ public class MmsSmsDatabase extends Database {
return getMessageFor(timestamp, author.serialize()); return getMessageFor(timestamp, author.serialize());
} }
public Cursor getConversation(long threadId, long offset, long limit) { public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null; String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
@ -122,8 +123,8 @@ public class MmsSmsDatabase extends Database {
return cursor; return cursor;
} }
public Cursor getConversation(long threadId) { public Cursor getConversation(long threadId, boolean reverse) {
return getConversation(threadId, 0, 0); return getConversation(threadId, reverse, 0, 0);
} }
public Cursor getConversationSnippet(long threadId) { public Cursor getConversationSnippet(long threadId) {
@ -406,7 +407,7 @@ public class MmsSmsDatabase extends Database {
return new Reader(cursor); return new Reader(cursor);
} }
public class Reader { public class Reader implements Closeable {
private final Cursor cursor; private final Cursor cursor;
private SmsDatabase.Reader smsReader; private SmsDatabase.Reader smsReader;
@ -448,7 +449,9 @@ public class MmsSmsDatabase extends Database {
} }
public void close() { public void close() {
cursor.close(); if (cursor != null) {
cursor.close();
}
} }
} }
} }

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@ -34,7 +36,9 @@ public class RecipientDatabase extends Database {
static final String TABLE_NAME = "recipient_preferences"; static final String TABLE_NAME = "recipient_preferences";
private static final String ID = "_id"; private static final String ID = "_id";
public static final String ADDRESS = "recipient_ids"; 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 NOTIFICATION = "notification";
private static final String VIBRATE = "vibrate"; private static final String VIBRATE = "vibrate";
private static final String MUTE_UNTIL = "mute_until"; 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 NOTIFY_TYPE = "notify_type"; // all, mentions only, none
private static final String[] RECIPIENT_PROJECTION = new String[] { 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, PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE, UNIDENTIFIED_ACCESS_MODE,
@ -102,6 +106,22 @@ public class RecipientDatabase extends Database {
"ADD COLUMN " + NOTIFY_TYPE + " INTEGER DEFAULT 0;"; "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_ALL = 0;
public static final int NOTIFY_TYPE_MENTIONS = 1; public static final int NOTIFY_TYPE_MENTIONS = 1;
public static final int NOTIFY_TYPE_NONE = 2; public static final int NOTIFY_TYPE_NONE = 2;
@ -137,6 +157,8 @@ public class RecipientDatabase extends Database {
Optional<RecipientSettings> getRecipientSettings(@NonNull Cursor cursor) { Optional<RecipientSettings> getRecipientSettings(@NonNull Cursor cursor) {
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; 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 messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE)); String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); 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, notifyType,
Recipient.VibrateState.fromId(messageVibrateState), Recipient.VibrateState.fromId(messageVibrateState),
Recipient.VibrateState.fromId(callVibrateState), Recipient.VibrateState.fromId(callVibrateState),
@ -213,6 +235,15 @@ public class RecipientDatabase extends Database {
recipient.resolve().setForceSmsSelection(forceSmsSelection); 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) { public void setBlocked(@NonNull Recipient recipient, boolean blocked) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0); values.put(BLOCK, blocked ? 1 : 0);

View File

@ -6,6 +6,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.* import org.session.libsession.messaging.jobs.*
import org.session.libsession.messaging.messages.control.ConfigurationMessage 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.*
import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment 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.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
@ -581,7 +583,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
recipientDatabase.setProfileSharing(recipient, true) recipientDatabase.setProfileSharing(recipient, true)
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED) recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
// create Thread if needed // 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()) { if (contacts.isNotEmpty()) {
threadDatabase.notifyConversationListListeners() threadDatabase.notifyConversationListListeners()
@ -613,17 +627,63 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
if (recipient.isBlocked) return if (recipient.isBlocked) return
val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1, val mediaMessage = IncomingMediaMessage(
0, false, address,
false, sentTimestamp,
Optional.absent(), -1,
Optional.absent(), 0,
Optional.absent(), false,
Optional.absent(), false,
Optional.absent(), false,
Optional.absent(), Optional.absent(),
Optional.of(message)) Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(message)
)
database.insertSecureDecryptedMessageInbox(mediaMessage, -1) 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)
}
}
} }

View File

@ -253,7 +253,7 @@ public class ThreadDatabase extends Database {
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadId); cursor = DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadId, true);
if (cursor != null && length > 0 && cursor.getCount() > length) { if (cursor != null && length > 0 && cursor.getCount() > length) {
Log.w("ThreadDatabase", "Cursor count is greater than length!"); Log.w("ThreadDatabase", "Cursor count is greater than length!");
@ -388,20 +388,88 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null); 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() { 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() { 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(); 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); String query = createQuery(where, 0);
Cursor cursor = db.rawQuery(query, new String[]{archived}); Cursor cursor = db.rawQuery(query, null);
setNotifyConverationListListeners(cursor); 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) { public void deleteConversation(long threadId) {
DatabaseComponent.get(context).smsDatabase().deleteThread(threadId); DatabaseComponent.get(context).smsDatabase().deleteThread(threadId);
DatabaseComponent.get(context).mmsDatabase().deleteThread(threadId); DatabaseComponent.get(context).mmsDatabase().deleteThread(threadId);

View File

@ -62,9 +62,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV28 = 49; private static final int lokiV28 = 49;
private static final int lokiV29 = 50; private static final int lokiV29 = 50;
private static final int lokiV30 = 51; 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 // 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 static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -138,6 +139,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand()); db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
db.execSQL(ThreadDatabase.getCreatePinnedCommand()); db.execSQL(ThreadDatabase.getCreatePinnedCommand());
db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand());
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -320,6 +324,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -118,8 +118,10 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isMissedCall(type); return SmsDatabase.Types.isMissedCall(type);
} }
public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); } public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); }
public boolean isMessageRequestResponse() { return MmsSmsColumns.Types.isMessageRequestResponse(type); }
public boolean isControlMessage() { public boolean isControlMessage() {
return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification(); return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification()
|| isMessageRequestResponse();
} }
} }

View File

@ -17,6 +17,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode 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.GlobalSearchAdapter
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import java.io.IOException import java.io.IOException
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -93,10 +98,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>() private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
private val publicKey: String private val publicKey: String
get() = TextSecurePreferences.getLocalNumber(this)!! get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy { 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 -> private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -157,7 +162,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
binding.sessionToolbar.disableClipping() binding.sessionToolbar.disableClipping()
// Set up seed reminder view // Set up seed reminder view
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) val hasViewedSeed = textSecurePreferences.getHasViewedSeed()
if (!hasViewedSeed) { if (!hasViewedSeed) {
binding.seedReminderView.isVisible = true binding.seedReminderView.isVisible = true
binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
@ -167,6 +172,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} else { } else {
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
setupMessageRequestsBanner()
setupHeaderImage() setupHeaderImage()
// Set up recycler view // Set up recycler view
binding.globalSearchInputLayout.listener = this binding.globalSearchInputLayout.listener = this
@ -208,8 +214,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up remaining components if needed // Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity) val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForFCMIfNeeded(false) application.registerForFCMIfNeeded(false)
val userPublicKey = TextSecurePreferences.getLocalNumber(this@HomeActivity) if (textSecurePreferences.getLocalNumber() != null) {
if (userPublicKey != null) {
OpenGroupManager.startPolling() OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs() JobQueue.shared.resumePendingJobs()
} }
@ -293,12 +298,35 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButtonSet.isVisible = !isShown 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<Cursor> { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return HomeLoader(this@HomeActivity) return HomeLoader(this@HomeActivity)
} }
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) { override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
homeAdapter.changeCursor(cursor) homeAdapter.changeCursor(cursor)
setupMessageRequestsBanner()
updateEmptyState() updateEmptyState()
} }
@ -309,15 +337,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) 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) IdentityKeyUtil.checkUpdate(this)
binding.profileButton.recycle() // clear cached image before update tje profilePictureView binding.profileButton.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.update() binding.profileButton.update()
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (textSecurePreferences.getHasViewedSeed()) {
if (hasViewedSeed) {
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
if (TextSecurePreferences.getConfigurationMessageSynced(this)) { if (textSecurePreferences.getConfigurationMessageSynced()) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
} }
@ -361,7 +388,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun updateProfileButton() { private fun updateProfileButton() {
binding.profileButton.publicKey = publicKey binding.profileButton.publicKey = publicKey
binding.profileButton.displayName = TextSecurePreferences.getProfileName(this) binding.profileButton.displayName = textSecurePreferences.getProfileName()
binding.profileButton.recycle() binding.profileButton.recycle()
binding.profileButton.update() binding.profileButton.update()
} }
@ -522,7 +549,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val recipient = thread.recipient val recipient = thread.recipient
val message = if (recipient.isGroupRecipient) { val message = if (recipient.isGroupRecipient) {
val group = groupDatabase.getGroup(recipient.address.toString()).orNull() 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." "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
} else { } else {
resources.getString(R.string.activity_home_leave_group_dialog_message) resources.getString(R.string.activity_home_leave_group_dialog_message)
@ -584,6 +611,25 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
show(intent) 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() { override fun createNewPrivateChat() {
val intent = Intent(this, CreatePrivateChatActivity::class.java) val intent = Intent(this, CreatePrivateChatActivity::class.java)
show(intent) show(intent)

View File

@ -8,6 +8,6 @@ import org.thoughtcrime.securesms.util.AbstractCursorLoader
class HomeLoader(context: Context) : AbstractCursorLoader(context) { class HomeLoader(context: Context) : AbstractCursorLoader(context) {
override fun getCursor(): Cursor { override fun getCursor(): Cursor {
return DatabaseComponent.get(context).threadDatabase().conversationList return DatabaseComponent.get(context).threadDatabase().approvedConversationList
} }
} }

View File

@ -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
}

View File

@ -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<Cursor> {
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<Cursor> {
return MessageRequestsLoader(this@MessageRequestsActivity)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
adapter.changeCursor(cursor)
updateEmptyState()
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
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()
}
}

View File

@ -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<MessageRequestsAdapter.ViewHolder>(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.")
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -171,35 +171,33 @@ public class DefaultMessageNotifier implements MessageNotifier {
} }
private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) {
if (Build.VERSION.SDK_INT >= 23) { try {
try { NotificationManager notifications = ServiceUtil.getNotificationManager(context);
NotificationManager notifications = ServiceUtil.getNotificationManager(context); StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification notification : activeNotifications) { for (StatusBarNotification notification : activeNotifications) {
boolean validNotification = false; boolean validNotification = false;
if (notification.getId() != SUMMARY_NOTIFICATION_ID && if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
notification.getId() != FOREGROUND_ID && notification.getId() != FOREGROUND_ID &&
notification.getId() != PENDING_MESSAGES_ID) notification.getId() != PENDING_MESSAGES_ID)
{ {
for (NotificationItem item : notificationState.getNotifications()) { for (NotificationItem item : notificationState.getNotifications()) {
if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) { if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) {
validNotification = true; validNotification = true;
break; break;
}
}
if (!validNotification) {
notifications.cancel(notification.getId());
} }
} }
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; boolean isVisible = visibleThread == threadId;
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase(); 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<MarkedMessageInfo> messageIds = threads.setRead(threadId, false); List<MarkedMessageInfo> 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) || if (!TextSecurePreferences.isNotificationsEnabled(context) ||
(recipients != null && recipients.isMuted())) (recipient != null && recipient.isMuted()))
{ {
return; return;
} }
@ -484,7 +486,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
{ {
NotificationState notificationState = new NotificationState(); NotificationState notificationState = new NotificationState();
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor); MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
MessageRecord record; MessageRecord record;
while ((record = reader.getNext()) != null) { while ((record = reader.getNext()) != null) {
@ -497,13 +499,20 @@ public class DefaultMessageNotifier implements MessageNotifier {
Recipient threadRecipients = null; Recipient threadRecipients = null;
SlideDeck slideDeck = null; SlideDeck slideDeck = null;
long timestamp = record.getTimestamp(); long timestamp = record.getTimestamp();
boolean messageRequest = false;
if (threadId != -1) { 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 (messageRequest) {
if (KeyCachingService.isLocked(context)) { 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)); body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);

View File

@ -16,6 +16,7 @@ import org.session.libsession.messaging.messages.control.ReadReceipt;
import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo;
@ -83,7 +84,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
for (Address address : addressMap.keySet()) { for (Address address : addressMap.keySet()) {
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); List<Long> 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 readReceipt = new ReadReceipt(timestamps);
readReceipt.setSentTimestamp(System.currentTimeMillis()); readReceipt.setSentTimestamp(System.currentTimeMillis());
MessageSender.send(readReceipt, address); MessageSender.send(readReceipt, address);

View File

@ -35,6 +35,7 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.home.PathActivity
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
@ -91,6 +92,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
pathContainer.disableClipping() pathContainer.disableClipping()
privacyButton.setOnClickListener { showPrivacySettings() } privacyButton.setOnClickListener { showPrivacySettings() }
notificationsButton.setOnClickListener { showNotificationSettings() } notificationsButton.setOnClickListener { showNotificationSettings() }
messageRequestsButton.setOnClickListener { showMessageRequests() }
chatsButton.setOnClickListener { showChatSettings() } chatsButton.setOnClickListener { showChatSettings() }
sendInvitationButton.setOnClickListener { sendInvitation() } sendInvitationButton.setOnClickListener { sendInvitation() }
faqButton.setOnClickListener { showFAQ() } faqButton.setOnClickListener { showFAQ() }
@ -283,6 +285,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
push(intent) push(intent)
} }
private fun showMessageRequests() {
val intent = Intent(this, MessageRequestsActivity::class.java)
push(intent)
}
private fun showChatSettings() { private fun showChatSettings() {
val intent = Intent(this, ChatSettingsActivity::class.java) val intent = Intent(this, ChatSettingsActivity::class.java)
push(intent) push(intent)

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.repository package org.thoughtcrime.securesms.repository
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation 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.LokiMessageDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SessionJobDatabase
import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -51,6 +56,17 @@ interface ConversationRepository {
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
suspend fun clearAllMessageRequests(): ResultOf<Unit>
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
fun declineMessageRequest(threadId: Long, recipient: Recipient)
fun hasReceived(threadId: Long): Boolean
} }
class DefaultConversationRepository @Inject constructor( class DefaultConversationRepository @Inject constructor(
@ -61,8 +77,10 @@ class DefaultConversationRepository @Inject constructor(
private val lokiThreadDb: LokiThreadDatabase, private val lokiThreadDb: LokiThreadDatabase,
private val smsDb: SmsDatabase, private val smsDb: SmsDatabase,
private val mmsDb: MmsDatabase, private val mmsDb: MmsDatabase,
private val mmsSmsDb: MmsSmsDatabase,
private val recipientDb: RecipientDatabase, private val recipientDb: RecipientDatabase,
private val lokiMessageDb: LokiMessageDatabase private val lokiMessageDb: LokiMessageDatabase,
private val sessionJobDb: SessionJobDatabase
) : ConversationRepository { ) : ConversationRepository {
override fun isOxenHostedOpenGroup(threadId: Long): Boolean { override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
@ -226,4 +244,47 @@ class DefaultConversationRepository @Inject constructor(
} }
} }
override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit> {
sessionJobDb.cancelPendingMessageSendJobs(thread.threadId)
recipientDb.setBlocked(thread.recipient, true)
return ResultOf.Success(Unit)
}
override suspend fun clearAllMessageRequests(): ResultOf<Unit> {
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<Unit> = 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
}
} }

View File

@ -110,6 +110,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1, IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
duration * 1000L, true, duration * 1000L, true,
false, false,
false,
Optional.absent(), Optional.absent(),
groupInfo, groupInfo,
Optional.absent(), Optional.absent(),

View File

@ -17,9 +17,17 @@ object ConfigurationMessageUtilities {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> 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 -> }.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 val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey))
@ -29,9 +37,17 @@ object ConfigurationMessageUtilities {
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> { fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit)
val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> 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 -> }.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 configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit)
val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))

View File

@ -48,8 +48,8 @@ object SessionMetaProtocol {
} }
@JvmStatic @JvmStatic
fun shouldSendReadReceipt(address: Address): Boolean { fun shouldSendReadReceipt(recipient: Recipient): Boolean {
return !address.isGroup return !recipient.isGroupRecipient && recipient.isApproved
} }
@JvmStatic @JvmStatic

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<corners android:radius="@dimen/medium_button_corner_radius" />
<stroke android:width="@dimen/border_thickness" android:color="@color/destructive" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/destructive">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2H4C2.9,2 2,2.9 2,4V22L6,18H20C21.1,18 22,17.1 22,16V4C22,2.9 21.1,2 20,2M20,16H5.2L4,17.2V4H20V16M12.2,5.5C11.3,5.5 10.6,5.7 10.1,6C9.5,6.4 9.2,7 9.3,7.7H11.3C11.3,7.4 11.4,7.2 11.6,7.1C11.8,7 12,6.9 12.3,6.9C12.6,6.9 12.9,7 13.1,7.2C13.3,7.4 13.4,7.6 13.4,7.9C13.4,8.2 13.3,8.4 13.2,8.6C13,8.8 12.8,9 12.6,9.1C12.1,9.4 11.7,9.7 11.5,9.9C11.1,10.2 11,10.5 11,11H13C13,10.7 13.1,10.5 13.1,10.3C13.2,10.1 13.4,10 13.6,9.8C14.1,9.6 14.4,9.3 14.7,8.9C15,8.5 15.1,8.1 15.1,7.7C15.1,7 14.8,6.4 14.3,6C13.9,5.7 13.1,5.5 12.2,5.5M11,12V14H13V12H11Z" />
</vector>

View File

@ -25,7 +25,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="36dp" android:layout_height="36dp"
android:visibility="gone" android:visibility="gone"
android:layout_above="@+id/inputBar" android:layout_above="@+id/messageRequestBar"
/> />
<org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar <org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
@ -91,9 +91,9 @@
android:visibility="gone" android:visibility="gone"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_alignParentRight="true" android:layout_alignParentEnd="true"
android:layout_above="@+id/inputBar" android:layout_above="@+id/messageRequestBar"
android:layout_marginRight="12dp" android:layout_marginEnd="12dp"
android:layout_marginBottom="32dp"> android:layout_marginBottom="32dp">
<RelativeLayout <RelativeLayout
@ -168,4 +168,52 @@
</RelativeLayout> </RelativeLayout>
<LinearLayout
android:id="@+id/messageRequestBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/inputBar"
android:layout_marginBottom="@dimen/large_spacing"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/sendAcceptsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_margin="@dimen/medium_spacing"
android:text="@string/message_requests_send_notice"
android:textColor="@color/text"
android:alpha="0.6"
android:textSize="@dimen/small_font_size" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:orientation="horizontal">
<Button
android:id="@+id/acceptMessageRequestButton"
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:text="@string/accept" />
<Button
android:id="@+id/declineMessageRequestButton"
style="@style/Widget.Session.Button.Common.DestructiveOutline"
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_weight="1"
android:text="@string/decline" />
</LinearLayout>
</LinearLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="172dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6"
tools:listitem="@layout/view_conversation" />
<View
android:id="@+id/gradientView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/home_activity_gradient" />
<LinearLayout
android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="32dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_request_empty_state_message"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size" />
</LinearLayout>
<Button
android:id="@+id/clearAllMessageRequestsButton"
style="@style/Widget.Session.Button.Common.DestructiveOutline"
android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/massive_spacing"
android:text="@string/message_requests_clear_all" />
</RelativeLayout>
</LinearLayout>

View File

@ -179,6 +179,22 @@
android:layout_height="1px" android:layout_height="1px"
android:background="?android:dividerHorizontal" /> android:background="?android:dividerHorizontal" />
<TextView
android:id="@+id/messageRequestsButton"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:background="@drawable/setting_button_background"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:gravity="center"
android:text="@string/activity_settings_message_requests_button_title" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?android:dividerHorizontal" />
<TextView <TextView
android:id="@+id/chatsButton" android:id="@+id/chatsButton"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -24,8 +24,10 @@
android:layout_width="12dp" android:layout_width="12dp"
android:layout_height="12dp" android:layout_height="12dp"
android:layout_marginBottom="@dimen/small_spacing" android:layout_marginBottom="@dimen/small_spacing"
android:visibility="gone"
app:tint="@color/text" app:tint="@color/text"
tools:src="@drawable/ic_timer" /> tools:src="@drawable/ic_timer"
tools:visibility="visible"/>
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/medium_spacing" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginEnd="@dimen/medium_spacing"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/displayNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
tools:text="I'm a very long display name. What are you going to do about it?" />
<TextView
android:id="@+id/timestampTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:alpha="0.4"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
tools:text="9:41 AM" />
</LinearLayout>
<TextView
android:id="@+id/snippetTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size"
tools:text="Sorry, gotta go fight crime again" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/conversation_view_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/accent_line_thickness"
android:paddingEnd="@dimen/medium_spacing">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:layout_marginVertical="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:padding="10dp"
android:src="@drawable/ic_outline_message_requests_24"
app:circleColor="#585858"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/conversationViewDisplayNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:drawablePadding="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/activity_message_requests_title"
android:textAlignment="viewStart"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/profilePictureView"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/unreadCountIndicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="4dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="#585858"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/conversationViewDisplayNameTextView"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/unreadCountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:textSize="@dimen/very_small_font_size"
android:textStyle="bold"
tools:text="8" />
</RelativeLayout>
<TextView
android:id="@+id/timestampTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:alpha="0.4"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="9:41 AM" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_delete_message_request"
android:icon="@drawable/ic_delete_24"
android:title="@string/delete"/>
</menu>

View File

@ -730,6 +730,7 @@
<string name="activity_settings_display_name_too_long_error">Please pick a shorter display name</string> <string name="activity_settings_display_name_too_long_error">Please pick a shorter display name</string>
<string name="activity_settings_privacy_button_title">Privacy</string> <string name="activity_settings_privacy_button_title">Privacy</string>
<string name="activity_settings_notifications_button_title">Notifications</string> <string name="activity_settings_notifications_button_title">Notifications</string>
<string name="activity_settings_message_requests_button_title">Message Requests</string>
<string name="activity_settings_chats_button_title">Chats</string> <string name="activity_settings_chats_button_title">Chats</string>
<string name="activity_settings_devices_button_title">Devices</string> <string name="activity_settings_devices_button_title">Devices</string>
<string name="activity_settings_invite_button_title">Invite a Friend</string> <string name="activity_settings_invite_button_title">Invite a Friend</string>
@ -795,7 +796,7 @@
<string name="activity_select_contacts_title">Select Contacts</string> <string name="activity_select_contacts_title">Select Contacts</string>
<string name="view_reset_secure_session_done_message">Secure session reset done</string> <string name="view_reset_secure_session_done_message">Secure session reset done</string>
<string name="dialog_ui_mode_title">Theme</string> <string name="dialog_ui_mode_title">Theme</string>
<string name="dialog_ui_mode_option_day">Day</string> <string name="dialog_ui_mode_option_day">Day</string>
<string name="dialog_ui_mode_option_night">Night</string> <string name="dialog_ui_mode_option_night">Night</string>
@ -882,8 +883,21 @@
<string name="mark_all_as_read">Mark all as read</string> <string name="mark_all_as_read">Mark all as read</string>
<string name="global_search_contacts_groups">Contacts and Groups</string> <string name="global_search_contacts_groups">Contacts and Groups</string>
<string name="global_search_messages">Messages</string> <string name="global_search_messages">Messages</string>
<string name="activity_message_requests_title">Message Requests</string>
<string name="message_requests_send_notice">Sending a message to this user will automatically accept their message request and reveal your Session ID.</string>
<string name="accept">Accept</string>
<string name="decline">Decline</string>
<string name="message_requests_clear_all">Clear All</string>
<string name="message_requests_delete_message">Are you sure you want to delete this message request?</string>
<string name="message_requests_deleted">Message request deleted</string>
<string name="message_requests_clear_all_message">Are you sure you want to clear all message requests?</string>
<string name="message_requests_cleared">Message requests deleted</string>
<string name="message_requests_accepted">Your message request has been accepted.</string>
<string name="message_requests_pending">Your message request is currently pending.</string>
<string name="message_request_empty_state_message">No pending message requests</string>
<string name="NewConversationButton_SessionTooltip">Direct Message</string> <string name="NewConversationButton_SessionTooltip">Direct Message</string>
<string name="NewConversationButton_ClosedGroupTooltip">Closed Group</string> <string name="NewConversationButton_ClosedGroupTooltip">Closed Group</string>
<string name="NewConversationButton_OpenGroupTooltip">Open Group</string> <string name="NewConversationButton_OpenGroupTooltip">Open Group</string>
<string name="message_requests_notification">You have a new message request</string>
</resources> </resources>

View File

@ -95,6 +95,12 @@
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item> <item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Common.DestructiveOutline">
<item name="android:background">@drawable/destructive_outline_button_medium_background</item>
<item name="android:textColor">@color/destructive</item>
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item>
</style>
<style name="Widget.Session.Button.Dialog" parent=""> <style name="Widget.Session.Button.Dialog" parent="">
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/small_font_size</item> <item name="android:textSize">@dimen/small_font_size</item>

View File

@ -158,6 +158,20 @@ class ConversationViewModelTest: BaseViewModelTest() {
) )
} }
@Test
fun `should accept message request`() = runBlockingTest {
viewModel.acceptMessageRequest()
verify(repository).acceptMessageRequest(threadId, recipient)
}
@Test
fun `should decline message request`() {
viewModel.declineMessageRequest()
verify(repository).declineMessageRequest(threadId, recipient)
}
@Test @Test
fun `should remove shown message`() = runBlockingTest { fun `should remove shown message`() = runBlockingTest {
// Given that a message is generated // Given that a message is generated

View File

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.messagerequests
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
class MessageRequestsViewModelTest : BaseViewModelTest() {
private val repository = mock(ConversationRepository::class.java)
private val viewModel: MessageRequestsViewModel by lazy {
MessageRequestsViewModel(repository)
}
@Test
fun `should delete message request`() = runBlockingTest {
val thread = mock(ThreadRecord::class.java)
viewModel.deleteMessageRequest(thread)
verify(repository).deleteMessageRequest(thread)
}
@Test
fun `should clear all message requests`() = runBlockingTest {
viewModel.clearAllMessageRequests()
verify(repository).clearAllMessageRequests()
}
}

View File

@ -8,6 +8,7 @@ import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
@ -156,4 +157,5 @@ interface StorageProtocol {
*/ */
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
fun insertMessageRequestResponse(response: MessageRequestResponse)
} }

View File

@ -60,19 +60,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
} }
} }
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) { class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?, var isApproved: Boolean?, var isBlocked: Boolean?, var didApproveMe: Boolean?) {
internal constructor() : this("", "", null, null) internal constructor() : this("", "", null, null, null, null, null)
companion object { companion object {
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? { fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
if (!proto.hasName() || !proto.hasProfileKey()) return null if (!proto.hasName()) return null
val publicKey = proto.publicKey.toByteArray().toHexString() val publicKey = proto.publicKey.toByteArray().toHexString()
val name = proto.name val name = proto.name
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
return Contact(publicKey, name, profilePicture, profileKey) val isApproved = if (proto.hasIsApproved()) proto.isApproved else null
val isBlocked = if (proto.hasIsBlocked()) proto.isBlocked else null
val didApproveMe = if (proto.hasDidApproveMe()) proto.didApproveMe else null
return Contact(publicKey, name, profilePicture, profileKey, isApproved, isBlocked, didApproveMe)
} }
} }
@ -92,6 +95,18 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
if (profileKey != null) { if (profileKey != null) {
result.profileKey = ByteString.copyFrom(profileKey) result.profileKey = ByteString.copyFrom(profileKey)
} }
val isApproved = isApproved
if (isApproved != null) {
result.isApproved = isApproved
}
val isBlocked = isBlocked
if (isBlocked != null) {
result.isBlocked = isBlocked
}
val didApproveMe = didApproveMe
if (didApproveMe != null) {
result.didApproveMe = didApproveMe
}
return result.build() return result.build()
} }
} }

View File

@ -0,0 +1,33 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
override val isSelfSendValid: Boolean = true
override fun toProto(): SignalServiceProtos.Content? {
val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
.setIsApproved(isApproved)
return try {
SignalServiceProtos.Content.newBuilder()
.setMessageRequestResponse(messageRequestResponseProto.build())
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct message request response proto from: $this")
null
}
}
companion object {
const val TAG = "MessageRequestResponse"
fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? {
val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null
val isApproved = messageRequestResponseProto.isApproved
return MessageRequestResponse(isApproved)
}
}
}

View File

@ -28,6 +28,7 @@ public class IncomingMediaMessage {
private final long expiresIn; private final long expiresIn;
private final boolean expirationUpdate; private final boolean expirationUpdate;
private final boolean unidentified; private final boolean unidentified;
private final boolean messageRequestResponse;
private final DataExtractionNotificationInfoMessage dataExtractionNotification; private final DataExtractionNotificationInfoMessage dataExtractionNotification;
private final QuoteModel quote; private final QuoteModel quote;
@ -42,6 +43,7 @@ public class IncomingMediaMessage {
long expiresIn, long expiresIn,
boolean expirationUpdate, boolean expirationUpdate,
boolean unidentified, boolean unidentified,
boolean messageRequestResponse,
Optional<String> body, Optional<String> body,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments, Optional<List<SignalServiceAttachment>> attachments,
@ -60,6 +62,7 @@ public class IncomingMediaMessage {
this.dataExtractionNotification = dataExtractionNotification.orNull(); this.dataExtractionNotification = dataExtractionNotification.orNull();
this.quote = quote.orNull(); this.quote = quote.orNull();
this.unidentified = unidentified; this.unidentified = unidentified;
this.messageRequestResponse = messageRequestResponse;
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get())); if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
else this.groupId = null; else this.groupId = null;
@ -78,7 +81,7 @@ public class IncomingMediaMessage {
Optional<List<LinkPreview>> linkPreviews) Optional<List<LinkPreview>> linkPreviews)
{ {
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false, return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent()); false, false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
} }
public int getSubscriptionId() { public int getSubscriptionId() {
@ -150,4 +153,8 @@ public class IncomingMediaMessage {
public boolean isUnidentified() { public boolean isUnidentified() {
return unidentified; return unidentified;
} }
public boolean isMessageRequestResponse() {
return messageRequestResponse;
}
} }

View File

@ -95,6 +95,7 @@ object MessageReceiver {
ExpirationTimerUpdate.fromProto(proto) ?: ExpirationTimerUpdate.fromProto(proto) ?:
ConfigurationMessage.fromProto(proto) ?: ConfigurationMessage.fromProto(proto) ?:
UnsendRequest.fromProto(proto) ?: UnsendRequest.fromProto(proto) ?:
MessageRequestResponse.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
// Ignore self send if needed // Ignore self send if needed
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend

View File

@ -5,7 +5,14 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.* import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
@ -15,7 +22,12 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.* import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
@ -28,8 +40,7 @@ import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.LinkedList
import kotlin.collections.ArrayList
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
@ -46,6 +57,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
is DataExtractionNotification -> handleDataExtractionNotification(message) is DataExtractionNotification -> handleDataExtractionNotification(message)
is ConfigurationMessage -> handleConfigurationMessage(message) is ConfigurationMessage -> handleConfigurationMessage(message)
is UnsendRequest -> handleUnsendRequest(message) is UnsendRequest -> handleUnsendRequest(message)
is MessageRequestResponse -> handleMessageRequestResponse(message)
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID) is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
} }
} }
@ -160,6 +172,10 @@ fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) {
SSKEnvironment.shared.notificationManager.updateNotification(context) SSKEnvironment.shared.notificationManager.updateNotification(context)
} }
} }
fun handleMessageRequestResponse(message: MessageRequestResponse) {
MessagingModuleConfiguration.shared.storage.insertMessageRequestResponse(message)
}
//endregion //endregion
// region Visible Messages // region Visible Messages
@ -177,10 +193,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
throw MessageReceiver.Error.NoThread throw MessageReceiver.Error.NoThread
} }
// Update profile if needed // Update profile if needed
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val profile = message.profile val profile = message.profile
if (profile != null && userPublicKey != message.sender) { if (profile != null && userPublicKey != message.sender) {
val profileManager = SSKEnvironment.shared.profileManager val profileManager = SSKEnvironment.shared.profileManager
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val name = profile.displayName!! val name = profile.displayName!!
if (name.isNotEmpty()) { if (name.isNotEmpty()) {
profileManager.setName(context, recipient, name) profileManager.setName(context, recipient, name)
@ -269,6 +285,8 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) { private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
if (!recipient.isApproved) return
val groupPublicKey = kind.publicKey.toByteArray().toHexString() val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() } val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() } val admins = kind.admins.map { it.toByteArray().toHexString() }

View File

@ -151,6 +151,8 @@ interface TextSecurePreferences {
fun setLastOpenDate() fun setLastOpenDate()
fun hasSeenLinkPreviewSuggestionDialog(): Boolean fun hasSeenLinkPreviewSuggestionDialog(): Boolean
fun setHasSeenLinkPreviewSuggestionDialog() fun setHasSeenLinkPreviewSuggestionDialog()
fun hasHiddenMessageRequests(): Boolean
fun setHasHiddenMessageRequests()
fun clearAll() fun clearAll()
companion object { companion object {
@ -227,6 +229,7 @@ interface TextSecurePreferences {
const val CONFIGURATION_SYNCED = "pref_configuration_synced" const val CONFIGURATION_SYNCED = "pref_configuration_synced"
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
const val LAST_OPEN_DATE = "pref_last_open_date" const val LAST_OPEN_DATE = "pref_last_open_date"
const val HAS_HIDDEN_MESSAGE_REQUESTS = "pref_message_requests_hidden"
@JvmStatic @JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long { fun getLastConfigurationSyncTime(context: Context): Long {
@ -870,6 +873,16 @@ interface TextSecurePreferences {
setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true) setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true)
} }
@JvmStatic
fun hasHiddenMessageRequests(context: Context): Boolean {
return getBooleanPreference(context, HAS_HIDDEN_MESSAGE_REQUESTS, false)
}
@JvmStatic
fun removeHasHiddenMessageRequests(context: Context) {
removePreference(context, HAS_HIDDEN_MESSAGE_REQUESTS)
}
@JvmStatic @JvmStatic
fun clearAll(context: Context) { fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit() getDefaultSharedPreferences(context).edit().clear().commit()
@ -1426,6 +1439,14 @@ class AppTextSecurePreferences @Inject constructor(
setBooleanPreference("has_seen_link_preview_suggestion_dialog", true) setBooleanPreference("has_seen_link_preview_suggestion_dialog", true)
} }
override fun hasHiddenMessageRequests(): Boolean {
return getBooleanPreference(TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS, false)
}
override fun setHasHiddenMessageRequests() {
setBooleanPreference(TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS, true)
}
override fun clearAll() { override fun clearAll() {
getDefaultSharedPreferences(context).edit().clear().commit() getDefaultSharedPreferences(context).edit().clear().commit()
} }

View File

@ -81,6 +81,8 @@ public class Recipient implements RecipientModifiedListener {
public long mutedUntil = 0; public long mutedUntil = 0;
public int notifyType = 0; public int notifyType = 0;
private boolean blocked = false; private boolean blocked = false;
private boolean approved = false;
private boolean approvedMe = false;
private VibrateState messageVibrate = VibrateState.DEFAULT; private VibrateState messageVibrate = VibrateState.DEFAULT;
private VibrateState callVibrate = VibrateState.DEFAULT; private VibrateState callVibrate = VibrateState.DEFAULT;
private int expireMessages = 0; private int expireMessages = 0;
@ -141,6 +143,8 @@ public class Recipient implements RecipientModifiedListener {
this.callRingtone = stale.callRingtone; this.callRingtone = stale.callRingtone;
this.mutedUntil = stale.mutedUntil; this.mutedUntil = stale.mutedUntil;
this.blocked = stale.blocked; this.blocked = stale.blocked;
this.approved = stale.approved;
this.approvedMe = stale.approvedMe;
this.messageVibrate = stale.messageVibrate; this.messageVibrate = stale.messageVibrate;
this.callVibrate = stale.callVibrate; this.callVibrate = stale.callVibrate;
this.expireMessages = stale.expireMessages; this.expireMessages = stale.expireMessages;
@ -169,6 +173,8 @@ public class Recipient implements RecipientModifiedListener {
this.callRingtone = details.get().callRingtone; this.callRingtone = details.get().callRingtone;
this.mutedUntil = details.get().mutedUntil; this.mutedUntil = details.get().mutedUntil;
this.blocked = details.get().blocked; this.blocked = details.get().blocked;
this.approved = details.get().approved;
this.approvedMe = details.get().approvedMe;
this.messageVibrate = details.get().messageVibrateState; this.messageVibrate = details.get().messageVibrateState;
this.callVibrate = details.get().callVibrateState; this.callVibrate = details.get().callVibrateState;
this.expireMessages = details.get().expireMessages; this.expireMessages = details.get().expireMessages;
@ -570,6 +576,30 @@ public class Recipient implements RecipientModifiedListener {
notifyListeners(); notifyListeners();
} }
public synchronized boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
synchronized (this) {
this.approved = approved;
}
notifyListeners();
}
public synchronized boolean hasApprovedMe() {
return approvedMe;
}
public void setHasApprovedMe(boolean approvedMe) {
synchronized (this) {
this.approvedMe = approvedMe;
}
notifyListeners();
}
public synchronized VibrateState getMessageVibrate() { public synchronized VibrateState getMessageVibrate() {
return messageVibrate; return messageVibrate;
} }
@ -779,6 +809,8 @@ public class Recipient implements RecipientModifiedListener {
public static class RecipientSettings { public static class RecipientSettings {
private final boolean blocked; private final boolean blocked;
private final boolean approved;
private final boolean approvedMe;
private final long muteUntil; private final long muteUntil;
private final int notifyType; private final int notifyType;
private final VibrateState messageVibrateState; private final VibrateState messageVibrateState;
@ -801,7 +833,7 @@ public class Recipient implements RecipientModifiedListener {
private final UnidentifiedAccessMode unidentifiedAccessMode; private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection; private final boolean forceSmsSelection;
public RecipientSettings(boolean blocked, long muteUntil, public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
int notifyType, int notifyType,
@NonNull VibrateState messageVibrateState, @NonNull VibrateState messageVibrateState,
@NonNull VibrateState callVibrateState, @NonNull VibrateState callVibrateState,
@ -824,6 +856,8 @@ public class Recipient implements RecipientModifiedListener {
boolean forceSmsSelection) boolean forceSmsSelection)
{ {
this.blocked = blocked; this.blocked = blocked;
this.approved = approved;
this.approvedMe = approvedMe;
this.muteUntil = muteUntil; this.muteUntil = muteUntil;
this.notifyType = notifyType; this.notifyType = notifyType;
this.messageVibrateState = messageVibrateState; this.messageVibrateState = messageVibrateState;
@ -855,6 +889,14 @@ public class Recipient implements RecipientModifiedListener {
return blocked; return blocked;
} }
public boolean isApproved() {
return approved;
}
public boolean hasApprovedMe() {
return approvedMe;
}
public long getMuteUntil() { public long getMuteUntil() {
return muteUntil; return muteUntil;
} }

View File

@ -171,6 +171,8 @@ class RecipientProvider {
@Nullable final VibrateState messageVibrateState; @Nullable final VibrateState messageVibrateState;
@Nullable final VibrateState callVibrateState; @Nullable final VibrateState callVibrateState;
final boolean blocked; final boolean blocked;
final boolean approved;
final boolean approvedMe;
final int expireMessages; final int expireMessages;
@NonNull final List<Recipient> participants; @NonNull final List<Recipient> participants;
@Nullable final String profileName; @Nullable final String profileName;
@ -201,6 +203,8 @@ class RecipientProvider {
this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null; this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null;
this.callVibrateState = settings != null ? settings.getCallVibrateState() : null; this.callVibrateState = settings != null ? settings.getCallVibrateState() : null;
this.blocked = settings != null && settings.isBlocked(); this.blocked = settings != null && settings.isBlocked();
this.approved = settings != null && settings.isApproved();
this.approvedMe = settings != null && settings.hasApprovedMe();
this.expireMessages = settings != null ? settings.getExpireMessages() : 0; this.expireMessages = settings != null ? settings.getExpireMessages() : 0;
this.participants = participants == null ? new LinkedList<>() : participants; this.participants = participants == null ? new LinkedList<>() : participants;
this.profileName = settings != null ? settings.getProfileName() : null; this.profileName = settings != null ? settings.getProfileName() : null;

View File

@ -49,6 +49,7 @@ message Content {
optional ConfigurationMessage configurationMessage = 7; optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8; optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9; optional UnsendRequest unsendRequest = 9;
optional MessageRequestResponse messageRequestResponse = 10;
} }
message KeyPair { message KeyPair {
@ -179,6 +180,9 @@ message ConfigurationMessage {
required string name = 2; required string name = 2;
optional string profilePicture = 3; optional string profilePicture = 3;
optional bytes profileKey = 4; optional bytes profileKey = 4;
optional bool isApproved = 5;
optional bool isBlocked = 6;
optional bool didApproveMe = 7;
} }
repeated ClosedGroup closedGroups = 1; repeated ClosedGroup closedGroups = 1;
@ -189,6 +193,11 @@ message ConfigurationMessage {
repeated Contact contacts = 6; repeated Contact contacts = 6;
} }
message MessageRequestResponse {
// @required
required bool isApproved = 1; // Whether the request was approved
}
message ReceiptMessage { message ReceiptMessage {
enum Type { enum Type {