Skip to content

Commit d56e812

Browse files
Merge pull request session-foundation#2027 from session-foundation/feature/ConvoV3-pt2
Feature/convo v3 pt2
2 parents 40d69fc + 7364e5e commit d56e812

29 files changed

Lines changed: 2314 additions & 850 deletions

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ dependencies {
365365
implementation(libs.androidx.fragment.ktx)
366366
implementation(libs.androidx.core.ktx)
367367
implementation(libs.androidx.interpolator)
368+
implementation(libs.androidx.paging.common)
369+
implementation(libs.androidx.paging.compose)
368370

369371
// Add firebase dependencies to specific variants
370372
for (variant in firebaseEnabledVariants) {

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ import org.thoughtcrime.securesms.conversation.v3.settings.notification.Notifica
168168
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
169169
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
170170
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
171+
import org.thoughtcrime.securesms.conversation.v3.ConversationActivityV3
171172
import org.thoughtcrime.securesms.conversation.v3.ConversationV3Destination
172173
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
173174
import org.thoughtcrime.securesms.database.GroupDatabase
@@ -1059,6 +1060,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
10591060
onSearchQueryChanged = ::onSearchQueryUpdated,
10601061
onSearchQueryClear = { onSearchQueryUpdated("") },
10611062
onSearchCanceled = ::onSearchClosed,
1063+
switchConvoVersion = {
1064+
startActivity(ConversationActivityV3.createIntent(this, address = IntentCompat.getParcelableExtra(intent,
1065+
ADDRESS, Address.Conversable::class.java)!!))
1066+
finish()
1067+
},
10621068
onAvatarPressed = {
10631069
val intent = ConversationSettingsActivity.createIntent(this, address)
10641070
settingsLauncher.launch(intent)

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
100100
ProBadgeText(
101101
modifier = modifier,
102102
text = authorDisplayName,
103-
textStyle = LocalType.current.small.bold().copy(color = Color(textColor)),
103+
textStyle = LocalType.current.base.bold().copy(color = Color(textColor)),
104104
showBadge = authorRecipient.shouldShowProBadge,
105105
badgeColors = if(isOutgoingMessage && mode == Mode.Regular) proBadgeColorOutgoing()
106106
else proBadgeColorStandard()

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt

Lines changed: 139 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import android.text.Spannable
66
import android.text.SpannableString
77
import android.text.style.ForegroundColorSpan
88
import android.text.style.StyleSpan
9-
import android.util.Range
10-
import network.loki.messenger.R
119
import org.session.libsession.utilities.Address.Companion.toAddress
1210
import org.session.libsession.utilities.ThemeUtil
1311
import org.session.libsession.utilities.getColorFromAttr
@@ -17,6 +15,7 @@ import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
1715
import org.thoughtcrime.securesms.database.RecipientRepository
1816
import org.thoughtcrime.securesms.util.RoundedBackgroundSpan
1917
import org.thoughtcrime.securesms.util.getAccentColor
18+
import network.loki.messenger.R
2019
import java.util.regex.Pattern
2120

2221
object MentionUtilities {
@@ -27,152 +26,201 @@ object MentionUtilities {
2726
/**
2827
* In-place replacement on the *live* MentionEditable that the
2928
* input-bar is already using.
30-
*
31-
* It swaps every "@<64-hex>" token for "@DisplayName" **and**
32-
* attaches a MentionSpan so later normalisation still works.
3329
*/
3430
fun substituteIdsInPlace(
3531
editable: MentionEditable,
3632
membersById: Map<String, MentionViewModel.Member>
3733
) {
3834
ACCOUNT_ID.findAll(editable)
39-
.toList() // avoid index shifts
40-
.asReversed() // back-to-front replacement
35+
.toList() // avoid index shifts
36+
.asReversed() // back-to-front replacement
4137
.forEach { m ->
42-
val id = m.groupValues[1]
43-
val member = membersById[id] ?: return@forEach
38+
val id = m.groupValues[1]
39+
val member = membersById[id] ?: return@forEach
4440

4541
val start = m.range.first
46-
val end = m.range.last + 1 // inclusive ➜ exclusive
42+
val end = m.range.last + 1 // inclusive ➜ exclusive
4743

4844
editable.replace(start, end, "@${member.name}")
49-
editable.addMention(member, start .. start + member.name.length + 1)
45+
editable.addMention(member, start..start + member.name.length + 1)
5046
}
5147
}
5248

49+
// ----------------------------
50+
// Shared parsing/substitution core
51+
// ----------------------------
52+
53+
data class MentionToken(
54+
val start: Int, // start in FINAL substituted text
55+
val endExclusive: Int, // end-exclusive in FINAL substituted text
56+
val publicKey: String,
57+
val isSelf: Boolean
58+
)
59+
60+
data class ParsedMentions(
61+
val text: String,
62+
val mentions: List<MentionToken>
63+
)
5364

5465
/**
55-
* Highlights mentions in a given text.
66+
* Shared core:
67+
* - replaces "@<66-hex>" with "@DisplayName"
68+
* - returns the final text + mention ranges (in that final text) + metadata
5669
*
57-
* @param text The text to highlight mentions in.
58-
* @param isOutgoingMessage Whether the message is outgoing.
59-
* @param isQuote Whether the message is a quote.
60-
* @param formatOnly Whether to only format the mentions. If true we only format the text itself,
61-
* for example resolving an accountID to a username. If false we also apply styling, like colors and background.
62-
* @param context The context to use.
63-
* @return A SpannableString with highlighted mentions.
70+
* This is UI-agnostic and is used by BOTH:
71+
* - legacy XML span formatting
72+
* - Compose rich text formatting
6473
*/
6574
@JvmStatic
66-
fun highlightMentions(
75+
fun parseAndSubstituteMentions(
6776
recipientRepository: RecipientRepository,
68-
text: CharSequence,
69-
isOutgoingMessage: Boolean = false,
70-
isQuote: Boolean = false,
71-
formatOnly: Boolean = false,
77+
input: CharSequence,
7278
context: Context
73-
): SpannableString {
74-
@Suppress("NAME_SHADOWING") var text = text
79+
): ParsedMentions {
80+
@Suppress("NAME_SHADOWING")
81+
var text: CharSequence = input
7582

7683
var matcher = pattern.matcher(text)
77-
val mentions = mutableListOf<Pair<Range<Int>, String>>()
84+
val mentions = mutableListOf<MentionToken>()
7885
var startIndex = 0
7986

80-
// Format the mention text
8187
if (matcher.find(startIndex)) {
8288
while (true) {
83-
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
84-
val user = recipientRepository.getRecipientSync(publicKey.toAddress())
89+
val publicKey =
90+
text.subSequence(matcher.start() + 1, matcher.end()).toString() // drop '@'
8591

86-
val userDisplayName: String = if (user.isSelf) {
92+
val user = recipientRepository.getRecipientSync(publicKey.toAddress())
93+
val displayName = if (user.isSelf) {
8794
context.getString(R.string.you)
8895
} else {
8996
user.displayName(attachesBlindedId = true)
9097
}
9198

92-
val mention = "@$userDisplayName"
93-
text = text.subSequence(0, matcher.start()).toString() + mention + text.subSequence(matcher.end(), text.length)
94-
val endIndex = matcher.start() + 1 + userDisplayName.length
95-
startIndex = endIndex
96-
mentions.add(Pair(Range.create(matcher.start(), endIndex), publicKey))
99+
val replacement = "@$displayName"
100+
101+
val newText = buildString(
102+
text.length - (matcher.end() - matcher.start()) + replacement.length
103+
) {
104+
append(text.subSequence(0, matcher.start()))
105+
append(replacement)
106+
append(text.subSequence(matcher.end(), text.length))
107+
}
108+
109+
val start = matcher.start()
110+
val endExclusive = start + replacement.length
111+
112+
mentions += MentionToken(
113+
start = start,
114+
endExclusive = endExclusive,
115+
publicKey = publicKey,
116+
isSelf = user.isSelf
117+
)
118+
119+
text = newText
120+
startIndex = endExclusive
97121

98122
matcher = pattern.matcher(text)
99-
if (!matcher.find(startIndex)) { break }
123+
if (!matcher.find(startIndex)) break
100124
}
101125
}
102-
val result = SpannableString(text)
103126

104-
// apply styling if required
127+
return ParsedMentions(
128+
text = text.toString(),
129+
mentions = mentions
130+
)
131+
}
132+
133+
// ----------------------------
134+
// Legacy (XML/TextView) formatter
135+
// ----------------------------
136+
137+
/**
138+
* Legacy (XML/TextView) formatter.
139+
*
140+
* Highlights mentions in a given text.
141+
*
142+
* @param formatOnly If true we only format the text itself,
143+
* for example resolving an accountID to a username. If false we also apply styling.
144+
*/
145+
@JvmStatic
146+
fun highlightMentions(
147+
recipientRepository: RecipientRepository,
148+
text: CharSequence,
149+
isOutgoingMessage: Boolean = false,
150+
isQuote: Boolean = false,
151+
formatOnly: Boolean = false,
152+
context: Context
153+
): SpannableString {
154+
val parsed = parseAndSubstituteMentions(recipientRepository, text, context)
155+
val result = SpannableString(parsed.text)
156+
157+
if (formatOnly) return result
158+
105159
// Normal text color: black in dark mode and primary text color for light mode
106160
val mainTextColor by lazy {
107161
if (ThemeUtil.isDarkTheme(context)) context.getColor(R.color.black)
108162
else context.getColorFromAttr(android.R.attr.textColorPrimary)
109163
}
110164

111-
// Highlighted text color: primary/accent in dark mode and primary text color for light mode
165+
// Highlighted text color: accent in dark theme and primary text for light
112166
val highlightedTextColor by lazy {
113167
if (ThemeUtil.isDarkTheme(context)) context.getAccentColor()
114168
else context.getColorFromAttr(android.R.attr.textColorPrimary)
115169
}
116170

117-
if(!formatOnly) {
118-
for (mention in mentions) {
119-
val backgroundColor: Int?
120-
val foregroundColor: Int?
171+
parsed.mentions.forEach { mention ->
172+
val backgroundColor: Int?
173+
val foregroundColor: Int?
121174

122-
// quotes
123-
if(isQuote) {
124-
backgroundColor = null
125-
// the text color has different rule depending if the message is incoming or outgoing
126-
foregroundColor = if(isOutgoingMessage) null else highlightedTextColor
127-
}
128-
// incoming message mentioning you
129-
else if (recipientRepository.getRecipientSync(mention.second.toAddress()).isSelf) {
130-
backgroundColor = context.getAccentColor()
131-
foregroundColor = mainTextColor
132-
}
133-
// outgoing message
134-
else if (isOutgoingMessage) {
135-
backgroundColor = null
136-
foregroundColor = mainTextColor
137-
}
138-
// incoming messages mentioning someone else
139-
else {
140-
backgroundColor = null
141-
// accent color for dark themes and primary text for light
142-
foregroundColor = highlightedTextColor
143-
}
175+
// quotes
176+
if (isQuote) {
177+
backgroundColor = null
178+
// incoming quote gets accent-ish foreground, outgoing quote keeps default
179+
foregroundColor = if (isOutgoingMessage) null else highlightedTextColor
180+
}
181+
// incoming message mentioning you
182+
else if (mention.isSelf && !isOutgoingMessage) {
183+
backgroundColor = context.getAccentColor()
184+
foregroundColor = mainTextColor
185+
}
186+
// outgoing message
187+
else if (isOutgoingMessage) {
188+
backgroundColor = null
189+
foregroundColor = mainTextColor
190+
}
191+
// incoming messages mentioning someone else
192+
else {
193+
backgroundColor = null
194+
foregroundColor = highlightedTextColor
195+
}
144196

145-
// apply the background, if any
146-
backgroundColor?.let { background ->
147-
result.setSpan(
148-
RoundedBackgroundSpan(
149-
context = context,
150-
textColor = mainTextColor,
151-
backgroundColor = background
152-
),
153-
mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
154-
)
155-
}
197+
val start = mention.start
198+
val end = mention.endExclusive
156199

157-
// apply the foreground, if any
158-
foregroundColor?.let {
159-
result.setSpan(
160-
ForegroundColorSpan(it),
161-
mention.first.lower,
162-
mention.first.upper,
163-
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
164-
)
165-
}
200+
backgroundColor?.let { background ->
201+
result.setSpan(
202+
RoundedBackgroundSpan(
203+
context = context,
204+
textColor = mainTextColor,
205+
backgroundColor = background
206+
),
207+
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
208+
)
209+
}
166210

167-
// apply bold on the mention
211+
foregroundColor?.let { fg ->
168212
result.setSpan(
169-
StyleSpan(Typeface.BOLD),
170-
mention.first.lower,
171-
mention.first.upper,
172-
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
213+
ForegroundColorSpan(fg),
214+
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
173215
)
174216
}
217+
218+
result.setSpan(
219+
StyleSpan(Typeface.BOLD),
220+
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
221+
)
175222
}
223+
176224
return result
177225
}
178226
}

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,6 @@ import androidx.core.text.toSpannable
1515

1616
object TextUtilities {
1717

18-
fun getIntrinsicHeight(text: CharSequence, paint: TextPaint, width: Int): Int {
19-
val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
20-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
21-
.setLineSpacing(0.0f, 1.0f)
22-
.setIncludePad(false)
23-
val layout = builder.build()
24-
return layout.height
25-
}
26-
27-
fun getIntrinsicLayout(text: CharSequence, paint: TextPaint, width: Int): StaticLayout {
28-
val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
29-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
30-
.setLineSpacing(0.0f, 1.0f)
31-
.setIncludePad(false)
32-
return builder.build()
33-
}
34-
3518
fun TextView.getIntersectedModalSpans(event: MotionEvent): List<ModalURLSpan> {
3619
val xInt = event.rawX.toInt()
3720
val yInt = event.rawY.toInt()
@@ -59,17 +42,5 @@ object TextUtilities {
5942

6043
fun String.textSizeInBytes(): Int = this.toByteArray(Charsets.UTF_8).size
6144

62-
fun String.breakAt(vararg lengths: Int): String {
63-
var cursor = 0
64-
val out = StringBuilder()
65-
for (len in lengths) {
66-
val end = (cursor + len).coerceAtMost(length)
67-
out.append(substring(cursor, end))
68-
if (end < length) out.append('\n')
69-
cursor = end
70-
}
71-
if (cursor < length) out.append('\n').append(substring(cursor))
72-
return out.toString()
73-
}
7445

7546
}

0 commit comments

Comments
 (0)