mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-29 04:55:15 +00:00
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:
commit
42dbd11255
@ -7,6 +7,10 @@ import android.os.Build
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
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) {
|
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(
|
val projection = arrayOf(
|
||||||
MediaStore.Images.Media.DATA
|
MediaStore.Images.Media.DATA
|
||||||
)
|
)
|
||||||
context.contentResolver.query(
|
try {
|
||||||
uri,
|
context.contentResolver.query(
|
||||||
projection,
|
uri,
|
||||||
null,
|
projection,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
)?.use { cursor ->
|
null
|
||||||
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
|
)?.use { cursor ->
|
||||||
while (cursor.moveToNext()) {
|
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
|
||||||
val path = cursor.getString(dataColumn)
|
while (cursor.moveToNext()) {
|
||||||
if (path.contains("screenshot", true)) {
|
val path = cursor.getString(dataColumn)
|
||||||
if (cache.add(uri.hashCode())) {
|
if (path.contains("screenshot", true)) {
|
||||||
screenshotTriggered()
|
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.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.RELATIVE_PATH
|
MediaStore.Images.Media.RELATIVE_PATH
|
||||||
)
|
)
|
||||||
context.contentResolver.query(
|
|
||||||
uri,
|
try {
|
||||||
projection,
|
context.contentResolver.query(
|
||||||
null,
|
uri,
|
||||||
null,
|
projection,
|
||||||
null
|
null,
|
||||||
)?.use { cursor ->
|
null,
|
||||||
val relativePathColumn =
|
null
|
||||||
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
|
)?.use { cursor ->
|
||||||
val displayNameColumn =
|
val relativePathColumn =
|
||||||
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
|
||||||
while (cursor.moveToNext()) {
|
val displayNameColumn =
|
||||||
val name = cursor.getString(displayNameColumn)
|
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
val relativePath = cursor.getString(relativePathColumn)
|
while (cursor.moveToNext()) {
|
||||||
if (name.contains("screenshot", true) or
|
val name = cursor.getString(displayNameColumn)
|
||||||
relativePath.contains("screenshot", true)) {
|
val relativePath = cursor.getString(relativePathColumn)
|
||||||
if (cache.add(uri.hashCode())) {
|
if (name.contains("screenshot", true) or
|
||||||
screenshotTriggered()
|
relativePath.contains("screenshot", true)) {
|
||||||
|
if (cache.add(uri.hashCode())) {
|
||||||
|
screenshotTriggered()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Log.e(TAG, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -278,10 +278,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
|
||||||
private val isScrolledToBottom: Boolean
|
private val isScrolledToBottom: Boolean
|
||||||
get() {
|
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
||||||
val position = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: 0
|
|
||||||
return position == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private val layoutManager: LinearLayoutManager?
|
private val layoutManager: LinearLayoutManager?
|
||||||
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
||||||
@ -1233,12 +1230,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
val contentBounds = Rect()
|
val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) }
|
||||||
visibleMessageView.messageContentView.getGlobalVisibleRect(contentBounds)
|
|
||||||
val selectedConversationModel = SelectedConversationModel(
|
val selectedConversationModel = SelectedConversationModel(
|
||||||
messageContentBitmap,
|
messageContentBitmap,
|
||||||
contentBounds.left.toFloat(),
|
topLeft[0].toFloat(),
|
||||||
contentBounds.top.toFloat(),
|
topLeft[1].toFloat(),
|
||||||
visibleMessageView.messageContentView.width,
|
visibleMessageView.messageContentView.width,
|
||||||
message.isOutgoing,
|
message.isOutgoing,
|
||||||
visibleMessageView.messageContentView
|
visibleMessageView.messageContentView
|
||||||
@ -1879,6 +1875,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
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>) {
|
override fun resendMessage(messages: Set<MessageRecord>) {
|
||||||
messages.iterator().forEach { messageRecord ->
|
messages.iterator().forEach { messageRecord ->
|
||||||
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey)
|
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey)
|
||||||
@ -2039,6 +2042,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val selectedItems = setOf(message)
|
val selectedItems = setOf(message)
|
||||||
when (action) {
|
when (action) {
|
||||||
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
|
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
|
||||||
|
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
|
||||||
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
|
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
|
||||||
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
|
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
|
||||||
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
|
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
|
||||||
|
@ -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),
|
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)));
|
getContext().getResources().getString(R.string.AccessibilityId_select)));
|
||||||
// Reply
|
// Reply
|
||||||
if (!message.isPending() && !message.isFailed()) {
|
boolean canWrite = openGroup == null || openGroup.getCanWrite();
|
||||||
|
if (canWrite && !message.isPending() && !message.isFailed()) {
|
||||||
items.add(
|
items.add(
|
||||||
new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY),
|
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))
|
getContext().getResources().getString(R.string.AccessibilityId_reply_message))
|
||||||
@ -700,6 +701,10 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||||||
if (message.isFailed()) {
|
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)));
|
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
|
// Save media
|
||||||
if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
|
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),
|
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 {
|
public enum Action {
|
||||||
REPLY,
|
REPLY,
|
||||||
RESEND,
|
RESEND,
|
||||||
|
RESYNC,
|
||||||
DOWNLOAD,
|
DOWNLOAD,
|
||||||
COPY_MESSAGE,
|
COPY_MESSAGE,
|
||||||
COPY_SESSION_ID,
|
COPY_SESSION_ID,
|
||||||
|
@ -70,6 +70,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
||||||
// Resend
|
// Resend
|
||||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
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
|
// Save media
|
||||||
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
|
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
|
||||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
&& 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_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
|
||||||
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
||||||
R.id.menu_context_copy_public_key -> delegate?.copySessionID(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_context_resend -> delegate?.resendMessage(selectedItems)
|
||||||
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
||||||
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
||||||
@ -113,6 +116,7 @@ interface ConversationActionModeCallbackDelegate {
|
|||||||
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
||||||
fun copyMessages(messages: Set<MessageRecord>)
|
fun copyMessages(messages: Set<MessageRecord>)
|
||||||
fun copySessionID(messages: Set<MessageRecord>)
|
fun copySessionID(messages: Set<MessageRecord>)
|
||||||
|
fun resyncMessage(messages: Set<MessageRecord>)
|
||||||
fun resendMessage(messages: Set<MessageRecord>)
|
fun resendMessage(messages: Set<MessageRecord>)
|
||||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||||
fun saveAttachment(messages: Set<MessageRecord>)
|
fun saveAttachment(messages: Set<MessageRecord>)
|
||||||
|
@ -292,39 +292,46 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@StringRes val messageText: Int?,
|
@StringRes val messageText: Int?,
|
||||||
val contentDescription: String?)
|
val contentDescription: String?)
|
||||||
|
|
||||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo {
|
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
||||||
return when {
|
message.isFailed ->
|
||||||
!message.isOutgoing -> MessageStatusInfo(null,
|
MessageStatusInfo(
|
||||||
null,
|
R.drawable.ic_delivery_status_failed,
|
||||||
null,
|
resources.getColor(R.color.destructive, context.theme),
|
||||||
null)
|
R.string.delivery_status_failed,
|
||||||
message.isFailed ->
|
null
|
||||||
MessageStatusInfo(
|
)
|
||||||
R.drawable.ic_delivery_status_failed,
|
message.isSyncFailed ->
|
||||||
resources.getColor(R.color.destructive, context.theme),
|
MessageStatusInfo(
|
||||||
R.string.delivery_status_failed,
|
R.drawable.ic_delivery_status_failed,
|
||||||
null
|
context.getColor(R.color.accent_orange),
|
||||||
)
|
R.string.delivery_status_sync_failed,
|
||||||
message.isPending ->
|
null
|
||||||
MessageStatusInfo(
|
)
|
||||||
R.drawable.ic_delivery_status_sending,
|
message.isPending ->
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
MessageStatusInfo(
|
||||||
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
R.drawable.ic_delivery_status_sending,
|
||||||
)
|
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
||||||
message.isRead ->
|
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
||||||
MessageStatusInfo(
|
)
|
||||||
R.drawable.ic_delivery_status_read,
|
message.isResyncing ->
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
|
MessageStatusInfo(
|
||||||
null
|
R.drawable.ic_delivery_status_sending,
|
||||||
)
|
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing,
|
||||||
else ->
|
context.getString(R.string.AccessibilityId_message_sent_status_syncing)
|
||||||
MessageStatusInfo(
|
)
|
||||||
R.drawable.ic_delivery_status_sent,
|
message.isRead ->
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
MessageStatusInfo(
|
||||||
R.string.delivery_status_sent,
|
R.drawable.ic_delivery_status_read,
|
||||||
context.getString(R.string.AccessibilityId_message_sent_status_tick)
|
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) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
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.LinkPreview
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||||
import org.session.libsession.messaging.messages.visible.Quote
|
import org.session.libsession.messaging.messages.visible.Quote
|
||||||
@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
|||||||
|
|
||||||
object ResendMessageUtilities {
|
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 recipient: Recipient = messageRecord.recipient
|
||||||
val message = VisibleMessage()
|
val message = VisibleMessage()
|
||||||
message.id = messageRecord.getId()
|
message.id = messageRecord.getId()
|
||||||
@ -55,8 +56,13 @@ object ResendMessageUtilities {
|
|||||||
val sentTimestamp = message.sentTimestamp
|
val sentTimestamp = message.sentTimestamp
|
||||||
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
||||||
if (sentTimestamp != null && sender != null) {
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -38,13 +38,12 @@ object TextUtilities {
|
|||||||
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
||||||
val textLayout = layout ?: return emptyList()
|
val textLayout = layout ?: return emptyList()
|
||||||
val lineRect = Rect()
|
val lineRect = Rect()
|
||||||
val bodyTextRect = Rect()
|
val offset = intArrayOf(0, 0).also { getLocationOnScreen(it) }
|
||||||
getGlobalVisibleRect(bodyTextRect)
|
|
||||||
val textSpannable = text.toSpannable()
|
val textSpannable = text.toSpannable()
|
||||||
return (0 until textLayout.lineCount).flatMap { line ->
|
return (0 until textLayout.lineCount).flatMap { line ->
|
||||||
textLayout.getLineBounds(line, lineRect)
|
textLayout.getLineBounds(line, lineRect)
|
||||||
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
|
lineRect.offset(offset[0] + totalPaddingLeft, offset[1] + totalPaddingTop)
|
||||||
if ((Rect(lineRect)).contains(hitRect)) {
|
if (lineRect.contains(hitRect)) {
|
||||||
// calculate the url span intersected with (if any)
|
// calculate the url span intersected with (if any)
|
||||||
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
||||||
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
||||||
|
@ -45,44 +45,52 @@ public final class KeyStoreHelper {
|
|||||||
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
||||||
private static final String KEY_ALIAS = "SignalSecret";
|
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) {
|
public static SealedData seal(@NonNull byte[] input) {
|
||||||
SecretKey secretKey = getOrCreateKeyStoreEntry();
|
SecretKey secretKey = getOrCreateKeyStoreEntry();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
// 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[] iv = cipher.getIV();
|
||||||
byte[] data = cipher.doFinal(input);
|
byte[] data = cipher.doFinal(input);
|
||||||
|
|
||||||
return new SealedData(iv, data);
|
return new SealedData(iv, data);
|
||||||
|
}
|
||||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
public static byte[] unseal(@NonNull SealedData sealedData) {
|
public static byte[] unseal(@NonNull SealedData sealedData) {
|
||||||
SecretKey secretKey = getKeyStoreEntry();
|
SecretKey secretKey = getKeyStoreEntry();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
|
// 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) {
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private static SecretKey getOrCreateKeyStoreEntry() {
|
private static SecretKey getOrCreateKeyStoreEntry() {
|
||||||
if (hasKeyStoreEntry()) return getKeyStoreEntry();
|
if (hasKeyStoreEntry()) return getKeyStoreEntry();
|
||||||
else return createKeyStoreEntry();
|
else return createKeyStoreEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private static SecretKey createKeyStoreEntry() {
|
private static SecretKey createKeyStoreEntry() {
|
||||||
try {
|
try {
|
||||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
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() {
|
private static SecretKey getKeyStoreEntry() {
|
||||||
KeyStore keyStore = getKeyStore();
|
KeyStore keyStore = getKeyStore();
|
||||||
|
|
||||||
@ -137,7 +144,6 @@ public final class KeyStoreHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private static boolean hasKeyStoreEntry() {
|
private static boolean hasKeyStoreEntry() {
|
||||||
try {
|
try {
|
||||||
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
|
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||||
|
@ -37,6 +37,13 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
public abstract void markExpireStarted(long messageId, long startTime);
|
public abstract void markExpireStarted(long messageId, long startTime);
|
||||||
|
|
||||||
public abstract void markAsSent(long messageId, boolean secure);
|
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 markUnidentified(long messageId, boolean unidentified);
|
||||||
|
|
||||||
public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention);
|
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);
|
contentValues.put(THREAD_ID, newThreadId);
|
||||||
db.update(getTableName(), contentValues, where, args);
|
db.update(getTableName(), contentValues, where, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SyncMessageId {
|
public static class SyncMessageId {
|
||||||
|
|
||||||
private final Address address;
|
private final Address address;
|
||||||
|
@ -252,6 +252,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
notifyConversationListeners(threadId)
|
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) {
|
fun markAsSending(messageId: Long) {
|
||||||
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
|
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,13 @@ public interface MmsSmsColumns {
|
|||||||
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
||||||
public static final long BASE_DRAFT_TYPE = 27;
|
public static final long BASE_DRAFT_TYPE = 27;
|
||||||
protected static final long BASE_DELETED_TYPE = 28;
|
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,
|
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_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
||||||
BASE_PENDING_SECURE_SMS_FALLBACK,
|
BASE_PENDING_SECURE_SMS_FALLBACK,
|
||||||
BASE_PENDING_INSECURE_SMS_FALLBACK,
|
BASE_PENDING_INSECURE_SMS_FALLBACK,
|
||||||
@ -109,6 +114,18 @@ public interface MmsSmsColumns {
|
|||||||
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
|
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) {
|
public static boolean isFailedMessageType(long type) {
|
||||||
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
|
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
|
||||||
}
|
}
|
||||||
|
@ -202,6 +202,21 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
|
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
|
@Override
|
||||||
public void markUnidentified(long id, boolean unidentified) {
|
public void markUnidentified(long id, boolean unidentified) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
@ -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) {
|
override fun markAsSending(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
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 database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||||
if (messageRecord.isMms) {
|
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) {
|
override fun clearErrorMessage(messageID: Long) {
|
||||||
val db = DatabaseComponent.get(context).lokiMessageDatabase()
|
val db = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||||
db.clearErrorMessage(messageID)
|
db.clearErrorMessage(messageID)
|
||||||
@ -1538,5 +1574,4 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
|||||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||||
return recipientDb.blockedContacts
|
return recipientDb.blockedContacts
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -80,6 +80,18 @@ public abstract class DisplayRecord {
|
|||||||
return !isFailed() && !isPending();
|
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() {
|
public boolean isFailed() {
|
||||||
return MmsSmsColumns.Types.isFailedMessageType(type)
|
return MmsSmsColumns.Types.isFailedMessageType(type)
|
||||||
|| MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type)
|
|| MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type)
|
||||||
|
@ -5,14 +5,16 @@ import android.animation.AnimatorListenerAdapter
|
|||||||
import android.animation.FloatEvaluator
|
import android.animation.FloatEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.core.graphics.applyCanvas
|
||||||
|
|
||||||
fun View.contains(point: PointF): Boolean {
|
fun View.contains(point: PointF): Boolean {
|
||||||
return hitRect.contains(point.x.toInt(), point.y.toInt())
|
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
|
val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
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)
|
||||||
|
}
|
||||||
|
@ -138,6 +138,7 @@
|
|||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_above="@+id/messageRequestBar"
|
android:layout_above="@+id/messageRequestBar"
|
||||||
|
android:layout_alignWithParentIfMissing="true"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="32dp">
|
android:layout_marginBottom="32dp">
|
||||||
|
|
||||||
|
@ -32,6 +32,11 @@
|
|||||||
android:icon="?menu_copy_icon"
|
android:icon="?menu_copy_icon"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:title="@string/conversation_context__menu_resync_message"
|
||||||
|
android:id="@+id/menu_context_resync"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:title="@string/conversation_context__menu_resend_message"
|
android:title="@string/conversation_context__menu_resend_message"
|
||||||
android:id="@+id/menu_context_resend"
|
android:id="@+id/menu_context_resend"
|
||||||
|
@ -112,6 +112,7 @@
|
|||||||
<!-- Conversation View-->
|
<!-- Conversation View-->
|
||||||
<string name="AccessibilityId_message_sent_status_tick">Message sent status: Sent</string>
|
<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_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_request_config_message">Message request has been accepted</string>
|
||||||
<string name="AccessibilityId_message_body">Message Body</string>
|
<string name="AccessibilityId_message_body">Message Body</string>
|
||||||
<string name="AccessibilityId_voice_message">Voice message</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_delete_message">Delete message</string>
|
||||||
<string name="conversation_context__menu_ban_user">Ban user</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_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_resend_message">Resend message</string>
|
||||||
<string name="conversation_context__menu_reply">Reply</string>
|
<string name="conversation_context__menu_reply">Reply</string>
|
||||||
<string name="conversation_context__menu_reply_to_message">Reply to message</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="new_conversation_dialog_close_button_content_description">Close Dialog</string>
|
||||||
<string name="ErrorNotifier_migration">Database Upgrade Failed</string>
|
<string name="ErrorNotifier_migration">Database Upgrade Failed</string>
|
||||||
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error.</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_sending">Sending</string>
|
||||||
<string name="delivery_status_read">Read</string>
|
<string name="delivery_status_read">Read</string>
|
||||||
<string name="delivery_status_sent">Sent</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="delivery_status_failed">Failed to send</string>
|
||||||
<string name="giphy_permission_title">Search GIFs?</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>
|
<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>
|
||||||
|
@ -112,10 +112,13 @@ interface StorageProtocol {
|
|||||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||||
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
|
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 markAsSending(timestamp: Long, author: String)
|
||||||
fun markAsSent(timestamp: Long, author: String)
|
fun markAsSent(timestamp: Long, author: String)
|
||||||
fun markUnidentified(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 clearErrorMessage(messageID: Long)
|
||||||
fun setMessageServerHash(messageID: Long, serverHash: String)
|
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@ import org.session.libsignal.utilities.toHexString
|
|||||||
|
|
||||||
sealed class Destination {
|
sealed class Destination {
|
||||||
|
|
||||||
class Contact(var publicKey: String) : Destination() {
|
data class Contact(var publicKey: String) : Destination() {
|
||||||
internal constructor(): this("")
|
internal constructor(): this("")
|
||||||
}
|
}
|
||||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
data class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||||
internal constructor(): this("")
|
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("", "")
|
internal constructor(): this("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,20 +11,23 @@ import org.session.libsignal.protos.SignalServiceProtos
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
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.
|
* @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.
|
* **Note:** `nil` if this isn't a sync message.
|
||||||
*/
|
*/
|
||||||
var syncTarget: String? = null
|
class VisibleMessage(
|
||||||
var text: String? = null
|
var syncTarget: String? = null,
|
||||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
var text: String? = null,
|
||||||
var quote: Quote? = null
|
val attachmentIDs: MutableList<Long> = mutableListOf(),
|
||||||
var linkPreview: LinkPreview? = null
|
var quote: Quote? = null,
|
||||||
var profile: Profile? = null
|
var linkPreview: LinkPreview? = null,
|
||||||
var openGroupInvitation: OpenGroupInvitation? = null
|
var profile: Profile? = null,
|
||||||
var reaction: Reaction? = null
|
var openGroupInvitation: OpenGroupInvitation? = null,
|
||||||
|
var reaction: Reaction? = null,
|
||||||
var hasMention: Boolean = false
|
var hasMention: Boolean = false
|
||||||
|
) : Message() {
|
||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
// region Validation
|
// region Validation
|
||||||
|
@ -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.ExpirationTimerUpdate
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
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.SharedConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||||
import org.session.libsession.messaging.messages.visible.Quote
|
import org.session.libsession.messaging.messages.visible.Quote
|
||||||
@ -62,12 +63,11 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience
|
// 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) {
|
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
||||||
sendToOpenGroupDestination(destination, message)
|
sendToOpenGroupDestination(destination, message)
|
||||||
} else {
|
} else {
|
||||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
sendToSnodeDestination(destination, message, isSyncMessage)
|
||||||
sendToSnodeDestination(destination, message, destination is Destination.Contact && destination.publicKey == userPublicKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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> {
|
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val promise = deferred.promise
|
val promise = deferred.promise
|
||||||
@ -171,7 +172,7 @@ object MessageSender {
|
|||||||
|
|
||||||
// Set the failure handler (need it here already for precondition failure handling)
|
// Set the failure handler (need it here already for precondition failure handling)
|
||||||
fun handleFailure(error: Exception) {
|
fun handleFailure(error: Exception) {
|
||||||
handleFailedMessageSend(message, error)
|
handleFailedMessageSend(message, error, isSyncMessage)
|
||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
|
||||||
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
@ -393,16 +394,23 @@ object MessageSender {
|
|||||||
// • the destination was a contact
|
// • the destination was a contact
|
||||||
// • we didn't sync it already
|
// • we didn't sync it already
|
||||||
if (destination is Destination.Contact && !isSyncMessage) {
|
if (destination is Destination.Contact && !isSyncMessage) {
|
||||||
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
if (message is VisibleMessage) message.syncTarget = destination.publicKey
|
||||||
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
|
||||||
|
|
||||||
|
storage.markAsSyncing(message.sentTimestamp!!, userPublicKey)
|
||||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
|
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 storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
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
|
// Convenience
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun Context.getColorFromAttr(
|
fun Context.getColorFromAttr(
|
||||||
@ -14,3 +15,9 @@ fun Context.getColorFromAttr(
|
|||||||
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
||||||
return typedValue.data
|
return typedValue.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val RecyclerView.isScrolledToBottom: Boolean
|
||||||
|
get() {
|
||||||
|
val contentHeight = height - (paddingTop + paddingBottom)
|
||||||
|
return computeVerticalScrollRange() == computeVerticalScrollOffset() + contentHeight
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user