Merge remote-tracking branch 'upstream/dev' into libsession-integration

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
#	libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
This commit is contained in:
0x330a 2023-05-12 16:29:26 +10:00
commit 42dbd11255
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
23 changed files with 304 additions and 126 deletions

View File

@ -7,6 +7,10 @@ import android.os.Build
import android.os.Handler
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
private const val TAG = "ScreenshotObserver"
class ScreenshotObserver(private val context: Context, handler: Handler, private val screenshotTriggered: ()->Unit): ContentObserver(handler) {
@ -31,22 +35,26 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
val projection = arrayOf(
MediaStore.Images.Media.DATA
)
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val path = cursor.getString(dataColumn)
if (path.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
try {
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val path = cursor.getString(dataColumn)
if (path.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
}
}
}
}
} catch (e: SecurityException) {
Log.e(TAG, e)
}
}
@ -56,28 +64,32 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.RELATIVE_PATH
)
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val relativePathColumn =
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
val displayNameColumn =
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
val name = cursor.getString(displayNameColumn)
val relativePath = cursor.getString(relativePathColumn)
if (name.contains("screenshot", true) or
relativePath.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
try {
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val relativePathColumn =
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
val displayNameColumn =
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
val name = cursor.getString(displayNameColumn)
val relativePath = cursor.getString(relativePathColumn)
if (name.contains("screenshot", true) or
relativePath.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
}
}
}
}
} catch (e: IllegalStateException) {
Log.e(TAG, e)
}
}
}
}

View File

@ -278,10 +278,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val isScrolledToBottom: Boolean
get() {
val position = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: 0
return position == 0
}
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
private val layoutManager: LinearLayoutManager?
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
@ -1233,12 +1230,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
})
val contentBounds = Rect()
visibleMessageView.messageContentView.getGlobalVisibleRect(contentBounds)
val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) }
val selectedConversationModel = SelectedConversationModel(
messageContentBitmap,
contentBounds.left.toFloat(),
contentBounds.top.toFloat(),
topLeft[0].toFloat(),
topLeft[1].toFloat(),
visibleMessageView.messageContentView.width,
message.isOutgoing,
visibleMessageView.messageContentView
@ -1879,6 +1875,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
override fun resyncMessage(messages: Set<MessageRecord>) {
messages.iterator().forEach { messageRecord ->
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey, isResync = true)
}
endActionMode()
}
override fun resendMessage(messages: Set<MessageRecord>) {
messages.iterator().forEach { messageRecord ->
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey)
@ -2039,6 +2042,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val selectedItems = setOf(message)
when (action) {
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)

View File

@ -660,7 +660,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT),
getContext().getResources().getString(R.string.AccessibilityId_select)));
// Reply
if (!message.isPending() && !message.isFailed()) {
boolean canWrite = openGroup == null || openGroup.getCanWrite();
if (canWrite && !message.isPending() && !message.isFailed()) {
items.add(
new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY),
getContext().getResources().getString(R.string.AccessibilityId_reply_message))
@ -700,6 +701,10 @@ public final class ConversationReactionOverlay extends FrameLayout {
if (message.isFailed()) {
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
}
// Resync
if (message.isSyncFailed()) {
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resync_message), () -> handleActionItemClicked(Action.RESYNC)));
}
// Save media
if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD),
@ -885,6 +890,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
public enum Action {
REPLY,
RESEND,
RESYNC,
DOWNLOAD,
COPY_MESSAGE,
COPY_SESSION_ID,

View File

@ -70,6 +70,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
// Resend
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
// Resync
menu.findItem(R.id.menu_context_resync).isVisible = (selectedItems.size == 1 && firstMessage.isSyncFailed)
// Save media
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
@ -90,6 +92,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
@ -113,6 +116,7 @@ interface ConversationActionModeCallbackDelegate {
fun banAndDeleteAll(messages: Set<MessageRecord>)
fun copyMessages(messages: Set<MessageRecord>)
fun copySessionID(messages: Set<MessageRecord>)
fun resyncMessage(messages: Set<MessageRecord>)
fun resendMessage(messages: Set<MessageRecord>)
fun showMessageDetail(messages: Set<MessageRecord>)
fun saveAttachment(messages: Set<MessageRecord>)

View File

@ -292,39 +292,46 @@ class VisibleMessageView : LinearLayout {
@StringRes val messageText: Int?,
val contentDescription: String?)
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo {
return when {
!message.isOutgoing -> MessageStatusInfo(null,
null,
null,
null)
message.isFailed ->
MessageStatusInfo(
R.drawable.ic_delivery_status_failed,
resources.getColor(R.color.destructive, context.theme),
R.string.delivery_status_failed,
null
)
message.isPending ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
context.getString(R.string.AccessibilityId_message_sent_status_pending)
)
message.isRead ->
MessageStatusInfo(
R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
null
)
else ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sent,
context.getString(R.string.AccessibilityId_message_sent_status_tick)
)
}
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
message.isFailed ->
MessageStatusInfo(
R.drawable.ic_delivery_status_failed,
resources.getColor(R.color.destructive, context.theme),
R.string.delivery_status_failed,
null
)
message.isSyncFailed ->
MessageStatusInfo(
R.drawable.ic_delivery_status_failed,
context.getColor(R.color.accent_orange),
R.string.delivery_status_sync_failed,
null
)
message.isPending ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
context.getString(R.string.AccessibilityId_message_sent_status_pending)
)
message.isResyncing ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing,
context.getString(R.string.AccessibilityId_message_sent_status_syncing)
)
message.isRead ->
MessageStatusInfo(
R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
null
)
else ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sent,
context.getString(R.string.AccessibilityId_message_sent_status_tick)
)
}
private fun updateExpirationTimer(message: MessageRecord) {

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.visible.LinkPreview
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.Quote
@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
object ResendMessageUtilities {
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?) {
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) {
val recipient: Recipient = messageRecord.recipient
val message = VisibleMessage()
message.id = messageRecord.getId()
@ -55,8 +56,13 @@ object ResendMessageUtilities {
val sentTimestamp = message.sentTimestamp
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
if (sentTimestamp != null && sender != null) {
MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender)
if (isResync) {
MessagingModuleConfiguration.shared.storage.markAsResyncing(sentTimestamp, sender)
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = true)
} else {
MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender)
MessageSender.send(message, recipient.address)
}
}
MessageSender.send(message, recipient.address)
}
}

