Merge branch 'dev' into sync-everything

This commit is contained in:
andrew 2023-05-30 11:34:17 +09:30
commit 0af713317a
60 changed files with 466 additions and 285 deletions

View File

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

View File

@ -21,7 +21,6 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.drawToBitmap
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -212,10 +211,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
var searchViewItem: MenuItem? = null var searchViewItem: MenuItem? = null
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? }
@ -667,7 +663,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun updateSendAfterApprovalText() { private fun updateSendAfterApprovalText() {
binding?.textSendAfterApproval?.isGone = viewModel.recipient?.hasApprovedMe() ?: true binding?.textSendAfterApproval?.isVisible = viewModel.showSendAfterApprovalText
} }
private fun showOrHideInputIfNeeded() { private fun showOrHideInputIfNeeded() {
@ -1109,12 +1105,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
@ -1755,6 +1750,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)
@ -1915,6 +1917,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)

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), 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,

View File

@ -31,6 +31,9 @@ class ConversationViewModel(
private val storage: Storage private val storage: Storage
) : ViewModel() { ) : ViewModel() {
val showSendAfterApprovalText: Boolean
get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false
private val _uiState = MutableStateFlow(ConversationUiState()) private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> = _uiState val uiState: StateFlow<ConversationUiState> = _uiState

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) 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>)

View File

@ -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) {

View File

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

View File

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

View File

@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.crypto;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties; import android.security.keystore.KeyProperties;
import android.util.Base64; import android.util.Base64;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
@ -51,6 +49,9 @@ public final class KeyStoreHelper {
SecretKey secretKey = getOrCreateKeyStoreEntry(); SecretKey secretKey = getOrCreateKeyStoreEntry();
try { try {
// 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 (CIPHER_LOCK) { synchronized (CIPHER_LOCK) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.init(Cipher.ENCRYPT_MODE, secretKey);
@ -69,6 +70,9 @@ public final class KeyStoreHelper {
SecretKey secretKey = getKeyStoreEntry(); SecretKey secretKey = getKeyStoreEntry();
try { try {
// 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 (CIPHER_LOCK) { synchronized (CIPHER_LOCK) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));

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 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;

View File

@ -276,6 +276,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)
} }

View File

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

View File

@ -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);

View File

@ -377,6 +377,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
} }
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
@ -402,7 +418,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
} }
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) {
@ -425,6 +441,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
} }
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)
@ -983,5 +1019,4 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val recipientDb = DatabaseComponent.get(context).recipientDatabase() val recipientDb = DatabaseComponent.get(context).recipientDatabase()
return recipientDb.blockedContacts return recipientDb.blockedContacts
} }
} }

View File

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

View File

