mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 18:43:49 +00:00
Merge remote-tracking branch 'upstream/dev' into bluetooth-manager-crash
This commit is contained in:
commit
0c2a635d03
@ -7,6 +7,10 @@ import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
|
||||
|
||||
private const val TAG = "ScreenshotObserver"
|
||||
|
||||
class ScreenshotObserver(private val context: Context, handler: Handler, private val screenshotTriggered: ()->Unit): ContentObserver(handler) {
|
||||
|
||||
@ -31,6 +35,7 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.Media.DATA
|
||||
)
|
||||
try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
projection,
|
||||
@ -48,6 +53,9 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@ -56,6 +64,8 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
||||
MediaStore.Images.Media.DISPLAY_NAME,
|
||||
MediaStore.Images.Media.RELATIVE_PATH
|
||||
)
|
||||
|
||||
try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
projection,
|
||||
@ -78,6 +88,8 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSeparatorBinding
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
|
||||
class LabeledSeparatorView : RelativeLayout {
|
||||
|
||||
private lateinit var binding: ViewSeparatorBinding
|
||||
private val path = Path()
|
||||
|
||||
private val paint: Paint by lazy {
|
||||
val result = Paint()
|
||||
result.style = Paint.Style.STROKE
|
||||
result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)
|
||||
result.strokeWidth = toPx(1, resources).toFloat()
|
||||
result.isAntiAlias = true
|
||||
result
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
|
||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(binding.root, layoutParams)
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onDraw(c: Canvas) {
|
||||
super.onDraw(c)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val hMargin = toPx(16, resources).toFloat()
|
||||
path.reset()
|
||||
path.moveTo(0.0f, h / 2)
|
||||
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
|
||||
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
||||
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(w, h / 2)
|
||||
path.close()
|
||||
c.drawPath(path, paint)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -21,8 +21,6 @@ import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.DimenRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.drawToBitmap
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@ -211,11 +209,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
var searchViewItem: MenuItem? = null
|
||||
|
||||
private var emojiPickerVisible = false
|
||||
|
||||
private val isScrolledToBottom: Boolean
|
||||
get() {
|
||||
val position = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: 0
|
||||
return position == 0
|
||||
}
|
||||
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
||||
|
||||
private val layoutManager: LinearLayoutManager?
|
||||
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
||||
@ -445,17 +442,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
handleRecyclerViewScrolled()
|
||||
}
|
||||
})
|
||||
|
||||
binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
showScrollToBottomButtonIfApplicable()
|
||||
}
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpToolBar() {
|
||||
setSupportActionBar(binding?.toolbar)
|
||||
val binding = binding ?: return
|
||||
setSupportActionBar(binding.toolbar)
|
||||
val actionBar = supportActionBar ?: return
|
||||
val recipient = viewModel.recipient ?: return
|
||||
actionBar.title = ""
|
||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
||||
actionBar.setHomeButtonEnabled(true)
|
||||
binding!!.toolbarContent.conversationTitleView.text = when {
|
||||
binding.toolbarContent.conversationTitleView.text = when {
|
||||
recipient.isLocalNumber -> getString(R.string.note_to_self)
|
||||
else -> recipient.toShortString()
|
||||
}
|
||||
@ -465,13 +467,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
R.dimen.small_profile_picture_size
|
||||
}
|
||||
val size = resources.getDimension(sizeID).roundToInt()
|
||||
binding!!.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
binding!!.toolbarContent.profilePictureView.root.glide = glide
|
||||
binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
binding.toolbarContent.profilePictureView.root.glide = glide
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
||||
val profilePictureView = binding!!.toolbarContent.profilePictureView.root
|
||||
viewModel.recipient?.let { recipient ->
|
||||
profilePictureView.update(recipient)
|
||||
}
|
||||
val profilePictureView = binding.toolbarContent.profilePictureView.root
|
||||
viewModel.recipient?.let(profilePictureView::update)
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
@ -908,15 +908,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val binding = binding ?: return
|
||||
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
|
||||
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
||||
binding.typingIndicatorViewContainer.isVisible
|
||||
showOrHidScrollToBottomButton()
|
||||
showScrollToBottomButtonIfApplicable()
|
||||
val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1
|
||||
unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0)
|
||||
updateUnreadCountIndicator()
|
||||
}
|
||||
|
||||
private fun showOrHidScrollToBottomButton(show: Boolean = true) {
|
||||
binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0
|
||||
private fun showScrollToBottomButtonIfApplicable() {
|
||||
binding?.scrollToBottomButton?.isVisible = !emojiPickerVisible && !isScrolledToBottom && adapter.itemCount > 0
|
||||
}
|
||||
|
||||
private fun updateUnreadCountIndicator() {
|
||||
@ -1088,33 +1087,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
Log.e("Loki", "Failed to show emoji picker", e)
|
||||
return
|
||||
}
|
||||
|
||||
val binding = binding ?: return
|
||||
|
||||
emojiPickerVisible = true
|
||||
ViewUtil.hideKeyboard(this, visibleMessageView)
|
||||
binding?.reactionsShade?.isVisible = true
|
||||
showOrHidScrollToBottomButton(false)
|
||||
binding?.conversationRecyclerView?.suppressLayout(true)
|
||||
binding.reactionsShade.isVisible = true
|
||||
binding.scrollToBottomButton.isVisible = false
|
||||
binding.conversationRecyclerView.suppressLayout(true)
|
||||
reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message))
|
||||
reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener {
|
||||
override fun startHide() {
|
||||
binding?.reactionsShade?.let {
|
||||
emojiPickerVisible = false
|
||||
binding.reactionsShade.let {
|
||||
ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE)
|
||||
}
|
||||
showOrHidScrollToBottomButton(true)
|
||||
showScrollToBottomButtonIfApplicable()
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
binding?.conversationRecyclerView?.suppressLayout(false)
|
||||
binding.conversationRecyclerView.suppressLayout(false)
|
||||
|
||||
WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2);
|
||||
WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2);
|
||||
}
|
||||
|
||||
})
|
||||
val contentBounds = Rect()
|
||||
visibleMessageView.messageContentView.getGlobalVisibleRect(contentBounds)
|
||||
val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) }
|
||||
val selectedConversationModel = SelectedConversationModel(
|
||||
messageContentBitmap,
|
||||
contentBounds.left.toFloat(),
|
||||
contentBounds.top.toFloat(),
|
||||
topLeft[0].toFloat(),
|
||||
topLeft[1].toFloat(),
|
||||
visibleMessageView.messageContentView.width,
|
||||
message.isOutgoing,
|
||||
visibleMessageView.messageContentView
|
||||
@ -1755,6 +1758,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
endActionMode()
|
||||
}
|
||||
|
||||
override fun resyncMessage(messages: Set<MessageRecord>) {
|
||||
messages.iterator().forEach { messageRecord ->
|
||||
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey, isResync = true)
|
||||
}
|
||||
endActionMode()
|
||||
}
|
||||
|
||||
override fun resendMessage(messages: Set<MessageRecord>) {
|
||||
messages.iterator().forEach { messageRecord ->
|
||||
ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey)
|
||||
@ -1915,6 +1925,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val selectedItems = setOf(message)
|
||||
when (action) {
|
||||
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
|
||||
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
|
||||
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
|
||||
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
|
||||
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
|
||||
|
@ -660,7 +660,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_select)));
|
||||
// Reply
|
||||
if (!message.isPending() && !message.isFailed()) {
|
||||
boolean canWrite = openGroup == null || openGroup.getCanWrite();
|
||||
if (canWrite && !message.isPending() && !message.isFailed()) {
|
||||
items.add(
|
||||
new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_reply_message))
|
||||
@ -700,6 +701,10 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
if (message.isFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||
}
|
||||
// Resync
|
||||
if (message.isSyncFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resync_message), () -> handleActionItemClicked(Action.RESYNC)));
|
||||
}
|
||||
// Save media
|
||||
if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
|
||||
items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD),
|
||||
@ -885,6 +890,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
public enum Action {
|
||||
REPLY,
|
||||
RESEND,
|
||||
RESYNC,
|
||||
DOWNLOAD,
|
||||
COPY_MESSAGE,
|
||||
COPY_SESSION_ID,
|
||||
|
@ -70,6 +70,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
||||
// Resend
|
||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
||||
// Resync
|
||||
menu.findItem(R.id.menu_context_resync).isVisible = (selectedItems.size == 1 && firstMessage.isSyncFailed)
|
||||
// Save media
|
||||
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
|
||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
||||
@ -90,6 +92,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
|
||||
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
||||
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
|
||||
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
||||
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
||||
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
||||
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
||||
@ -113,6 +116,7 @@ interface ConversationActionModeCallbackDelegate {
|
||||
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
||||
fun copyMessages(messages: Set<MessageRecord>)
|
||||
fun copySessionID(messages: Set<MessageRecord>)
|
||||
fun resyncMessage(messages: Set<MessageRecord>)
|
||||
fun resendMessage(messages: Set<MessageRecord>)
|
||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||
fun saveAttachment(messages: Set<MessageRecord>)
|
||||
|
@ -292,12 +292,7 @@ class VisibleMessageView : LinearLayout {
|
||||
@StringRes val messageText: Int?,
|
||||
val contentDescription: String?)
|
||||
|
||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo {
|
||||
return when {
|
||||
!message.isOutgoing -> MessageStatusInfo(null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
||||
message.isFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
@ -305,12 +300,25 @@ class VisibleMessageView : LinearLayout {
|
||||
R.string.delivery_status_failed,
|
||||
null
|
||||
)
|
||||
message.isSyncFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
context.getColor(R.color.accent_orange),
|
||||
R.string.delivery_status_sync_failed,
|
||||
null
|
||||
)
|
||||
message.isPending ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
||||
)
|
||||
message.isResyncing ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing,
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_syncing)
|
||||
)
|
||||
message.isRead ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_read,
|
||||
@ -325,7 +333,6 @@ class VisibleMessageView : LinearLayout {
|
||||
context.getString(R.string.AccessibilityId_message_sent_status_tick)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val container = binding.messageInnerContainer
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
object ResendMessageUtilities {
|
||||
|
||||
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?) {
|
||||
fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) {
|
||||
val recipient: Recipient = messageRecord.recipient
|
||||
val message = VisibleMessage()
|
||||
message.id = messageRecord.getId()
|
||||
@ -55,8 +56,13 @@ object ResendMessageUtilities {
|
||||
val sentTimestamp = message.sentTimestamp
|
||||
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
||||
if (sentTimestamp != null && sender != null) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -38,13 +38,12 @@ object TextUtilities {
|
||||
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
||||
val textLayout = layout ?: return emptyList()
|
||||
val lineRect = Rect()
|
||||
val bodyTextRect = Rect()
|
||||
getGlobalVisibleRect(bodyTextRect)
|
||||
val offset = intArrayOf(0, 0).also { getLocationOnScreen(it) }
|
||||
val textSpannable = text.toSpannable()
|
||||
return (0 until textLayout.lineCount).flatMap { line ->
|
||||
textLayout.getLineBounds(line, lineRect)
|
||||
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
|
||||
if ((Rect(lineRect)).contains(hitRect)) {
|
||||
lineRect.offset(offset[0] + totalPaddingLeft, offset[1] + totalPaddingTop)
|
||||
if (lineRect.contains(hitRect)) {
|
||||
// calculate the url span intersected with (if any)
|
||||
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
||||
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
||||
|
@ -45,11 +45,16 @@ public final class KeyStoreHelper {
|
||||
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
||||
private static final String KEY_ALIAS = "SignalSecret";
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static final Object lock = new Object();
|
||||
|
||||
public static SealedData seal(@NonNull byte[] input) {
|
||||
SecretKey secretKey = getOrCreateKeyStoreEntry();
|
||||
|
||||
try {
|
||||
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||
// prevent crashes with quickly repeated encrypt/decrypt operations
|
||||
// https://github.com/mozilla-mobile/android-components/issues/5342
|
||||
synchronized (lock) {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
|
||||
@ -57,32 +62,35 @@ public final class KeyStoreHelper {
|
||||
byte[] data = cipher.doFinal(input);
|
||||
|
||||
return new SealedData(iv, data);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public static byte[] unseal(@NonNull SealedData sealedData) {
|
||||
SecretKey secretKey = getKeyStoreEntry();
|
||||
|
||||
try {
|
||||
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
|
||||
// prevent crashes with quickly repeated encrypt/decrypt operations
|
||||
// https://github.com/mozilla-mobile/android-components/issues/5342
|
||||
synchronized (lock) {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
|
||||
|
||||
return cipher.doFinal(sealedData.data);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getOrCreateKeyStoreEntry() {
|
||||
if (hasKeyStoreEntry()) return getKeyStoreEntry();
|
||||
else return createKeyStoreEntry();
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey createKeyStoreEntry() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
||||
@ -99,7 +107,6 @@ public final class KeyStoreHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getKeyStoreEntry() {
|
||||
KeyStore keyStore = getKeyStore();
|
||||
|
||||
@ -137,7 +144,6 @@ public final class KeyStoreHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static boolean hasKeyStoreEntry() {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||
|
@ -37,6 +37,13 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
|
||||
public abstract void markAsSent(long messageId, boolean secure);
|
||||
|
||||
public abstract void markAsSyncing(long id);
|
||||
|
||||
public abstract void markAsResyncing(long id);
|
||||
|
||||
public abstract void markAsSyncFailed(long id);
|
||||
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention);
|
||||
@ -199,7 +206,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
contentValues.put(THREAD_ID, newThreadId);
|
||||
db.update(getTableName(), contentValues, where, args);
|
||||
}
|
||||
|
||||
public static class SyncMessageId {
|
||||
|
||||
private final Address address;
|
||||
|
@ -276,6 +276,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
|
||||
override fun markAsSyncing(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SYNCING_TYPE)
|
||||
}
|
||||
override fun markAsResyncing(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_RESYNCING_TYPE)
|
||||
}
|
||||
override fun markAsSyncFailed(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SYNC_FAILED_TYPE)
|
||||
}
|
||||
|
||||
fun markAsSending(messageId: Long) {
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
|
||||
}
|
||||
|
@ -47,8 +47,13 @@ public interface MmsSmsColumns {
|
||||
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
||||
public static final long BASE_DRAFT_TYPE = 27;
|
||||
protected static final long BASE_DELETED_TYPE = 28;
|
||||
protected static final long BASE_SYNCING_TYPE = 29;
|
||||
protected static final long BASE_RESYNCING_TYPE = 30;
|
||||
protected static final long BASE_SYNC_FAILED_TYPE = 31;
|
||||
|
||||
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
|
||||
BASE_SYNCING_TYPE, BASE_RESYNCING_TYPE,
|
||||
BASE_SYNC_FAILED_TYPE,
|
||||
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
||||
BASE_PENDING_SECURE_SMS_FALLBACK,
|
||||
BASE_PENDING_INSECURE_SMS_FALLBACK,
|
||||
@ -109,6 +114,18 @@ public interface MmsSmsColumns {
|
||||
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isResyncingType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_RESYNCING_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isSyncingType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SYNCING_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isSyncFailedMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SYNC_FAILED_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isFailedMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ public class RecipientDatabase extends Database {
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocked(@NonNull List<Recipient> recipients, boolean blocked) {
|
||||
public void setBlocked(@NonNull Iterable<Recipient> recipients, boolean blocked) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
|
@ -202,6 +202,21 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSyncing(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNCING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsResyncing(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_RESYNCING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSyncFailed(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SYNC_FAILED_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markUnidentified(long id, boolean unidentified) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
@ -380,6 +380,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) {
|
||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
@ -405,7 +421,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 messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
if (messageRecord.isMms) {
|
||||
@ -428,6 +444,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) {
|
||||
val db = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||
db.clearErrorMessage(messageID)
|
||||
@ -977,7 +1013,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
|
||||
}
|
||||
|
||||
override fun unblock(toUnblock: List<Recipient>) {
|
||||
override fun unblock(toUnblock: Iterable<Recipient>) {
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
recipientDb.setBlocked(toUnblock, false)
|
||||
}
|
||||
@ -986,5 +1022,4 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
return recipientDb.blockedContacts
|
||||
}
|
||||
|
||||
}
|
@ -80,6 +80,18 @@ public abstract class DisplayRecord {
|
||||
return !isFailed() && !isPending();
|
||||
}
|
||||
|
||||
public boolean isSyncing() {
|
||||
return MmsSmsColumns.Types.isSyncingType(type);
|
||||
}
|
||||
|
||||
public boolean isResyncing() {
|
||||
return MmsSmsColumns.Types.isResyncingType(type);
|
||||
}
|
||||
|
||||
public boolean isSyncFailed() {
|
||||
return MmsSmsColumns.Types.isSyncFailedMessageType(type);
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return MmsSmsColumns.Types.isFailedMessageType(type)
|
||||
|| MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type)
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -11,59 +10,27 @@ import network.loki.messenger.databinding.ActivityBlockedContactsBinding
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener {
|
||||
class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
lateinit var binding: ActivityBlockedContactsBinding
|
||||
|
||||
val viewModel: BlockedContactsViewModel by viewModels()
|
||||
|
||||
val adapter = BlockedContactsAdapter()
|
||||
val adapter: BlockedContactsAdapter by lazy { BlockedContactsAdapter(viewModel) }
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
if (v === binding.unblockButton && adapter.getSelectedItems().isNotEmpty()) {
|
||||
val contactsToUnblock = adapter.getSelectedItems()
|
||||
fun unblock() {
|
||||
// show dialog
|
||||
val title = if (contactsToUnblock.size == 1) {
|
||||
getString(R.string.Unblock_dialog__title_single, contactsToUnblock.first().name)
|
||||
} else {
|
||||
getString(R.string.Unblock_dialog__title_multiple)
|
||||
}
|
||||
val title = viewModel.getTitle(this)
|
||||
|
||||
val message = if (contactsToUnblock.size == 1) {
|
||||
getString(R.string.Unblock_dialog__message, contactsToUnblock.first().name)
|
||||
} else {
|
||||
val stringBuilder = StringBuilder()
|
||||
val iterator = contactsToUnblock.iterator()
|
||||
var numberAdded = 0
|
||||
while (iterator.hasNext() && numberAdded < 3) {
|
||||
val nextRecipient = iterator.next()
|
||||
if (numberAdded > 0) stringBuilder.append(", ")
|
||||
|
||||
stringBuilder.append(nextRecipient.name)
|
||||
numberAdded++
|
||||
}
|
||||
val overflow = contactsToUnblock.size - numberAdded
|
||||
if (overflow > 0) {
|
||||
stringBuilder.append(" ")
|
||||
val string = resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow)
|
||||
stringBuilder.append(string.format(overflow))
|
||||
}
|
||||
getString(R.string.Unblock_dialog__message, stringBuilder.toString())
|
||||
}
|
||||
val message = viewModel.getMessage(this)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_2) { d, _ ->
|
||||
viewModel.unblock(contactsToUnblock)
|
||||
d.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { d, _ ->
|
||||
d.dismiss()
|
||||
}
|
||||
.setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock() }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
@ -73,15 +40,15 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnCli
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
viewModel.subscribe(this)
|
||||
.observe(this) { newState ->
|
||||
adapter.submitList(newState.blockedContacts)
|
||||
val isEmpty = newState.blockedContacts.isEmpty()
|
||||
binding.emptyStateMessageTextView.isVisible = isEmpty
|
||||
binding.nonEmptyStateGroup.isVisible = !isEmpty
|
||||
.observe(this) { state ->
|
||||
adapter.submitList(state.items)
|
||||
binding.emptyStateMessageTextView.isVisible = state.emptyStateMessageTextViewVisible
|
||||
binding.nonEmptyStateGroup.isVisible = state.nonEmptyStateGroupVisible
|
||||
binding.unblockButton.isEnabled = state.unblockButtonEnabled
|
||||
}
|
||||
|
||||
binding.unblockButton.setOnClickListener(this)
|
||||
binding.unblockButton.setOnClickListener { unblock() }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,38 +10,30 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.BlockedContactLayoutBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.adapter.SelectableItem
|
||||
|
||||
class BlockedContactsAdapter: ListAdapter<Recipient,BlockedContactsAdapter.ViewHolder>(RecipientDiffer()) {
|
||||
typealias SelectableRecipient = SelectableItem<Recipient>
|
||||
|
||||
class RecipientDiffer: DiffUtil.ItemCallback<Recipient>() {
|
||||
override fun areItemsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem === newItem
|
||||
override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem
|
||||
class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter<SelectableRecipient,BlockedContactsAdapter.ViewHolder>(RecipientDiffer()) {
|
||||
|
||||
class RecipientDiffer: DiffUtil.ItemCallback<SelectableRecipient>() {
|
||||
override fun areItemsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.item.address == new.item.address
|
||||
override fun areContentsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.isSelected == new.isSelected
|
||||
override fun getChangePayload(old: SelectableRecipient, new: SelectableRecipient) = new.isSelected
|
||||
}
|
||||
|
||||
private val selectedItems = mutableListOf<Recipient>()
|
||||
|
||||
fun getSelectedItems() = selectedItems
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false)
|
||||
return ViewHolder(itemView)
|
||||
}
|
||||
|
||||
private fun toggleSelection(recipient: Recipient, isSelected: Boolean, position: Int) {
|
||||
if (isSelected) {
|
||||
selectedItems -= recipient
|
||||
} else {
|
||||
selectedItems += recipient
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.blocked_contact_layout, parent, false)
|
||||
.let(::ViewHolder)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val recipient = getItem(position)
|
||||
val isSelected = recipient in selectedItems
|
||||
holder.bind(recipient, isSelected) {
|
||||
toggleSelection(recipient, isSelected, position)
|
||||
holder.bind(getItem(position), viewModel::toggle)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) holder.bind(getItem(position), viewModel::toggle)
|
||||
else holder.select(getItem(position).isSelected)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
@ -54,15 +46,18 @@ class BlockedContactsAdapter: ListAdapter<Recipient,BlockedContactsAdapter.ViewH
|
||||
val glide = GlideApp.with(itemView)
|
||||
val binding = BlockedContactLayoutBinding.bind(itemView)
|
||||
|
||||
fun bind(recipient: Recipient, isSelected: Boolean, toggleSelection: () -> Unit) {
|
||||
binding.recipientName.text = recipient.name
|
||||
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
|
||||
binding.recipientName.text = selectable.item.name
|
||||
with (binding.profilePictureView.root) {
|
||||
glide = this@ViewHolder.glide
|
||||
update(recipient)
|
||||
update(selectable.item)
|
||||
}
|
||||
binding.root.setOnClickListener { toggleSelection() }
|
||||
binding.root.setOnClickListener { toggle(selectable) }
|
||||
binding.selectButton.isSelected = selectable.isSelected
|
||||
}
|
||||
|
||||
fun select(isSelected: Boolean) {
|
||||
binding.selectButton.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -17,9 +17,11 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.util.adapter.SelectableItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@ -29,7 +31,9 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
|
||||
|
||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
private val _contacts = MutableLiveData(BlockedContactsViewState(emptyList()))
|
||||
private val _state = MutableLiveData(BlockedContactsViewState())
|
||||
|
||||
val state get() = _state.value!!
|
||||
|
||||
fun subscribe(context: Context): LiveData<BlockedContactsViewState> {
|
||||
executor.launch(IO) {
|
||||
@ -45,21 +49,74 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
|
||||
}
|
||||
executor.launch(IO) {
|
||||
for (update in listUpdateChannel) {
|
||||
val blockedContactState = BlockedContactsViewState(storage.blockedContacts().sortedBy { it.name })
|
||||
val blockedContactState = state.copy(
|
||||
blockedContacts = storage.blockedContacts().sortedBy { it.name }
|
||||
)
|
||||
withContext(Main) {
|
||||
_contacts.value = blockedContactState
|
||||
_state.value = blockedContactState
|
||||
}
|
||||
}
|
||||
}
|
||||
return _contacts
|
||||
return _state
|
||||
}
|
||||
|
||||
fun unblock(toUnblock: List<Recipient>) {
|
||||
storage.unblock(toUnblock)
|
||||
fun unblock() {
|
||||
storage.unblock(state.selectedItems)
|
||||
_state.value = state.copy(selectedItems = emptySet())
|
||||
}
|
||||
|
||||
fun select(selectedItem: Recipient, isSelected: Boolean) {
|
||||
_state.value = state.run {
|
||||
if (isSelected) copy(selectedItems = selectedItems + selectedItem)
|
||||
else copy(selectedItems = selectedItems - selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTitle(context: Context): String =
|
||||
if (state.selectedItems.size == 1) {
|
||||
context.getString(R.string.Unblock_dialog__title_single, state.selectedItems.first().name)
|
||||
} else {
|
||||
context.getString(R.string.Unblock_dialog__title_multiple)
|
||||
}
|
||||
|
||||
fun getMessage(context: Context): String {
|
||||
if (state.selectedItems.size == 1) {
|
||||
return context.getString(R.string.Unblock_dialog__message, state.selectedItems.first().name)
|
||||
}
|
||||
val stringBuilder = StringBuilder()
|
||||
val iterator = state.selectedItems.iterator()
|
||||
var numberAdded = 0
|
||||
while (iterator.hasNext() && numberAdded < 3) {
|
||||
val nextRecipient = iterator.next()
|
||||
if (numberAdded > 0) stringBuilder.append(", ")
|
||||
|
||||
stringBuilder.append(nextRecipient.name)
|
||||
numberAdded++
|
||||
}
|
||||
val overflow = state.selectedItems.size - numberAdded
|
||||
if (overflow > 0) {
|
||||
stringBuilder.append(" ")
|
||||
val string = context.resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow)
|
||||
stringBuilder.append(string.format(overflow))
|
||||
}
|
||||
return context.getString(R.string.Unblock_dialog__message, stringBuilder.toString())
|
||||
}
|
||||
|
||||
fun toggle(selectable: SelectableItem<Recipient>) {
|
||||
_state.value = state.run {
|
||||
if (selectable.item in selectedItems) copy(selectedItems = selectedItems - selectable.item)
|
||||
else copy(selectedItems = selectedItems + selectable.item)
|
||||
}
|
||||
}
|
||||
|
||||
data class BlockedContactsViewState(
|
||||
val blockedContacts: List<Recipient>
|
||||
)
|
||||
val blockedContacts: List<Recipient> = emptyList(),
|
||||
val selectedItems: Set<Recipient> = emptySet()
|
||||
) {
|
||||
val items = blockedContacts.map { SelectableItem(it, it in selectedItems) }
|
||||
|
||||
val unblockButtonEnabled get() = selectedItems.isNotEmpty()
|
||||
val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty()
|
||||
val nonEmptyStateGroupVisible get() = blockedContacts.isNotEmpty()
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ class ClearAllDataDialog : BaseDialog() {
|
||||
var selectedOption = device
|
||||
val optionAdapter = RadioOptionAdapter { selectedOption = it }
|
||||
binding.recyclerView.apply {
|
||||
itemAnimator = null
|
||||
adapter = optionAdapter
|
||||
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
setHasFixedSize(true)
|
||||
|
@ -1,42 +1,41 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import network.loki.messenger.databinding.DialogListPreferenceBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
class ListPreferenceDialog(
|
||||
private val listPreference: ListPreference,
|
||||
private val dialogListener: () -> Unit
|
||||
) : BaseDialog() {
|
||||
private lateinit var binding: DialogListPreferenceBinding
|
||||
fun listPreferenceDialog(
|
||||
context: Context,
|
||||
listPreference: ListPreference,
|
||||
dialogListener: () -> Unit
|
||||
) : AlertDialog {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val builder = AlertDialog.Builder(context)
|
||||
|
||||
val binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(context))
|
||||
binding.titleTextView.text = listPreference.dialogTitle
|
||||
binding.messageTextView.text = listPreference.dialogMessage
|
||||
binding.closeButton.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
val options = listPreference.entryValues.zip(listPreference.entries) { value, title ->
|
||||
RadioOption(value.toString(), title.toString())
|
||||
}
|
||||
val valueIndex = listPreference.findIndexOfValue(listPreference.value)
|
||||
val optionAdapter = RadioOptionAdapter(valueIndex) {
|
||||
listPreference.value = it.value
|
||||
dismiss()
|
||||
dialogListener.invoke()
|
||||
}
|
||||
binding.recyclerView.apply {
|
||||
adapter = optionAdapter
|
||||
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
optionAdapter.submitList(options)
|
||||
builder.setView(binding.root)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
builder.setView(binding.root)
|
||||
|
||||
val dialog = builder.show()
|
||||
|
||||
val valueIndex = listPreference.findIndexOfValue(listPreference.value)
|
||||
RadioOptionAdapter(valueIndex) {
|
||||
listPreference.value = it.value
|
||||
dialog.dismiss()
|
||||
dialogListener()
|
||||
}
|
||||
.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
|
||||
}
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -77,10 +79,10 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
ListPreference listPreference = (ListPreference) preference;
|
||||
listPreference.setDialogMessage(R.string.preferences_notifications__content_message);
|
||||
new ListPreferenceDialog(listPreference, () -> {
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
listPreferenceDialog(getContext(), listPreference, () -> {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
return null;
|
||||
}).show(getChildFragmentManager(), "ListPreferenceDialog");
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -16,8 +16,8 @@ class RadioOptionAdapter(
|
||||
) : ListAdapter<RadioOption, RadioOptionAdapter.ViewHolder>(RadioOptionDiffer()) {
|
||||
|
||||
class RadioOptionDiffer: DiffUtil.ItemCallback<RadioOption>() {
|
||||
override fun areItemsTheSame(oldItem: RadioOption, newItem: RadioOption) = oldItem === newItem
|
||||
override fun areContentsTheSame(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.value == newItem.value
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@ -31,7 +31,7 @@ class RadioOptionAdapter(
|
||||
holder.bind(option, isSelected) {
|
||||
onClickListener(it)
|
||||
selectedOptionPosition = position
|
||||
notifyDataSetChanged()
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,7 @@ package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
@ -19,6 +16,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import network.loki.messenger.BuildConfig
|
||||
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.successUi
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.ProfilePictureUtilities
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
import org.thoughtcrime.securesms.home.PathActivity
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
@ -57,8 +53,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private var displayNameEditActionMode: ActionMode? = null
|
||||
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
||||
private lateinit var glide: GlideRequests
|
||||
private var displayNameToBeUploaded: String? = null
|
||||
private var profilePictureToBeUploaded: ByteArray? = null
|
||||
private var tempFile: File? = null
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
@ -76,14 +70,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||
val displayName = getDisplayName()
|
||||
glide = GlideApp.with(this)
|
||||
with(binding) {
|
||||
profilePictureView.root.glide = glide
|
||||
profilePictureView.root.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.root.displayName = displayName
|
||||
profilePictureView.root.isLarge = true
|
||||
profilePictureView.root.update()
|
||||
setupProfilePictureView(profilePictureView.root)
|
||||
profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
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) {
|
||||
super.onSaveInstanceState(outState)
|
||||
val scrollBundle = SparseArray<Parcelable>()
|
||||
@ -154,9 +155,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
AsyncTask.execute {
|
||||
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 {
|
||||
updateProfile(true)
|
||||
updateProfile(true, profilePictureToBeUploaded)
|
||||
}
|
||||
} catch (e: BitmapDecodingException) {
|
||||
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
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
val displayName = displayNameToBeUploaded
|
||||
if (displayName != null) {
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
}
|
||||
val profilePicture = profilePictureToBeUploaded
|
||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
if (isUpdatingProfilePicture) {
|
||||
if (profilePicture != null) {
|
||||
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
|
||||
} else {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, System.currentTimeMillis())
|
||||
TextSecurePreferences.setProfilePictureURL(this, null)
|
||||
}
|
||||
}
|
||||
val compoundPromise = all(promises)
|
||||
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)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
}
|
||||
@ -218,12 +226,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
if (displayName != null) {
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
if (isUpdatingProfilePicture) {
|
||||
binding.profilePictureView.root.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.root.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
profilePictureToBeUploaded = null
|
||||
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()
|
||||
return false
|
||||
}
|
||||
displayNameToBeUploaded = displayName
|
||||
updateProfile(false)
|
||||
updateProfile(false, displayName = displayName)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -255,6 +260,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
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.
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
|
@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
@ -13,6 +14,7 @@ import androidx.annotation.DimenRes
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.graphics.applyCanvas
|
||||
|
||||
fun View.contains(point: PointF): Boolean {
|
||||
return hitRect.contains(point.x.toInt(), point.y.toInt())
|
||||
@ -65,3 +67,9 @@ fun View.hideKeyboard() {
|
||||
val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap =
|
||||
Bitmap.createBitmap(width, height, config).applyCanvas {
|
||||
translate(-scrollX.toFloat(), -scrollY.toFloat())
|
||||
draw(this)
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
package org.thoughtcrime.securesms.util.adapter
|
||||
|
||||
data class SelectableItem<T>(val item: T, val isSelected: Boolean)
|
5
app/src/main/res/color/button_destructive.xml
Normal file
5
app/src/main/res/color/button_destructive.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="?android:textColorTertiary"/>
|
||||
<item android:color="@color/destructive"/>
|
||||
</selector>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorCellRipple">
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?conversation_pinned_background_color" />
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorCellRipple">
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?conversation_unread_background_color" />
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorCellRipple">
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?colorCellBackground" />
|
||||
|
@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/transparent" />
|
||||
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/button_destructive">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorPrimary"/>
|
||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
||||
|
||||
<stroke android:width="@dimen/border_thickness" android:color="@color/destructive" />
|
||||
<stroke
|
||||
android:color="@color/button_destructive"
|
||||
android:width="@dimen/border_thickness" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?mention_candidates_view_background_ripple">
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?mention_candidates_view_background" />
|
||||
|
@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/transparent" />
|
||||
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?prominentButtonColor">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorPrimary"/>
|
||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
||||
|
||||
<stroke android:width="@dimen/border_thickness" android:color="?prominentButtonColor" />
|
||||
<stroke
|
||||
android:color="?prominentButtonColor"
|
||||
android:width="@dimen/border_thickness" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorCellRipple">
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?colorCellBackground" />
|
||||
|
6
app/src/main/res/drawable/view_separator.xml
Normal file
6
app/src/main/res/drawable/view_separator.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<stroke android:color="?colorDividerBackground" android:width="1dp"/>
|
||||
<corners android:radius="16dp"/>
|
||||
<solid android:color="?colorPrimary"/>
|
||||
</shape>
|
@ -4,28 +4,37 @@
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/unblockButton"
|
||||
app:cardCornerRadius="?preferenceCornerRadius"
|
||||
app:cardElevation="0dp"
|
||||
app:cardBackgroundColor="?colorSettingsBackground"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/unblockButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/preference_single_no_padding"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyStateMessageTextView"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="@+id/recyclerView"
|
||||
app:layout_constraintTop_toTopOf="@+id/cardView"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
app:layout_constraintStart_toStartOf="@+id/recyclerView"
|
||||
app:layout_constraintEnd_toEndOf="@+id/recyclerView"
|
||||
app:layout_constraintStart_toStartOf="@+id/cardView"
|
||||
app:layout_constraintEnd_toEndOf="@+id/cardView"
|
||||
android:text="@string/blocked_contacts_empty_state"
|
||||
/>
|
||||
|
||||
@ -38,7 +47,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/recyclerView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardView"
|
||||
android:id="@+id/unblockButton"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
@ -49,6 +58,6 @@
|
||||
android:id="@+id/nonEmptyStateGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="unblockButton,recyclerView"/>
|
||||
app:constraint_referenced_ids="unblockButton,cardView"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -138,6 +138,7 @@
|
||||
android:layout_height="50dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_above="@+id/messageRequestBar"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
|
@ -11,7 +11,6 @@
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height" />
|
||||
|
@ -12,7 +12,6 @@
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height" />
|
||||
|
@ -6,7 +6,6 @@
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height" />
|
||||
|
@ -61,7 +61,7 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledSeparatorView
|
||||
<include layout="@layout/view_separator"
|
||||
android:id="@+id/separatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -43,7 +43,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/unblockButton"
|
||||
android:contentDescription="@string/AccessibilityId_block_confirm"
|
||||
android:layout_width="0dp"
|
||||
|
17
app/src/main/res/layout/dialog_change_avatar.xml
Normal file
17
app/src/main/res/layout/dialog_change_avatar.xml
Normal 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>
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -44,7 +44,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/downloadButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -43,7 +43,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/joinButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -44,7 +44,7 @@
|
||||
android:contentDescription="@string/AccessibilityId_cancel_link_preview_button"/>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/enableLinkPreviewsButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -3,8 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?dialog_background_color">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
@ -21,11 +20,10 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeButton"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@ -36,7 +34,6 @@
|
||||
android:id="@+id/messageTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?dialog_background_color"
|
||||
android:drawablePadding="@dimen/large_spacing"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -43,7 +43,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/openURLButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -35,7 +35,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -43,7 +43,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Destructive"
|
||||
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
||||
android:id="@+id/sendSeedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -33,7 +33,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
@ -41,7 +41,7 @@
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
|
@ -70,7 +70,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/promptTextView">
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledSeparatorView
|
||||
<include layout="@layout/view_separator"
|
||||
android:id="@+id/separatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
|
@ -47,7 +47,6 @@
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -48,7 +48,6 @@
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -62,7 +62,7 @@
|
||||
android:text="@string/fragment_view_my_qr_code_explanation" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantOutline"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
|
@ -28,7 +28,6 @@
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Widget.Session.TabLayout"
|
||||
app:tabRippleColor="@color/cell_selected"
|
||||
app:tabIndicatorColor="?colorAccent"
|
||||
android:scrollbars="horizontal"/>
|
||||
|
@ -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>
|
@ -21,7 +21,6 @@
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableTabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"/>
|
||||
|
@ -1,17 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:layout_gravity="center"
|
||||
android:background="?colorDividerBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/view_separator"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:text="@string/your_session_id" />
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
@ -32,6 +32,11 @@
|
||||
android:icon="?menu_copy_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:title="@string/conversation_context__menu_resync_message"
|
||||
android:id="@+id/menu_context_resync"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:title="@string/conversation_context__menu_resend_message"
|
||||
android:id="@+id/menu_context_resend"
|
||||
|
@ -150,7 +150,6 @@
|
||||
<attr name="conversation_shadow_main" format="color|reference"/>
|
||||
<attr name="default_background_start" 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="colorSettingsBackground" 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_border" 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_border" format="color|reference"/>
|
||||
<attr name="conversation_unread_count_indicator_background" format="color|reference"/>
|
||||
|
@ -112,6 +112,7 @@
|
||||
<!-- Conversation View-->
|
||||
<string name="AccessibilityId_message_sent_status_tick">Message sent status: Sent</string>
|
||||
<string name="AccessibilityId_message_sent_status_pending">Message sent status pending</string>
|
||||
<string name="AccessibilityId_message_sent_status_syncing">Message sent status syncing</string>
|
||||
<string name="AccessibilityId_message_request_config_message">Message request has been accepted</string>
|
||||
<string name="AccessibilityId_message_body">Message Body</string>
|
||||
<string name="AccessibilityId_voice_message">Voice message</string>
|
||||
@ -627,6 +628,7 @@
|
||||
<string name="conversation_context__menu_delete_message">Delete message</string>
|
||||
<string name="conversation_context__menu_ban_user">Ban user</string>
|
||||
<string name="conversation_context__menu_ban_and_delete_all">Ban and delete all</string>
|
||||
<string name="conversation_context__menu_resync_message">Resync message</string>
|
||||
<string name="conversation_context__menu_resend_message">Resend message</string>
|
||||
<string name="conversation_context__menu_reply">Reply</string>
|
||||
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
|
||||
@ -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="fragment_enter_chat_url_edit_text_hint">Enter an open group URL</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_missing_error">Please pick a 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="ErrorNotifier_migration">Database Upgrade Failed</string>
|
||||
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error.</string>
|
||||
<string name="delivery_status_syncing">Syncing</string>
|
||||
<string name="delivery_status_sending">Sending</string>
|
||||
<string name="delivery_status_read">Read</string>
|
||||
<string name="delivery_status_sent">Sent</string>
|
||||
<string name="delivery_status_sync_failed">Failed to sync</string>
|
||||
<string name="delivery_status_failed">Failed to send</string>
|
||||
<string name="giphy_permission_title">Search GIFs?</string>
|
||||
<string name="giphy_permission_message">Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.</string>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<resources>
|
||||
|
||||
<!-- Session -->
|
||||
<style name="Widget.Session.ActionBar" parent="Widget.AppCompat.Light.ActionBar.Solid">
|
||||
@ -29,8 +29,6 @@
|
||||
<item name="backgroundTint">?colorPrimary</item>
|
||||
<item name="android:backgroundDimEnabled">true</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:windowBackground">@null</item>
|
||||
<item name="textColorAlertDialogListItem">?android:textColorPrimary</item>
|
||||
@ -46,14 +44,6 @@
|
||||
<item name="android:background">@drawable/default_bottom_sheet_background</item>
|
||||
</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>
|
||||
@ -66,7 +56,6 @@
|
||||
<item name="elevation">1dp</item>
|
||||
<item name="tabIndicatorColor">@color/transparent</item>
|
||||
<item name="tabIndicatorHeight">@dimen/accent_line_thickness</item>
|
||||
<item name="tabRippleColor">@color/cell_selected</item>
|
||||
<item name="tabTextAppearance">@style/TextAppearance.Session.Tab</item>
|
||||
</style>
|
||||
|
||||
@ -93,38 +82,38 @@
|
||||
<style name="Widget.Session.Button.Common.ProminentFilled">
|
||||
<item name="android:background">@drawable/prominent_filled_button_medium_background</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 name="Widget.Session.Button.Common.ProminentOutline">
|
||||
<item name="android:background">@drawable/prominent_outline_button_medium_background</item>
|
||||
<item name="android:textColorPrimary">?attr/prominentButtonColor</item>
|
||||
<item name="android:drawableTint" tools:ignore="NewApi">?attr/prominentButtonColor</item>
|
||||
<item name="android:textColor">?attr/prominentButtonColor</item>
|
||||
<item name="android:drawableTint">?attr/prominentButtonColor</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Session.Button.Common.UnimportantFilled">
|
||||
<item name="android:background">@drawable/unimportant_filled_button_medium_background</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 name="Widget.Session.Button.Common.UnimportantOutline">
|
||||
<item name="android:background">@drawable/unimportant_outline_button_medium_background</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 name="Widget.Session.Button.Common.UnimportantDestructive">
|
||||
<item name="android:background">@drawable/unimportant_outline_button_medium_background</item>
|
||||
<item name="android:textColor">?android:textColorPrimary</item>
|
||||
<item name="android:backgroundTint" tools:ignore="NewApi">@color/destructive</item>
|
||||
<item name="android:drawableTint" tools:ignore="NewApi">@color/destructive</item>
|
||||
<item name="android:backgroundTint">@color/destructive</item>
|
||||
<item name="android:drawableTint">@color/destructive</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Session.Button.Common.DestructiveOutline">
|
||||
<item name="android:background">@drawable/destructive_outline_button_medium_background</item>
|
||||
<item name="android:textColor">@color/destructive</item>
|
||||
<item name="android:drawableTint" tools:ignore="NewApi">?android:textColorPrimary</item>
|
||||
<item name="android:textColor">@color/button_destructive</item>
|
||||
<item name="android:drawableTint">?android:textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Session.Button.Dialog" parent="">
|
||||
@ -133,19 +122,10 @@
|
||||
<item name="android:textColor">?android:textColorPrimary</item>
|
||||
</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">
|
||||
<item name="android:background">@drawable/unimportant_dialog_text_button_background</item>
|
||||
</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">
|
||||
<item name="android:background">@drawable/destructive_dialog_text_button_background</item>
|
||||
<item name="android:textColor">@color/destructive</item>
|
||||
|
@ -303,6 +303,8 @@
|
||||
<item name="dividerHorizontal">?dividerVertical</item>
|
||||
<item name="message_received_background_color">#F2F2F2</item>
|
||||
<item name="colorAccent">@color/classic_accent</item>
|
||||
<item name="colorControlHighlight">?android:colorControlHighlight</item>
|
||||
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
|
||||
</style>
|
||||
|
||||
<style name="Ocean">
|
||||
@ -310,6 +312,8 @@
|
||||
<item name="dividerHorizontal">?dividerVertical</item>
|
||||
<item name="message_received_background_color">#F2F2F2</item>
|
||||
<item name="colorAccent">@color/ocean_accent</item>
|
||||
<item name="colorControlHighlight">?android:colorControlHighlight</item>
|
||||
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
|
||||
</style>
|
||||
|
||||
<style name="Classic.Dark">
|
||||
@ -319,7 +323,6 @@
|
||||
<item name="colorPrimaryDark">@color/classic_dark_0</item>
|
||||
<item name="colorControlNormal">?android:textColorPrimary</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:textColorPrimary">@color/classic_dark_6</item>
|
||||
<item name="android:textColorSecondary">?android:textColorPrimary</item>
|
||||
<item name="android:textColorTertiary">@color/classic_dark_5</item>
|
||||
@ -334,7 +337,7 @@
|
||||
<item name="colorCellBackground">@color/classic_dark_1</item>
|
||||
<item name="colorSettingsBackground">@color/classic_dark_1</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="actionBarWidgetTheme">@null</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_border">@color/classic_dark_3</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_border">@color/classic_dark_3</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="colorControlNormal">?android:textColorPrimary</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:textColorPrimary">@color/classic_light_0</item>
|
||||
<item name="android:textColorSecondary">@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="colorSettingsBackground">@color/classic_light_5</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="android:actionMenuTextColor">?android:textColorPrimary</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_border">@color/classic_light_2</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_border">@color/classic_light_2</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="colorControlNormal">@color/ocean_dark_7</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:textColorPrimary">@color/ocean_dark_7</item>
|
||||
<item name="android:textColorSecondary">@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="colorSettingsBackground">@color/ocean_dark_1</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="popupTheme">?actionBarPopupTheme</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_border">?colorPrimary</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_border">?colorPrimary</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="colorControlNormal">@color/ocean_light_1</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:textColorPrimary">@color/ocean_light_1</item>
|
||||
<item name="android:textColorSecondary">@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="colorSettingsBackground">@color/ocean_light_6</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="actionBarPopupTheme">@style/Light.Popup</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_border">@color/ocean_light_1</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_border">?input_bar_button_background_opaque_border</item>
|
||||
<item name="conversation_unread_count_indicator_background">?colorAccent</item>
|
||||
|
@ -10,7 +10,7 @@
|
||||
android:summary="@string/preferences_notifications_strategy_category_fast_mode_summary"
|
||||
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" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
@ -107,10 +107,13 @@ interface StorageProtocol {
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
|
||||
fun markAsResyncing(timestamp: Long, author: String)
|
||||
fun markAsSyncing(timestamp: Long, author: String)
|
||||
fun markAsSending(timestamp: Long, author: String)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun clearErrorMessage(messageID: Long)
|
||||
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||
|
||||
@ -200,6 +203,6 @@ interface StorageProtocol {
|
||||
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
|
||||
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
|
||||
fun deleteReactions(messageId: Long, mms: Boolean)
|
||||
fun unblock(toUnblock: List<Recipient>)
|
||||
fun unblock(toUnblock: Iterable<Recipient>)
|
||||
fun blockedContacts(): List<Recipient>
|
||||
}
|
||||
|
@ -7,13 +7,13 @@ import org.session.libsignal.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(var publicKey: String) : Destination() {
|
||||
data class Contact(var publicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
data class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
|
||||
data class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
|
||||
internal constructor(): this("", "")
|
||||
}
|
||||
|
||||
|
@ -11,20 +11,22 @@ import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
|
||||
class VisibleMessage : Message() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var text: String? = null
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||
var quote: Quote? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var profile: Profile? = null
|
||||
var openGroupInvitation: OpenGroupInvitation? = null
|
||||
var reaction: Reaction? = null
|
||||
class VisibleMessage(
|
||||
var syncTarget: String? = null,
|
||||
var text: String? = null,
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf(),
|
||||
var quote: Quote? = null,
|
||||
var linkPreview: LinkPreview? = null,
|
||||
var profile: Profile? = null,
|
||||
var openGroupInvitation: OpenGroupInvitation? = null,
|
||||
var reaction: Reaction? = null,
|
||||
var hasMention: Boolean = false
|
||||
) : Message() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
|
@ -61,11 +61,11 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
fun send(message: Message, destination: Destination, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
||||
sendToOpenGroupDestination(destination, message)
|
||||
} else {
|
||||
sendToSnodeDestination(destination, message)
|
||||
sendToSnodeDestination(destination, message, isSyncMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ object MessageSender {
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
handleFailedMessageSend(message, error, isSyncMessage)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
@ -374,16 +374,23 @@ object MessageSender {
|
||||
// • the destination was a contact
|
||||
// • we didn't sync it already
|
||||
if (destination is Destination.Contact && !isSyncMessage) {
|
||||
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
||||
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
||||
if (message is VisibleMessage) message.syncTarget = destination.publicKey
|
||||
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
|
||||
|
||||
storage.markAsSyncing(message.sentTimestamp!!, userPublicKey)
|
||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleFailedMessageSend(message: Message, error: Exception) {
|
||||
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
|
||||
|
||||
val timestamp = message.sentTimestamp!!
|
||||
val author = message.sender ?: userPublicKey
|
||||
|
||||
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)
|
||||
else storage.markAsSentFailed(timestamp, author, error)
|
||||
}
|
||||
|
||||
// Convenience
|
||||
|
@ -7,16 +7,17 @@ import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
|
||||
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 = ""
|
||||
val updateData = updateMessageData.kind ?: return message
|
||||
if (!isOutgoing && sender == null) return message
|
||||
if (!isOutgoing && senderId == null) return message
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
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) }
|
||||
|
||||
when (updateData) {
|
||||
@ -77,11 +78,11 @@ object UpdateMessageBuilder {
|
||||
return message
|
||||
}
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && sender == null) return ""
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && senderId == null) return ""
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
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) }
|
||||
return if (duration <= 0) {
|
||||
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 senderName = storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender!!
|
||||
val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
||||
return when (kind) {
|
||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
||||
|
@ -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
|
@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.max
|
||||
|
||||
@ColorInt
|
||||
fun Context.getColorFromAttr(
|
||||
@ -14,3 +16,6 @@ fun Context.getColorFromAttr(
|
||||
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
val RecyclerView.isScrolledToBottom: Boolean
|
||||
get() = max(0, computeVerticalScrollOffset()) + computeVerticalScrollExtent() >= computeVerticalScrollRange()
|
||||
|
Loading…
x
Reference in New Issue
Block a user