View File

@ -38,13 +38,12 @@ object TextUtilities {
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
val textLayout = layout ?: return emptyList()
val lineRect = Rect()
val bodyTextRect = Rect()
getGlobalVisibleRect(bodyTextRect)
val offset = intArrayOf(0, 0).also { getLocationOnScreen(it) }
val textSpannable = text.toSpannable()
return (0 until textLayout.lineCount).flatMap { line ->
textLayout.getLineBounds(line, lineRect)
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
if ((Rect(lineRect)).contains(hitRect)) {
lineRect.offset(offset[0] + totalPaddingLeft, offset[1] + totalPaddingTop)
if (lineRect.contains(hitRect)) {
// calculate the url span intersected with (if any)
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
textSpannable.getSpans<ModalURLSpan>(off, off).toList()

View File

@ -45,44 +45,52 @@ public final class KeyStoreHelper {
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "SignalSecret";
@RequiresApi(Build.VERSION_CODES.M)
private static final Object lock = new Object();
public static SealedData seal(@NonNull byte[] input) {
SecretKey secretKey = getOrCreateKeyStoreEntry();
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
// prevent crashes with quickly repeated encrypt/decrypt operations
// https://github.com/mozilla-mobile/android-components/issues/5342
synchronized (lock) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV();
byte[] data = cipher.doFinal(input);
byte[] iv = cipher.getIV();
byte[] data = cipher.doFinal(input);
return new SealedData(iv, data);
return new SealedData(iv, data);
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
public static byte[] unseal(@NonNull SealedData sealedData) {
SecretKey secretKey = getKeyStoreEntry();
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
// prevent crashes with quickly repeated encrypt/decrypt operations
// https://github.com/mozilla-mobile/android-components/issues/5342
synchronized (lock) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
return cipher.doFinal(sealedData.data);
return cipher.doFinal(sealedData.data);
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey getOrCreateKeyStoreEntry() {
if (hasKeyStoreEntry()) return getKeyStoreEntry();
else return createKeyStoreEntry();
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey createKeyStoreEntry() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
@ -99,7 +107,6 @@ public final class KeyStoreHelper {
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey getKeyStoreEntry() {
KeyStore keyStore = getKeyStore();
@ -137,7 +144,6 @@ public final class KeyStoreHelper {
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static boolean hasKeyStoreEntry() {
try {
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);

View File

@ -37,6 +37,13 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markExpireStarted(long messageId, long startTime);
public abstract void markAsSent(long messageId, boolean secure);
public abstract void markAsSyncing(long id);
public abstract void markAsResyncing(long id);
public abstract void markAsSyncFailed(long id);
public abstract void markUnidentified(long messageId, boolean unidentified);
public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention);
@ -199,7 +206,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
contentValues.put(THREAD_ID, newThreadId);
db.update(getTableName(), contentValues, where, args);
}
public static class SyncMessageId {
private final Address address;

View File

@ -252,6 +252,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
notifyConversationListeners(threadId)
}
override fun markAsSyncing(messageId: Long) {
markAs(messageId, MmsSmsColumns.Types.BASE_SYNCING_TYPE)
}
override fun markAsResyncing(messageId: Long) {
markAs(messageId, MmsSmsColumns.Types.BASE_RESYNCING_TYPE)
}
override fun markAsSyncFailed(messageId: Long) {
markAs(messageId, MmsSmsColumns.Types.BASE_SYNC_FAILED_TYPE)
}
fun markAsSending(messageId: Long) {
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
}

View File

@ -47,8 +47,13 @@ public interface MmsSmsColumns {
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
public static final long BASE_DRAFT_TYPE = 27;
protected static final long BASE_DELETED_TYPE = 28;
protected static final long BASE_SYNCING_TYPE = 29;
protected static final long BASE_RESYNCING_TYPE = 30;
protected static final long BASE_SYNC_FAILED_TYPE = 31;
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
BASE_SYNCING_TYPE, BASE_RESYNCING_TYPE,
BASE_SYNC_FAILED_TYPE,
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
BASE_PENDING_SECURE_SMS_FALLBACK,
BASE_PENDING_INSECURE_SMS_FALLBACK,
@ -109,6 +114,18 @@ public interface MmsSmsColumns {
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
}
public static boolean isResyncingType(long type) {
return (type & BASE_TYPE_MASK) == BASE_RESYNCING_TYPE;
}
public static boolean isSyncingType(long type) {
return (type & BASE_TYPE_MASK) == BASE_SYNCING_TYPE;
}
public static boolean isSyncFailedMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_SYNC_FAILED_TYPE;
}
public static boolean isFailedMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
}

View File

@ -202,6 +202,21 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
}
@Override
public void markAsSyncing(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNCING_TYPE);
}
@Override
public void markAsResyncing(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_RESYNCING_TYPE);
}
@Override
public void markAsSyncFailed(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNC_FAILED_TYPE);
}
@Override
public void markUnidentified(long id, boolean unidentified) {
ContentValues contentValues = new ContentValues(1);

View File

@ -714,6 +714,22 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
}
}
override fun markAsSyncing(timestamp: Long, author: String) {
DatabaseComponent.get(context).mmsSmsDatabase()
.getMessageFor(timestamp, author)
?.run { getMmsDatabaseElseSms(isMms).markAsSyncing(id) }
}
private fun getMmsDatabaseElseSms(isMms: Boolean) =
if (isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
override fun markAsResyncing(timestamp: Long, author: String) {
DatabaseComponent.get(context).mmsSmsDatabase()
.getMessageFor(timestamp, author)
?.run { getMmsDatabaseElseSms(isMms).markAsResyncing(id) }
}
override fun markAsSending(timestamp: Long, author: String) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val messageRecord = database.getMessageFor(timestamp, author) ?: return
@ -739,7 +755,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
}
}
override fun setErrorMessage(timestamp: Long, author: String, error: Exception) {
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val messageRecord = database.getMessageFor(timestamp, author) ?: return
if (messageRecord.isMms) {
@ -762,6 +778,26 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
}
}
override fun markAsSyncFailed(timestamp: Long, author: String, error: Exception) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val messageRecord = database.getMessageFor(timestamp, author) ?: return
database.getMessageFor(timestamp, author)
?.run { getMmsDatabaseElseSms(isMms).markAsSyncFailed(id) }
if (error.localizedMessage != null) {
val message: String
if (error is OnionRequestAPI.HTTPRequestFailedAtDestinationException && error.statusCode == 429) {
message = "429: Rate limited."
} else {
message = error.localizedMessage!!
}
DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), message)
} else {
DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), error.javaClass.simpleName)
}
}
override fun clearErrorMessage(messageID: Long) {
val db = DatabaseComponent.get(context).lokiMessageDatabase()
db.clearErrorMessage(messageID)
@ -1538,5 +1574,4 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
return recipientDb.blockedContacts
}
}