@ -42,6 +42,7 @@ class ClearAllDataDialog : BaseDialog() {
var selectedOption = device var selectedOption = device
val optionAdapter = RadioOptionAdapter { selectedOption = it } val optionAdapter = RadioOptionAdapter { selectedOption = it }
binding.recyclerView.apply { binding.recyclerView.apply {
itemAnimator = null
adapter = optionAdapter adapter = optionAdapter
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
setHasFixedSize(true) setHasFixedSize(true)

View File

@ -1,42 +1,41 @@
package org.thoughtcrime.securesms.preferences package org.thoughtcrime.securesms.preferences
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.recyclerview.widget.DividerItemDecoration
import network.loki.messenger.databinding.DialogListPreferenceBinding import network.loki.messenger.databinding.DialogListPreferenceBinding
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
class ListPreferenceDialog( fun listPreferenceDialog(
private val listPreference: ListPreference, context: Context,
private val dialogListener: () -> Unit listPreference: ListPreference,
) : BaseDialog() { dialogListener: () -> Unit
private lateinit var binding: DialogListPreferenceBinding ) : AlertDialog {
override fun setContentView(builder: AlertDialog.Builder) { val builder = AlertDialog.Builder(context)
binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(requireContext()))
binding.titleTextView.text = listPreference.dialogTitle val binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(context))
binding.messageTextView.text = listPreference.dialogMessage binding.titleTextView.text = listPreference.dialogTitle
binding.closeButton.setOnClickListener { binding.messageTextView.text = listPreference.dialogMessage
dismiss()
} builder.setView(binding.root)
val options = listPreference.entryValues.zip(listPreference.entries) { value, title ->
RadioOption(value.toString(), title.toString()) val dialog = builder.show()
}
val valueIndex = listPreference.findIndexOfValue(listPreference.value) val valueIndex = listPreference.findIndexOfValue(listPreference.value)
val optionAdapter = RadioOptionAdapter(valueIndex) { RadioOptionAdapter(valueIndex) {
listPreference.value = it.value listPreference.value = it.value
dismiss() dialog.dismiss()
dialogListener.invoke() dialogListener()
}
binding.recyclerView.apply {
adapter = optionAdapter
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
setHasFixedSize(true)
}
optionAdapter.submitList(options)
builder.setView(binding.root)
builder.setCancelable(false)
} }
.apply {
listPreference.entryValues.zip(listPreference.entries) { value, title ->
RadioOption(value.toString(), title.toString())
}.let(this::submitList)
}
.let { binding.recyclerView.adapter = it }
binding.closeButton.setOnClickListener { dialog.dismiss() }
return dialog
} }

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.preferences;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -77,10 +79,10 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
.setOnPreferenceClickListener(preference -> { .setOnPreferenceClickListener(preference -> {
ListPreference listPreference = (ListPreference) preference; ListPreference listPreference = (ListPreference) preference;
listPreference.setDialogMessage(R.string.preferences_notifications__content_message); listPreference.setDialogMessage(R.string.preferences_notifications__content_message);
new ListPreferenceDialog(listPreference, () -> { listPreferenceDialog(getContext(), listPreference, () -> {
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
return null; return null;
}).show(getChildFragmentManager(), "ListPreferenceDialog"); });
return true; return true;
}); });

View File

@ -16,8 +16,8 @@ class RadioOptionAdapter(
) : ListAdapter<RadioOption, RadioOptionAdapter.ViewHolder>(RadioOptionDiffer()) { ) : ListAdapter<RadioOption, RadioOptionAdapter.ViewHolder>(RadioOptionDiffer()) {
class RadioOptionDiffer: DiffUtil.ItemCallback<RadioOption>() { class RadioOptionDiffer: DiffUtil.ItemCallback<RadioOption>() {
override fun areItemsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem === newItem override fun areItemsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem.title == newItem.title
override fun areContentsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem == newItem override fun areContentsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem.value == newItem.value
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -31,7 +31,7 @@ class RadioOptionAdapter(
holder.bind(option, isSelected) { holder.bind(option, isSelected) {
onClickListener(it) onClickListener(it)
selectedOptionPosition = position selectedOptionPosition = position
notifyDataSetChanged() notifyItemRangeChanged(0, itemCount)
} }
} }

View File

@ -2,10 +2,7 @@ package org.thoughtcrime.securesms.preferences
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.*
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
@ -19,6 +16,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import network.loki.messenger.BuildConfig import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
@ -28,13 +26,11 @@ import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.utilities.Address import org.session.libsession.utilities.*
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
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.components.ProfilePictureView
import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.home.PathActivity
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
@ -57,8 +53,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private var displayNameEditActionMode: ActionMode? = null private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() } set(value) { field = value; handleDisplayNameEditActionModeChanged() }
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var displayNameToBeUploaded: String? = null
private var profilePictureToBeUploaded: ByteArray? = null
private var tempFile: File? = null private var tempFile: File? = null
private val hexEncodedPublicKey: String private val hexEncodedPublicKey: String
@ -76,14 +70,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey val displayName = getDisplayName()
glide = GlideApp.with(this) glide = GlideApp.with(this)
with(binding) { with(binding) {
profilePictureView.root.glide = glide setupProfilePictureView(profilePictureView.root)
profilePictureView.root.publicKey = hexEncodedPublicKey
profilePictureView.root.displayName = displayName
profilePictureView.root.isLarge = true
profilePictureView.root.update()
profilePictureView.root.setOnClickListener { showEditProfilePictureUI() } profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = displayName btnGroupNameDisplay.text = displayName
@ -105,6 +95,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun getDisplayName(): String =
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
view.glide = glide
view.publicKey = hexEncodedPublicKey
view.displayName = getDisplayName()
view.isLarge = true
view.update()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
val scrollBundle = SparseArray<Parcelable>() val scrollBundle = SparseArray<Parcelable>()
@ -154,9 +155,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
AsyncTask.execute { AsyncTask.execute {
try { try {
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
updateProfile(true) updateProfile(true, profilePictureToBeUploaded)
} }
} catch (e: BitmapDecodingException) { } catch (e: BitmapDecodingException) {
e.printStackTrace() e.printStackTrace()
@ -190,23 +191,30 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun updateProfile(isUpdatingProfilePicture: Boolean) { private fun updateProfile(
isUpdatingProfilePicture: Boolean,
profilePicture: ByteArray? = null,
displayName: String? = null
) {
binding.loader.isVisible = true binding.loader.isVisible = true
val promises = mutableListOf<Promise<*, Exception>>() val promises = mutableListOf<Promise<*, Exception>>()
val displayName = displayNameToBeUploaded
if (displayName != null) { if (displayName != null) {
TextSecurePreferences.setProfileName(this, displayName) TextSecurePreferences.setProfileName(this, displayName)
} }
val profilePicture = profilePictureToBeUploaded
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture) {
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) if (profilePicture != null) {
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
} else {
TextSecurePreferences.setLastProfilePictureUpload(this, System.currentTimeMillis())
TextSecurePreferences.setProfilePictureURL(this, null)
}
} }
val compoundPromise = all(promises) val compoundPromise = all(promises)
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time) TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
} }
@ -218,12 +226,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
if (displayName != null) { if (displayName != null) {
binding.btnGroupNameDisplay.text = displayName binding.btnGroupNameDisplay.text = displayName
} }
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture) {
binding.profilePictureView.root.recycle() // Clear the cached image before updating binding.profilePictureView.root.recycle() // Clear the cached image before updating
binding.profilePictureView.root.update() binding.profilePictureView.root.update()
} }
displayNameToBeUploaded = null
profilePictureToBeUploaded = null
binding.loader.isVisible = false binding.loader.isVisible = false
} }
} }
@ -244,8 +250,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
return false return false
} }
displayNameToBeUploaded = displayName updateProfile(false, displayName = displayName)
updateProfile(false)
return true return true
} }
@ -255,6 +260,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
private fun showEditProfilePictureUI() { private fun showEditProfilePictureUI() {
AlertDialog.Builder(this)
.setTitle(R.string.activity_settings_set_display_picture)
.setView(R.layout.dialog_change_avatar)
.setPositiveButton(R.string.activity_settings_upload) { _, _ ->
startAvatarSelection()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.apply {
if (TextSecurePreferences.getProfileAvatarId(context) != 0) {
setNeutralButton(R.string.activity_settings_remove) { _, _ -> removeAvatar() }
}
}
.show().apply {
findViewById<ProfilePictureView>(R.id.profile_picture_view)?.let(::setupProfilePictureView)
}
}
private fun removeAvatar() {
updateProfile(true)
}
private fun startAvatarSelection() {
// Ask for an optional camera permission. // Ask for an optional camera permission.
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)

View File

@ -5,6 +5,7 @@ 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
@ -13,6 +14,7 @@ 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 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)
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple <ripple
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorCellRipple"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?conversation_pinned_background_color" /> <color android:color="?conversation_pinned_background_color" />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple <ripple
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorCellRipple"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?conversation_unread_background_color" /> <color android:color="?conversation_unread_background_color" />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple <ripple
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorCellRipple"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?colorCellBackground" /> <color android:color="?colorCellBackground" />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple <ripple
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?mention_candidates_view_background_ripple"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?mention_candidates_view_background" /> <color android:color="?mention_candidates_view_background" />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ripple <ripple
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorCellRipple"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?colorCellBackground" /> <color android:color="?colorCellBackground" />

View File

@ -128,6 +128,7 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_above="@id/messageRequestBar"/> android:layout_above="@id/messageRequestBar"/>
<RelativeLayout <RelativeLayout
@ -137,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">

View File

@ -11,7 +11,6 @@
android:layout_height="match_parent" > android:layout_height="match_parent" >
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
style="@style/Widget.Session.TabLayout"
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" /> android:layout_height="@dimen/tab_bar_height" />

View File

@ -12,7 +12,6 @@
android:layout_height="match_parent" > android:layout_height="match_parent" >
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
style="@style/Widget.Session.TabLayout"
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" /> android:layout_height="@dimen/tab_bar_height" />

View File

@ -6,7 +6,6 @@
android:layout_height="match_parent" > android:layout_height="match_parent" >
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
style="@style/Widget.Session.TabLayout"
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" /> android:layout_height="@dimen/tab_bar_height" />

View File

@ -7,7 +7,6 @@
android:paddingHorizontal="@dimen/medium_spacing" android:paddingHorizontal="@dimen/medium_spacing"
android:paddingVertical="@dimen/small_spacing" android:paddingVertical="@dimen/small_spacing"
android:gravity="center_vertical" android:gravity="center_vertical"
android:background="?selectableItemBackground"
android:id="@+id/backgroundContainer"> android:id="@+id/backgroundContainer">
<include layout="@layout/view_profile_picture" <include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView" android:id="@+id/profilePictureView"

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -43,7 +43,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/unblockButton" android:id="@+id/unblockButton"
android:contentDescription="@string/AccessibilityId_block_confirm" android:contentDescription="@string/AccessibilityId_block_confirm"
android:layout_width="0dp" android:layout_width="0dp"

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent">
<include layout="@layout/view_profile_picture"
android:layout_margin="30dp"
android:id="@+id/profile_picture_view"
android:layout_gravity="center"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"
android:layout_marginTop="@dimen/medium_spacing"
android:contentDescription="@string/AccessibilityId_profile_picture" />
</FrameLayout>

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -44,7 +44,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/downloadButton" android:id="@+id/downloadButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -43,7 +43,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/joinButton" android:id="@+id/joinButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -44,7 +44,7 @@
android:contentDescription="@string/AccessibilityId_cancel_link_preview_button"/> android:contentDescription="@string/AccessibilityId_cancel_link_preview_button"/>
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/enableLinkPreviewsButton" android:id="@+id/enableLinkPreviewsButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:background="?dialog_background_color">
<TextView <TextView
android:id="@+id/titleTextView" android:id="@+id/titleTextView"
@ -21,11 +20,10 @@
<ImageView <ImageView
android:id="@+id/closeButton" android:id="@+id/closeButton"
android:background="?selectableItemBackgroundBorderless"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/medium_spacing" android:layout_marginEnd="@dimen/medium_spacing"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_baseline_close_24" android:src="@drawable/ic_baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/titleTextView" app:layout_constraintBottom_toBottomOf="@id/titleTextView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -36,7 +34,6 @@
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?dialog_background_color"
android:drawablePadding="@dimen/large_spacing" android:drawablePadding="@dimen/large_spacing"
android:gravity="center_horizontal|center_vertical" android:gravity="center_horizontal|center_vertical"
android:paddingHorizontal="@dimen/large_spacing" android:paddingHorizontal="@dimen/large_spacing"

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -43,7 +43,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/openURLButton" android:id="@+id/openURLButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -35,7 +35,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -43,7 +43,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Destructive" style="@style/Widget.Session.Button.Dialog.DestructiveText"
android:id="@+id/sendSeedButton" android:id="@+id/sendSeedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -33,7 +33,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"
@ -41,7 +41,7 @@
android:text="@string/cancel" /> android:text="@string/cancel" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.Unimportant" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/shareButton" android:id="@+id/shareButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/small_button_height"

View File

@ -47,7 +47,6 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
style="@style/Widget.Session.TabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" android:layout_height="@dimen/tab_bar_height"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -48,7 +48,6 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
style="@style/Widget.Session.TabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" android:layout_height="@dimen/tab_bar_height"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -62,7 +62,7 @@
android:text="@string/fragment_view_my_qr_code_explanation" /> android:text="@string/fragment_view_my_qr_code_explanation" />
<Button <Button
style="@style/Widget.Session.Button.Common.UnimportantOutline" style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/shareButton" android:id="@+id/shareButton"
android:layout_width="196dp" android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height" android:layout_height="@dimen/medium_button_height"

View File

@ -28,7 +28,6 @@
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/Widget.Session.TabLayout"
app:tabRippleColor="@color/cell_selected" app:tabRippleColor="@color/cell_selected"
app:tabIndicatorColor="?colorAccent" app:tabIndicatorColor="?colorAccent"
android:scrollbars="horizontal"/> android:scrollbars="horizontal"/>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.preferences.widgets.NotificationSettingsPreference
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textColor="?colorAccent"
android:id="@+id/device_settings_text"
android:text="@string/go_to_device_notification_settings"
android:paddingTop="@dimen/medium_spacing"
android:paddingBottom="@dimen/medium_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</org.thoughtcrime.securesms.preferences.widgets.NotificationSettingsPreference>

View File

@ -21,7 +21,6 @@
<org.thoughtcrime.securesms.components.ControllableTabLayout <org.thoughtcrime.securesms.components.ControllableTabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
style="@style/Widget.Session.TabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top"/> android:layout_gravity="top"/>

View File

@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:contentDescription="@string/AccessibilityId_message_request_banner"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/conversation_view_background" android:background="@drawable/conversation_view_background"
android:contentDescription="@string/AccessibilityId_message_request_banner"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/accent_line_thickness" android:paddingStart="@dimen/accent_line_thickness"
@ -72,11 +72,15 @@
android:alpha="0.4" android:alpha="0.4"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAlignment="textEnd"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="@dimen/small_font_size" android:textSize="@dimen/small_font_size"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/unreadCountIndicator"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="9:41 AM" /> tools:text="11 Apr, 9:41 AM" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -150,7 +150,6 @@
<attr name="conversation_shadow_main" format="color|reference"/> <attr name="conversation_shadow_main" format="color|reference"/>
<attr name="default_background_start" format="color|reference"/> <attr name="default_background_start" format="color|reference"/>
<attr name="default_background_end" format="color|reference"/> <attr name="default_background_end" format="color|reference"/>
<attr name="colorCellRipple" format="color|reference"/>
<attr name="colorCellBackground" format="color|reference" /> <attr name="colorCellBackground" format="color|reference" />
<attr name="colorSettingsBackground" format="color|reference" /> <attr name="colorSettingsBackground" format="color|reference" />
<attr name="colorDividerBackground" format="color|reference" /> <attr name="colorDividerBackground" format="color|reference" />
@ -176,7 +175,6 @@
<attr name="input_bar_lock_view_background" format="color|reference"/> <attr name="input_bar_lock_view_background" format="color|reference"/>
<attr name="input_bar_lock_view_border" format="color|reference"/> <attr name="input_bar_lock_view_border" format="color|reference"/>
<attr name="mention_candidates_view_background" format="color|reference"/> <attr name="mention_candidates_view_background" format="color|reference"/>
<attr name="mention_candidates_view_background_ripple" format="color|reference"/>
<attr name="scroll_to_bottom_button_background" format="color|reference"/> <attr name="scroll_to_bottom_button_background" format="color|reference"/>
<attr name="scroll_to_bottom_button_border" format="color|reference"/> <attr name="scroll_to_bottom_button_border" format="color|reference"/>
<attr name="conversation_unread_count_indicator_background" format="color|reference"/> <attr name="conversation_unread_count_indicator_background" format="color|reference"/>

View File

@ -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>
@ -767,6 +769,9 @@
<string name="activity_join_public_chat_scan_qr_code_explanation">Scan the QR code of the open group you\'d like to join</string> <string name="activity_join_public_chat_scan_qr_code_explanation">Scan the QR code of the open group you\'d like to join</string>
<string name="fragment_enter_chat_url_edit_text_hint">Enter an open group URL</string> <string name="fragment_enter_chat_url_edit_text_hint">Enter an open group URL</string>
<string name="activity_settings_title">Settings</string> <string name="activity_settings_title">Settings</string>
<string name="activity_settings_set_display_picture">Set display picture</string>
<string name="activity_settings_upload">Upload</string>
<string name="activity_settings_remove">Remove</string>
<string name="activity_settings_display_name_edit_text_hint">Enter a display name</string> <string name="activity_settings_display_name_edit_text_hint">Enter a display name</string>
<string name="activity_settings_display_name_missing_error">Please pick a display name</string> <string name="activity_settings_display_name_missing_error">Please pick a display name</string>
<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>
@ -1007,9 +1012,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>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Session --> <!-- Session -->
<style name="Widget.Session.ActionBar" parent="Widget.AppCompat.Light.ActionBar.Solid"> <style name="Widget.Session.ActionBar" parent="Widget.AppCompat.Light.ActionBar.Solid">
@ -29,8 +29,6 @@
<item name="backgroundTint">?colorPrimary</item> <item name="backgroundTint">?colorPrimary</item>
<item name="android:backgroundDimEnabled">true</item> <item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.6</item> <item name="android:backgroundDimAmount">0.6</item>
<item name="buttonBarNegativeButtonStyle">@style/Widget.Session.AlertDialog.NegativeButtonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.Session.AlertDialog.PositiveButtonStyle</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@null</item> <item name="android:windowBackground">@null</item>
<item name="textColorAlertDialogListItem">?android:textColorPrimary</item> <item name="textColorAlertDialogListItem">?android:textColorPrimary</item>
@ -46,14 +44,6 @@
<item name="android:background">@drawable/default_bottom_sheet_background</item> <item name="android:background">@drawable/default_bottom_sheet_background</item>
</style> </style>
<style name="Widget.Session.AlertDialog.NegativeButtonStyle" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">?colorAccent</item>
</style>
<style name="Widget.Session.AlertDialog.PositiveButtonStyle" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">?colorAccent</item>
</style>
<style name="Widget.Session.AppBarLayout" parent="@style/Widget.Design.AppBarLayout"> <style name="Widget.Session.AppBarLayout" parent="@style/Widget.Design.AppBarLayout">
</style> </style>
@ -66,7 +56,6 @@
<item name="elevation">1dp</item> <item name="elevation">1dp</item>
<item name="tabIndicatorColor">@color/transparent</item> <item name="tabIndicatorColor">@color/transparent</item>
<item name="tabIndicatorHeight">@dimen/accent_line_thickness</item> <item name="tabIndicatorHeight">@dimen/accent_line_thickness</item>
<item name="tabRippleColor">@color/cell_selected</item>
<item name="tabTextAppearance">@style/TextAppearance.Session.Tab</item> <item name="tabTextAppearance">@style/TextAppearance.Session.Tab</item>
</style> </style>
@ -93,38 +82,38 @@
<style name="Widget.Session.Button.Common.ProminentFilled"> <style name="Widget.Session.Button.Common.ProminentFilled">
<item name="android:background">@drawable/prominent_filled_button_medium_background</item> <item name="android:background">@drawable/prominent_filled_button_medium_background</item>
<item name="android:textColor">@color/black</item> <item name="android:textColor">@color/black</item>
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item> <item name="android:drawableTint">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Common.ProminentOutline"> <style name="Widget.Session.Button.Common.ProminentOutline">
<item name="android:background">@drawable/prominent_outline_button_medium_background</item> <item name="android:background">@drawable/prominent_outline_button_medium_background</item>
<item name="android:textColorPrimary">?attr/prominentButtonColor</item> <item name="android:textColor">?attr/prominentButtonColor</item>
<item name="android:drawableTint" tools:ignore="NewApi">?attr/prominentButtonColor</item> <item name="android:drawableTint">?attr/prominentButtonColor</item>
</style> </style>
<style name="Widget.Session.Button.Common.UnimportantFilled"> <style name="Widget.Session.Button.Common.UnimportantFilled">
<item name="android:background">@drawable/unimportant_filled_button_medium_background</item> <item name="android:background">@drawable/unimportant_filled_button_medium_background</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item> <item name="android:drawableTint">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Common.UnimportantOutline"> <style name="Widget.Session.Button.Common.UnimportantOutline">
<item name="android:background">@drawable/unimportant_outline_button_medium_background</item> <item name="android:background">@drawable/unimportant_outline_button_medium_background</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item> <item name="android:drawableTint">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Common.UnimportantDestructive"> <style name="Widget.Session.Button.Common.UnimportantDestructive">
<item name="android:background">@drawable/unimportant_outline_button_medium_background</item> <item name="android:background">@drawable/unimportant_outline_button_medium_background</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:backgroundTint" tools:ignore="NewApi">@color/destructive</item> <item name="android:backgroundTint">@color/destructive</item>
<item name="android:drawableTint" tools:ignore="NewApi">@color/destructive</item> <item name="android:drawableTint">@color/destructive</item>
</style> </style>
<style name="Widget.Session.Button.Common.DestructiveOutline"> <style name="Widget.Session.Button.Common.DestructiveOutline">
<item name="android:background">@drawable/destructive_outline_button_medium_background</item> <item name="android:background">@drawable/destructive_outline_button_medium_background</item>
<item name="android:textColor">@color/destructive</item> <item name="android:textColor">@color/destructive</item>
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item> <item name="android:drawableTint">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Dialog" parent=""> <style name="Widget.Session.Button.Dialog" parent="">
@ -133,19 +122,10 @@
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
</style> </style>
<style name="Widget.Session.Button.Dialog.Unimportant">
<item name="android:background">@drawable/unimportant_dialog_button_background</item>
</style>
<style name="Widget.Session.Button.Dialog.UnimportantText"> <style name="Widget.Session.Button.Dialog.UnimportantText">
<item name="android:background">@drawable/unimportant_dialog_text_button_background</item> <item name="android:background">@drawable/unimportant_dialog_text_button_background</item>
</style> </style>
<style name="Widget.Session.Button.Dialog.Destructive">
<item name="android:background">@drawable/destructive_dialog_button_background</item>
<item name="android:textColor">@color/black</item>
</style>
<style name="Widget.Session.Button.Dialog.DestructiveText"> <style name="Widget.Session.Button.Dialog.DestructiveText">
<item name="android:background">@drawable/destructive_dialog_text_button_background</item> <item name="android:background">@drawable/destructive_dialog_text_button_background</item>
<item name="android:textColor">@color/destructive</item> <item name="android:textColor">@color/destructive</item>

View File

@ -303,6 +303,8 @@
<item name="dividerHorizontal">?dividerVertical</item> <item name="dividerHorizontal">?dividerVertical</item>
<item name="message_received_background_color">#F2F2F2</item> <item name="message_received_background_color">#F2F2F2</item>
<item name="colorAccent">@color/classic_accent</item> <item name="colorAccent">@color/classic_accent</item>
<item name="colorControlHighlight">?android:colorControlHighlight</item>
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
</style> </style>
<style name="Ocean"> <style name="Ocean">
@ -310,6 +312,8 @@
<item name="dividerHorizontal">?dividerVertical</item> <item name="dividerHorizontal">?dividerVertical</item>
<item name="message_received_background_color">#F2F2F2</item> <item name="message_received_background_color">#F2F2F2</item>
<item name="colorAccent">@color/ocean_accent</item> <item name="colorAccent">@color/ocean_accent</item>
<item name="colorControlHighlight">?android:colorControlHighlight</item>
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
</style> </style>
<style name="Classic.Dark"> <style name="Classic.Dark">
@ -319,7 +323,6 @@
<item name="colorPrimaryDark">@color/classic_dark_0</item> <item name="colorPrimaryDark">@color/classic_dark_0</item>
<item name="colorControlNormal">?android:textColorPrimary</item> <item name="colorControlNormal">?android:textColorPrimary</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/classic_dark_6</item> <item name="android:textColorPrimary">@color/classic_dark_6</item>
<item name="android:textColorSecondary">?android:textColorPrimary</item> <item name="android:textColorSecondary">?android:textColorPrimary</item>
<item name="android:textColorTertiary">@color/classic_dark_5</item> <item name="android:textColorTertiary">@color/classic_dark_5</item>
@ -334,7 +337,7 @@
<item name="colorCellBackground">@color/classic_dark_1</item> <item name="colorCellBackground">@color/classic_dark_1</item>
<item name="colorSettingsBackground">@color/classic_dark_1</item> <item name="colorSettingsBackground">@color/classic_dark_1</item>
<item name="colorDividerBackground">@color/classic_dark_3</item> <item name="colorDividerBackground">@color/classic_dark_3</item>
<item name="colorCellRipple">@color/classic_dark_3</item> <item name="android:colorControlHighlight">@color/classic_dark_3</item>
<item name="actionBarPopupTheme">@style/Dark.Popup</item> <item name="actionBarPopupTheme">@style/Dark.Popup</item>
<item name="actionBarWidgetTheme">@null</item> <item name="actionBarWidgetTheme">@null</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item> <item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
@ -382,7 +385,6 @@
<item name="input_bar_lock_view_background">@color/classic_dark_2</item> <item name="input_bar_lock_view_background">@color/classic_dark_2</item>
<item name="input_bar_lock_view_border">@color/classic_dark_3</item> <item name="input_bar_lock_view_border">@color/classic_dark_3</item>
<item name="mention_candidates_view_background">@color/classic_dark_2</item> <item name="mention_candidates_view_background">@color/classic_dark_2</item>
<item name="mention_candidates_view_background_ripple">@color/classic_dark_3</item>
<item name="scroll_to_bottom_button_background">@color/classic_dark_1</item> <item name="scroll_to_bottom_button_background">@color/classic_dark_1</item>
<item name="scroll_to_bottom_button_border">@color/classic_dark_3</item> <item name="scroll_to_bottom_button_border">@color/classic_dark_3</item>
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item> <item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
@ -397,7 +399,6 @@
<item name="colorPrimaryDark">@color/classic_light_6</item> <item name="colorPrimaryDark">@color/classic_light_6</item>
<item name="colorControlNormal">?android:textColorPrimary</item> <item name="colorControlNormal">?android:textColorPrimary</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/classic_light_0</item> <item name="android:textColorPrimary">@color/classic_light_0</item>
<item name="android:textColorSecondary">@color/classic_light_1</item> <item name="android:textColorSecondary">@color/classic_light_1</item>
<item name="android:textColorTertiary">@color/classic_light_1</item> <item name="android:textColorTertiary">@color/classic_light_1</item>
@ -408,7 +409,7 @@
<item name="colorCellBackground">@color/classic_light_6</item> <item name="colorCellBackground">@color/classic_light_6</item>
<item name="colorSettingsBackground">@color/classic_light_5</item> <item name="colorSettingsBackground">@color/classic_light_5</item>
<item name="colorDividerBackground">@color/classic_light_3</item> <item name="colorDividerBackground">@color/classic_light_3</item>
<item name="colorCellRipple">@color/classic_light_3</item> <item name="android:colorControlHighlight">@color/classic_light_3</item>
<item name="bottomSheetDialogTheme">@style/Classic.Light.BottomSheet</item> <item name="bottomSheetDialogTheme">@style/Classic.Light.BottomSheet</item>
<item name="android:actionMenuTextColor">?android:textColorPrimary</item> <item name="android:actionMenuTextColor">?android:textColorPrimary</item>
<item name="popupTheme">?actionBarPopupTheme</item> <item name="popupTheme">?actionBarPopupTheme</item>
@ -467,7 +468,6 @@
<item name="input_bar_lock_view_background">@color/classic_light_4</item> <item name="input_bar_lock_view_background">@color/classic_light_4</item>
<item name="input_bar_lock_view_border">@color/classic_light_2</item> <item name="input_bar_lock_view_border">@color/classic_light_2</item>
<item name="mention_candidates_view_background">?colorCellBackground</item> <item name="mention_candidates_view_background">?colorCellBackground</item>
<item name="mention_candidates_view_background_ripple">?colorCellRipple</item>
<item name="scroll_to_bottom_button_background">@color/classic_light_4</item> <item name="scroll_to_bottom_button_background">@color/classic_light_4</item>
<item name="scroll_to_bottom_button_border">@color/classic_light_2</item> <item name="scroll_to_bottom_button_border">@color/classic_light_2</item>
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item> <item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
@ -482,7 +482,6 @@
<item name="colorPrimaryDark">@color/ocean_dark_2</item> <item name="colorPrimaryDark">@color/ocean_dark_2</item>
<item name="colorControlNormal">@color/ocean_dark_7</item> <item name="colorControlNormal">@color/ocean_dark_7</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_dark_7</item> <item name="android:textColorPrimary">@color/ocean_dark_7</item>
<item name="android:textColorSecondary">@color/ocean_dark_5</item> <item name="android:textColorSecondary">@color/ocean_dark_5</item>
<item name="android:textColorTertiary">@color/ocean_dark_5</item> <item name="android:textColorTertiary">@color/ocean_dark_5</item>
@ -496,7 +495,7 @@
<item name="colorCellBackground">@color/ocean_dark_3</item> <item name="colorCellBackground">@color/ocean_dark_3</item>
<item name="colorSettingsBackground">@color/ocean_dark_1</item> <item name="colorSettingsBackground">@color/ocean_dark_1</item>
<item name="colorDividerBackground">@color/ocean_dark_4</item> <item name="colorDividerBackground">@color/ocean_dark_4</item>
<item name="colorCellRipple">@color/ocean_dark_4</item> <item name="android:colorControlHighlight">@color/ocean_dark_4</item>
<item name="bottomSheetDialogTheme">@style/Ocean.Dark.BottomSheet</item> <item name="bottomSheetDialogTheme">@style/Ocean.Dark.BottomSheet</item>
<item name="popupTheme">?actionBarPopupTheme</item> <item name="popupTheme">?actionBarPopupTheme</item>
<item name="actionMenuTextColor">?android:textColorPrimary</item> <item name="actionMenuTextColor">?android:textColorPrimary</item>
@ -549,7 +548,6 @@
<item name="input_bar_lock_view_background">?colorPrimary</item> <item name="input_bar_lock_view_background">?colorPrimary</item>
<item name="input_bar_lock_view_border">?colorPrimary</item> <item name="input_bar_lock_view_border">?colorPrimary</item>
<item name="mention_candidates_view_background">@color/ocean_dark_2</item> <item name="mention_candidates_view_background">@color/ocean_dark_2</item>
<item name="mention_candidates_view_background_ripple">@color/ocean_dark_3</item>
<item name="scroll_to_bottom_button_background">@color/ocean_dark_4</item> <item name="scroll_to_bottom_button_background">@color/ocean_dark_4</item>
<item name="scroll_to_bottom_button_border">?colorPrimary</item> <item name="scroll_to_bottom_button_border">?colorPrimary</item>
<item name="conversation_unread_count_indicator_background">@color/ocean_dark_4</item> <item name="conversation_unread_count_indicator_background">@color/ocean_dark_4</item>
@ -564,7 +562,6 @@
<item name="colorPrimaryDark">@color/ocean_light_6</item> <item name="colorPrimaryDark">@color/ocean_light_6</item>
<item name="colorControlNormal">@color/ocean_light_1</item> <item name="colorControlNormal">@color/ocean_light_1</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_light_1</item> <item name="android:textColorPrimary">@color/ocean_light_1</item>
<item name="android:textColorSecondary">@color/ocean_light_2</item> <item name="android:textColorSecondary">@color/ocean_light_2</item>
<item name="android:textColorTertiary">@color/ocean_light_2</item> <item name="android:textColorTertiary">@color/ocean_light_2</item>
@ -578,7 +575,7 @@
<item name="colorCellBackground">@color/ocean_light_5</item> <item name="colorCellBackground">@color/ocean_light_5</item>
<item name="colorSettingsBackground">@color/ocean_light_6</item> <item name="colorSettingsBackground">@color/ocean_light_6</item>
<item name="colorDividerBackground">@color/ocean_light_3</item> <item name="colorDividerBackground">@color/ocean_light_3</item>
<item name="colorCellRipple">@color/ocean_light_4</item> <item name="android:colorControlHighlight">@color/ocean_light_4</item>
<item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item> <item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item>
<item name="actionBarPopupTheme">@style/Light.Popup</item> <item name="actionBarPopupTheme">@style/Light.Popup</item>
<item name="popupTheme">?actionBarPopupTheme</item> <item name="popupTheme">?actionBarPopupTheme</item>
@ -633,7 +630,6 @@
<item name="input_bar_lock_view_background">@color/ocean_light_5</item> <item name="input_bar_lock_view_background">@color/ocean_light_5</item>
<item name="input_bar_lock_view_border">@color/ocean_light_1</item> <item name="input_bar_lock_view_border">@color/ocean_light_1</item>
<item name="mention_candidates_view_background">?colorCellBackground</item> <item name="mention_candidates_view_background">?colorCellBackground</item>
<item name="mention_candidates_view_background_ripple">?colorCellRipple</item>
<item name="scroll_to_bottom_button_background">?input_bar_button_background_opaque</item> <item name="scroll_to_bottom_button_background">?input_bar_button_background_opaque</item>
<item name="scroll_to_bottom_button_border">?input_bar_button_background_opaque_border</item> <item name="scroll_to_bottom_button_border">?input_bar_button_background_opaque_border</item>
<item name="conversation_unread_count_indicator_background">?colorAccent</item> <item name="conversation_unread_count_indicator_background">?colorAccent</item>

View File

@ -10,7 +10,7 @@
android:summary="@string/preferences_notifications_strategy_category_fast_mode_summary" android:summary="@string/preferences_notifications_strategy_category_fast_mode_summary"
android:defaultValue="false" /> android:defaultValue="false" />
<Preference android:layout="@layout/go_to_device_settings" <Preference android:title="@string/go_to_device_notification_settings"
android:key="pref_notification_priority" /> android:key="pref_notification_priority" />
</PreferenceCategory> </PreferenceCategory>

View File

@ -106,10 +106,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)

