diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java index 51f66ec323..c43d406575 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java @@ -39,15 +39,20 @@ public abstract class BaseActionBarActivity extends AppCompatActivity { public int getDesiredTheme() { ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences()); int userSelectedTheme = themeState.getTheme(); + + // If the user has configured Session to follow the system light/dark theme mode then do so.. if (themeState.getFollowSystem()) { - // do light or dark based on the selected theme + + // Use light or dark versions of the user's theme based on light-mode / dark-mode settings boolean isDayUi = UiModeUtilities.isDayUiMode(this); if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) { return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark; } else { return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark; } - } else { + } + else // ..otherwise just return their selected theme. + { return userSelectedTheme; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt index 39e2dac935..a9b5476aa5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt @@ -8,13 +8,14 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.Button import android.widget.LinearLayout import android.widget.LinearLayout.VERTICAL +import android.widget.Space import android.widget.TextView import androidx.annotation.AttrRes import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.annotation.StyleRes import androidx.appcompat.app.AlertDialog -import androidx.core.view.setPadding +import androidx.core.view.setMargins import androidx.core.view.updateMargins import androidx.fragment.app.Fragment import network.loki.messenger.R @@ -36,7 +37,9 @@ class SessionDialogBuilder(val context: Context) { private var dialog: AlertDialog? = null private fun dismiss() = dialog?.dismiss() - private val topView = LinearLayout(context).apply { orientation = VERTICAL } + private val topView = LinearLayout(context) + .apply { setPadding(0, dp20, 0, 0) } + .apply { orientation = VERTICAL } .also(dialogBuilder::setCustomTitle) private val contentView = LinearLayout(context).apply { orientation = VERTICAL } private val buttonLayout = LinearLayout(context) @@ -52,14 +55,14 @@ class SessionDialogBuilder(val context: Context) { fun title(text: CharSequence?) = title(text?.toString()) fun title(text: String?) { - text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) } + text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) } } fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style) fun text(text: CharSequence?, @StyleRes style: Int = 0) { text(text, style) { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) - .apply { updateMargins(dp40, 0, dp40, dp20) } + .apply { updateMargins(dp40, 0, dp40, 0) } } } @@ -71,6 +74,10 @@ class SessionDialogBuilder(val context: Context) { textAlignment = View.TEXT_ALIGNMENT_CENTER modify() }.let(topView::addView) + + Space(context).apply { + layoutParams = LinearLayout.LayoutParams(0, dp20) + }.let(topView::addView) } fun view(view: View) = contentView.addView(view) @@ -123,7 +130,8 @@ class SessionDialogBuilder(val context: Context) { ) = Button(context, null, 0, style).apply { setText(text) contentDescription = resources.getString(contentDescriptionRes) - layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, dp60, 1f) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f) + .apply { setMargins(dp20) } setOnClickListener { listener.invoke() if (dismiss) dismiss() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt index 31870e9b7a..69dec0cdd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import network.loki.messenger.R +import org.session.libsession.utilities.getColorFromAttr import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel @@ -84,7 +85,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) { context.theme.resolveAttribute(item.iconRes, typedValue, true) icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId)) - icon.imageTintList = color?.let(ColorStateList::valueOf) + icon.imageTintList = ColorStateList.valueOf(color ?: context.getColorFromAttr(android.R.attr.textColor)) } item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it } title.setText(item.title) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 5959c41d16..f9cbea2b1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -140,6 +140,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) { + quoteView?.let(binding.inputBarAdditionalContentContainer::removeView) + quote = message // If we already have a link preview View then clear the 'additional content' layout so that diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 1ba4a0c3e5..a52d8458f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -4,18 +4,24 @@ import android.content.Context import android.graphics.Typeface import android.text.Spannable import android.text.SpannableString +import android.text.style.BackgroundColorSpan import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.util.Range +import androidx.appcompat.widget.ThemeUtils import androidx.core.content.res.ResourcesCompat import network.loki.messenger.R import nl.komponents.kovenant.combine.Tuple2 import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.ThemeUtil +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.getAccentColor +import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr +import org.thoughtcrime.securesms.util.getMessageTextColourAttr import java.util.regex.Pattern object MentionUtilities { @@ -58,15 +64,37 @@ object MentionUtilities { } } val result = SpannableString(text) - val isLightMode = UiModeUtilities.isDayUiMode(context) - val color = if (isOutgoingMessage) { - ResourcesCompat.getColor(context.resources, if (isLightMode) R.color.white else R.color.black, context.theme) - } else { - context.getAccentColor() + + var mentionTextColour: Int? = null + // In dark themes.. + if (ThemeUtil.isDarkTheme(context)) { + // ..we use the standard outgoing message colour for outgoing messages.. + if (isOutgoingMessage) { + val mentionTextColourAttributeId = getMessageTextColourAttr(true) + val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId) + mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme) + } + else // ..but we use the accent colour for incoming messages (i.e., someone mentioning us).. + { + mentionTextColour = context.getAccentColor() + } } + else // ..while in light themes we always just use the incoming or outgoing message text colour for mentions. + { + val mentionTextColourAttributeId = getMessageTextColourAttr(isOutgoingMessage) + val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId) + mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme) + } + for (mention in mentions) { - result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + result.setSpan(ForegroundColorSpan(mentionTextColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + // If we're using a light theme then we change the background colour of the mention to be the accent colour + if (ThemeUtil.isLightTheme(context)) { + val backgroundColour = context.getAccentColor(); + result.setSpan(BackgroundColorSpan(backgroundColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } } return result } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 1bf1dcdb15..da4f39f0c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -320,6 +320,19 @@ public class MmsSmsDatabase extends Database { return -1; } + public long getLastMessageTimestamp(long threadId) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; + + try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) { + if (cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT)); + } + } + + return -1; + } + public Cursor getUnread() { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC"; String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 507088a0ad..ef3937d4d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -934,7 +934,17 @@ public class ThreadDatabase extends Database { readReceiptCount = 0; } - return new ThreadRecord(body, snippetUri, recipient, date, count, + MessageRecord lastMessage = null; + + if (count > 0) { + MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); + long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId); + if (messageTimestamp > 0) { + lastMessage = mmsSmsDatabase.getMessageForTimestamp(messageTimestamp); + } + } + + return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count, unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index f3e72a8747..0c023a8f29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -43,6 +43,7 @@ import network.loki.messenger.R; public class ThreadRecord extends DisplayRecord { private @Nullable final Uri snippetUri; + public @Nullable final MessageRecord lastMessage; private final long count; private final int unreadCount; private final int unreadMentionCount; @@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord { private final int initialRecipientHash; public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, - @NonNull Recipient recipient, long date, long count, int unreadCount, + @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, long snippetType, int distributionType, boolean archived, long expiresIn, long lastSeen, int readReceiptCount, boolean pinned) { super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); this.snippetUri = snippetUri; + this.lastMessage = lastMessage; this.count = count; this.unreadCount = unreadCount; this.unreadMentionCount = unreadMentionCount; diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index c876edb822..c9896a5b8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.ColorDrawable +import android.text.SpannableString +import android.text.TextUtils import android.util.AttributeSet import android.util.TypedValue import android.view.View @@ -89,7 +91,7 @@ class ConversationView : LinearLayout { || (configFactory.convoVolatile?.getConversationUnread(thread) == true) binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) - val senderDisplayName = getUserDisplayName(thread.recipient) + val senderDisplayName = getTitle(thread.recipient) ?: thread.recipient.address.toString() binding.conversationViewDisplayNameTextView.text = senderDisplayName binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) } @@ -101,9 +103,7 @@ class ConversationView : LinearLayout { R.drawable.ic_notifications_mentions } binding.muteIndicatorImageView.setImageResource(drawableRes) - val rawSnippet = thread.getDisplayBody(context) - val snippet = highlightMentions(rawSnippet, thread.threadId, context) - binding.snippetTextView.text = snippet + binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context) binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { @@ -131,12 +131,21 @@ class ConversationView : LinearLayout { binding.profilePictureView.recycle() } - private fun getUserDisplayName(recipient: Recipient): String? { - return if (recipient.isLocalNumber) { - context.getString(R.string.note_to_self) - } else { - recipient.toShortString() // Internally uses the Contact API - } + private fun getTitle(recipient: Recipient): String? = when { + recipient.isLocalNumber -> context.getString(R.string.note_to_self) + else -> recipient.toShortString() // Internally uses the Contact API + } + + private fun ThreadRecord.getSnippet(): CharSequence = + concatSnippet(getSnippetPrefix(), getDisplayBody(context)) + + private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence = + prefix?.let { TextUtils.concat(it, ": ", body) } ?: body + + private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when { + recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null + lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you) + else -> lastMessage?.individualRecipient?.toShortString() } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt index dfd4ffe419..9c427e1f86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt @@ -9,12 +9,15 @@ import android.graphics.Bitmap import android.graphics.PointF import android.graphics.Rect import android.util.Size +import android.util.TypedValue import android.view.View import androidx.annotation.ColorInt import androidx.annotation.DimenRes import network.loki.messenger.R import org.session.libsession.utilities.getColorFromAttr import android.view.inputmethod.InputMethodManager +import androidx.annotation.AttrRes +import androidx.annotation.ColorRes import androidx.core.graphics.applyCanvas import kotlin.math.roundToInt @@ -32,6 +35,20 @@ val View.hitRect: Rect @ColorInt fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent) +// Method to grab the appropriate attribute for a message colour. +// Note: This is an attribute, NOT a resource Id - see `getColorResourceIdFromAttr` for that. +@AttrRes +fun getMessageTextColourAttr(messageIsOutgoing: Boolean): Int = + if (messageIsOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color + +// Method to get an actual R.id. resource Id from an attribute such as R.attr.message_sent_text_color etc. +@ColorRes +fun getColorResourceIdFromAttr(context: Context, attr: Int): Int { + val typedValue = TypedValue() + context.theme.resolveAttribute(attr, typedValue, true) + return typedValue.resourceId +} + fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) { val startSize = resources.getDimension(startSizeID) val endSize = resources.getDimension(endSizeID) @@ -70,7 +87,6 @@ fun View.hideKeyboard() { imm.hideSoftInputFromWindow(this.windowToken, 0) } - fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap { val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth) val scale = size.width / measuredWidth.toFloat() diff --git a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt index fd16061e67..c3537a334a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt @@ -2,7 +2,7 @@ package org.session.libsession.messaging.mentions import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact -import java.util.* +import java.util.Locale object MentionsManager { var userPublicKeyCache = mutableMapOf>() // Thread ID to set of user hex encoded public keys diff --git a/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java b/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java index dc77aae5ac..22375b1da7 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java +++ b/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java @@ -27,6 +27,10 @@ public class ThemeUtil { return getAttributeText(context, R.attr.theme_type, "light").equals("dark"); } + public static boolean isLightTheme(@NonNull Context context) { + return getAttributeText(context, R.attr.theme_type, "light").equals("light"); + } + public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) { TypedValue typedValue = new TypedValue(); Resources.Theme theme = context.getTheme();