mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
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:
parent
55aa266769
commit
206505abe8
@ -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"
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 ->
|
||||||
|
@ -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() {
|
||||||
|
@ -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())
|
||||||
|
@ -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 + " = ?",
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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(),
|
||||||
|
@ -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)))
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
10
app/src/main/res/drawable/ic_delete_24.xml
Normal file
10
app/src/main/res/drawable/ic_delete_24.xml
Normal 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>
|
@ -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>
|
@ -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>
|
||||||
|
63
app/src/main/res/layout/activity_message_requests.xml
Normal file
63
app/src/main/res/layout/activity_message_requests.xml
Normal 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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
77
app/src/main/res/layout/view_message_request.xml
Normal file
77
app/src/main/res/layout/view_message_request.xml
Normal 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>
|
81
app/src/main/res/layout/view_message_request_banner.xml
Normal file
81
app/src/main/res/layout/view_message_request_banner.xml
Normal 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>
|
9
app/src/main/res/menu/menu_message_request.xml
Normal file
9
app/src/main/res/menu/menu_message_request.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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() }
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user