View File

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

View File

@ -11,20 +11,22 @@ 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

View File

@ -61,11 +61,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 {
sendToSnodeDestination(destination, message) sendToSnodeDestination(destination, message, isSyncMessage)
} }
} }
@ -86,7 +86,7 @@ object MessageSender {
val isSelfSend = (message.recipient == userPublicKey) val isSelfSend = (message.recipient == userPublicKey)
// 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!!)
} }
@ -374,16 +374,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

View File

@ -7,16 +7,17 @@ import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.truncateIdForDisplay
object UpdateMessageBuilder { object UpdateMessageBuilder {
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String { fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
var message = "" var message = ""
val updateData = updateMessageData.kind ?: return message val updateData = updateMessageData.kind ?: return message
if (!isOutgoing && sender == null) return message if (!isOutgoing && senderId == null) return message
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val senderName: String = if (!isOutgoing) { val senderName: String = if (!isOutgoing) {
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
} else { context.getString(R.string.MessageRecord_you) } } else { context.getString(R.string.MessageRecord_you) }
when (updateData) { when (updateData) {
@ -77,11 +78,11 @@ object UpdateMessageBuilder {
return message return message
} }
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String { fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
if (!isOutgoing && sender == null) return "" if (!isOutgoing && senderId == null) return ""
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val senderName: String? = if (!isOutgoing) { val senderName: String? = if (!isOutgoing) {
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
} else { context.getString(R.string.MessageRecord_you) } } else { context.getString(R.string.MessageRecord_you) }
return if (duration <= 0) { return if (duration <= 0) {
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages) if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
@ -93,9 +94,9 @@ object UpdateMessageBuilder {
} }
} }
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String { fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val senderName = storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender!! val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
return when (kind) { return when (kind) {
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT -> DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName) context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)

View File

@ -0,0 +1,4 @@
package org.session.libsession.utilities
fun truncateIdForDisplay(id: String): String =
id.takeIf { it.length > 8 }?.apply{ "${take(4)}${takeLast(4)}" } ?: id

View File

@ -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,6 @@ fun Context.getColorFromAttr(
theme.resolveAttribute(attrColor, typedValue, resolveRefs) theme.resolveAttribute(attrColor, typedValue, resolveRefs)
return typedValue.data return typedValue.data
} }
val RecyclerView.isScrolledToBottom: Boolean
get() = computeVerticalScrollOffset() + computeVerticalScrollExtent() >= computeVerticalScrollRange()