View File

@ -80,6 +80,18 @@ public abstract class DisplayRecord {
return !isFailed() && !isPending();
}
public boolean isSyncing() {
return MmsSmsColumns.Types.isSyncingType(type);
}
public boolean isResyncing() {
return MmsSmsColumns.Types.isResyncingType(type);
}
public boolean isSyncFailed() {
return MmsSmsColumns.Types.isSyncFailedMessageType(type);
}
public boolean isFailed() {
return MmsSmsColumns.Types.isFailedMessageType(type)
|| MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type)

View File

@ -5,14 +5,16 @@ import android.animation.AnimatorListenerAdapter
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.PointF
import android.graphics.Rect
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.DimenRes
import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr
import android.view.inputmethod.InputMethodManager
import androidx.core.graphics.applyCanvas
fun View.contains(point: PointF): Boolean {
return hitRect.contains(point.x.toInt(), point.y.toInt())
@ -65,3 +67,9 @@ fun View.hideKeyboard() {
val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
}
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap =
Bitmap.createBitmap(width, height, config).applyCanvas {
translate(-scrollX.toFloat(), -scrollY.toFloat())
draw(this)
}

View File

@ -138,6 +138,7 @@
android:layout_height="50dp"
android:layout_alignParentEnd="true"
android:layout_above="@+id/messageRequestBar"
android:layout_alignWithParentIfMissing="true"
android:layout_marginEnd="12dp"
android:layout_marginBottom="32dp">

View File

@ -32,6 +32,11 @@
android:icon="?menu_copy_icon"
app:showAsAction="always" />
<item
android:title="@string/conversation_context__menu_resync_message"
android:id="@+id/menu_context_resync"
app:showAsAction="never" />
<item
android:title="@string/conversation_context__menu_resend_message"
android:id="@+id/menu_context_resend"

View File

@ -112,6 +112,7 @@
<!-- Conversation View-->
<string name="AccessibilityId_message_sent_status_tick">Message sent status: Sent</string>
<string name="AccessibilityId_message_sent_status_pending">Message sent status pending</string>
<string name="AccessibilityId_message_sent_status_syncing">Message sent status syncing</string>
<string name="AccessibilityId_message_request_config_message">Message request has been accepted</string>
<string name="AccessibilityId_message_body">Message Body</string>
<string name="AccessibilityId_voice_message">Voice message</string>
@ -627,6 +628,7 @@
<string name="conversation_context__menu_delete_message">Delete message</string>
<string name="conversation_context__menu_ban_user">Ban user</string>
<string name="conversation_context__menu_ban_and_delete_all">Ban and delete all</string>
<string name="conversation_context__menu_resync_message">Resync message</string>
<string name="conversation_context__menu_resend_message">Resend message</string>
<string name="conversation_context__menu_reply">Reply</string>
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
@ -1007,9 +1009,11 @@
<string name="new_conversation_dialog_close_button_content_description">Close Dialog</string>
<string name="ErrorNotifier_migration">Database Upgrade Failed</string>
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error.</string>
<string name="delivery_status_syncing">Syncing</string>
<string name="delivery_status_sending">Sending</string>
<string name="delivery_status_read">Read</string>
<string name="delivery_status_sent">Sent</string>
<string name="delivery_status_sync_failed">Failed to sync</string>
<string name="delivery_status_failed">Failed to send</string>
<string name="giphy_permission_title">Search GIFs?</string>
<string name="giphy_permission_message">Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.</string>

View File

@ -112,10 +112,13 @@ interface StorageProtocol {
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
fun markAsResyncing(timestamp: Long, author: String)
fun markAsSyncing(timestamp: Long, author: String)
fun markAsSending(timestamp: Long, author: String)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
fun clearErrorMessage(messageID: Long)
fun setMessageServerHash(messageID: Long, serverHash: String)

View File

@ -7,13 +7,13 @@ import org.session.libsignal.utilities.toHexString
sealed class Destination {
class Contact(var publicKey: String) : Destination() {
data class Contact(var publicKey: String) : Destination() {
internal constructor(): this("")
}
class ClosedGroup(var groupPublicKey: String) : Destination() {
data class ClosedGroup(var groupPublicKey: String) : Destination() {
internal constructor(): this("")
}
class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
data class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
internal constructor(): this("", "")
}

View File

@ -11,20 +11,23 @@ import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
class VisibleMessage : Message() {
/** In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
var syncTarget: String? = null
var text: String? = null
val attachmentIDs: MutableList<Long> = mutableListOf()
var quote: Quote? = null
var linkPreview: LinkPreview? = null
var profile: Profile? = null
var openGroupInvitation: OpenGroupInvitation? = null
var reaction: Reaction? = null
/**
* @param syncTarget In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
class VisibleMessage(
var syncTarget: String? = null,
var text: String? = null,
val attachmentIDs: MutableList<Long> = mutableListOf(),
var quote: Quote? = null,
var linkPreview: LinkPreview? = null,
var profile: Profile? = null,
var openGroupInvitation: OpenGroupInvitation? = null,
var reaction: Reaction? = null,
var hasMention: Boolean = false
) : Message() {
override val isSelfSendValid: Boolean = true
// region Validation

View File

@ -14,6 +14,7 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.LinkPreview
import org.session.libsession.messaging.messages.visible.Quote
@ -62,12 +63,11 @@ object MessageSender {
}
// Convenience
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
fun send(message: Message, destination: Destination, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
sendToOpenGroupDestination(destination, message)
} else {
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
sendToSnodeDestination(destination, message, destination is Destination.Contact && destination.publicKey == userPublicKey)
sendToSnodeDestination(destination, message, isSyncMessage)
}
}
@ -160,6 +160,7 @@ object MessageSender {
)
}
// One-on-One Chats & Closed Groups
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
val promise = deferred.promise
@ -171,7 +172,7 @@ object MessageSender {
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
handleFailedMessageSend(message, error, isSyncMessage)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
@ -393,16 +394,23 @@ object MessageSender {
// • the destination was a contact
// • we didn't sync it already
if (destination is Destination.Contact && !isSyncMessage) {
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
if (message is VisibleMessage) message.syncTarget = destination.publicKey
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
storage.markAsSyncing(message.sentTimestamp!!, userPublicKey)
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
}
}
fun handleFailedMessageSend(message: Message, error: Exception) {
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
val timestamp = message.sentTimestamp!!
val author = message.sender ?: userPublicKey
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)
else storage.markAsSentFailed(timestamp, author, error)
}
// Convenience

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
@ColorInt
fun Context.getColorFromAttr(
@ -13,4 +14,10 @@ fun Context.getColorFromAttr(
): Int {
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
return typedValue.data
}
}
val RecyclerView.isScrolledToBottom: Boolean
get() {
val contentHeight = height - (paddingTop + paddingBottom)
return computeVerticalScrollRange() == computeVerticalScrollOffset() + contentHeight
}