Skip to content

Commit ea40ebe

Browse files
Merge pull request #1275 from session-foundation/feature/character-limit
Feature/character limit
2 parents 2feb8d1 + edbf858 commit ea40ebe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1141
-371
lines changed

app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.session.libsession.utilities.Device
1212
import org.session.libsession.utilities.TextSecurePreferences
1313
import org.session.libsession.utilities.Toaster
1414
import org.session.libsession.utilities.UsernameUtils
15+
import org.thoughtcrime.securesms.pro.ProStatusManager
1516

1617
class MessagingModuleConfiguration(
1718
val context: Context,
@@ -25,7 +26,8 @@ class MessagingModuleConfiguration(
2526
val clock: SnodeClock,
2627
val preferences: TextSecurePreferences,
2728
val deprecationManager: LegacyGroupDeprecationManager,
28-
val usernameUtils: UsernameUtils
29+
val usernameUtils: UsernameUtils,
30+
val proStatusManager: ProStatusManager
2931
) {
3032

3133
companion object {

app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,12 @@ fun MessageReceiver.handleVisibleMessage(
470470
} ?: run {
471471
// A user is mentioned if their public key is in the body of a message or one of their messages
472472
// was quoted
473-
val messageText = message.text
473+
474+
// Verify the incoming message length and truncate it if needed, before saving it to the db
475+
val proStatusManager = MessagingModuleConfiguration.shared.proStatusManager
476+
val maxChars = proStatusManager.getIncomingMessageMaxLength(message)
477+
val messageText = message.text?.take(maxChars) // truncate to max char limit for this message
478+
message.text = messageText
474479
message.hasMention = listOf(userPublicKey, context.userBlindedKey)
475480
.filterNotNull()
476481
.any { key ->

app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ object StringSubstitutionConstants {
4242
const val URL_KEY = "url"
4343
const val VALUE_KEY = "value"
4444
const val VERSION_KEY = "version"
45+
const val LIMIT_KEY = "limit"
4546
}

app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DA
3535
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
3636
import org.session.libsession.utilities.TextSecurePreferences.Companion.SELECTED_ACCENT_COLOR
3737
import org.session.libsession.utilities.TextSecurePreferences.Companion.SELECTED_STYLE
38+
import org.session.libsession.utilities.TextSecurePreferences.Companion.SET_FORCE_CURRENT_USER_PRO
39+
import org.session.libsession.utilities.TextSecurePreferences.Companion.SET_FORCE_INCOMING_MESSAGE_PRO
40+
import org.session.libsession.utilities.TextSecurePreferences.Companion.SET_FORCE_POST_PRO
3841
import org.session.libsession.utilities.TextSecurePreferences.Companion.SHOWN_CALL_NOTIFICATION
3942
import org.session.libsession.utilities.TextSecurePreferences.Companion.SHOWN_CALL_WARNING
4043
import org.session.libsession.utilities.TextSecurePreferences.Companion._events
@@ -169,6 +172,12 @@ interface TextSecurePreferences {
169172
fun setHasSeenLinkPreviewSuggestionDialog()
170173
fun hasHiddenMessageRequests(): Boolean
171174
fun setHasHiddenMessageRequests(hidden: Boolean)
175+
fun forceCurrentUserAsPro(): Boolean
176+
fun setForceCurrentUserAsPro(hidden: Boolean)
177+
fun forceIncomingMessagesAsPro(): Boolean
178+
fun setForceIncomingMessagesAsPro(hidden: Boolean)
179+
fun forcePostPro(): Boolean
180+
fun setForcePostPro(hidden: Boolean)
172181
fun hasHiddenNoteToSelf(): Boolean
173182
fun setHasHiddenNoteToSelf(hidden: Boolean)
174183
fun setShownCallWarning(): Boolean
@@ -286,6 +295,9 @@ interface TextSecurePreferences {
286295
const val LAST_OPEN_DATE = "pref_last_open_date"
287296
const val HAS_HIDDEN_MESSAGE_REQUESTS = "pref_message_requests_hidden"
288297
const val HAS_HIDDEN_NOTE_TO_SELF = "pref_note_to_self_hidden"
298+
const val SET_FORCE_CURRENT_USER_PRO = "pref_force_current_user_pro"
299+
const val SET_FORCE_INCOMING_MESSAGE_PRO = "pref_force_incoming_message_pro"
300+
const val SET_FORCE_POST_PRO = "pref_force_post_pro"
289301
const val CALL_NOTIFICATIONS_ENABLED = "pref_call_notifications_enabled"
290302
const val SHOWN_CALL_WARNING = "pref_shown_call_warning" // call warning is user-facing warning of enabling calls
291303
const val SHOWN_CALL_NOTIFICATION = "pref_shown_call_notification" // call notification is a prompt to check privacy settings
@@ -1592,6 +1604,33 @@ class AppTextSecurePreferences @Inject constructor(
15921604
_events.tryEmit(HAS_HIDDEN_NOTE_TO_SELF)
15931605
}
15941606

1607+
override fun forceCurrentUserAsPro(): Boolean {
1608+
return getBooleanPreference(SET_FORCE_CURRENT_USER_PRO, false)
1609+
}
1610+
1611+
override fun setForceCurrentUserAsPro(hidden: Boolean) {
1612+
setBooleanPreference(SET_FORCE_CURRENT_USER_PRO, hidden)
1613+
_events.tryEmit(SET_FORCE_CURRENT_USER_PRO)
1614+
}
1615+
1616+
override fun forceIncomingMessagesAsPro(): Boolean {
1617+
return getBooleanPreference(SET_FORCE_INCOMING_MESSAGE_PRO, false)
1618+
}
1619+
1620+
override fun setForceIncomingMessagesAsPro(hidden: Boolean) {
1621+
setBooleanPreference(SET_FORCE_INCOMING_MESSAGE_PRO, hidden)
1622+
_events.tryEmit(SET_FORCE_INCOMING_MESSAGE_PRO)
1623+
}
1624+
1625+
override fun forcePostPro(): Boolean {
1626+
return getBooleanPreference(SET_FORCE_POST_PRO, false)
1627+
}
1628+
1629+
override fun setForcePostPro(hidden: Boolean) {
1630+
setBooleanPreference(SET_FORCE_POST_PRO, hidden)
1631+
_events.tryEmit(SET_FORCE_POST_PRO)
1632+
}
1633+
15951634
override fun getFingerprintKeyGenerated(): Boolean {
15961635
return getBooleanPreference(TextSecurePreferences.FINGERPRINT_KEY_GENERATED, false)
15971636
}

app/src/main/java/org/session/libsession/utilities/ViewUtils.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package org.session.libsession.utilities
22

33
import android.content.Context
4+
import android.os.Build
5+
import android.text.Layout
6+
import android.text.StaticLayout
7+
import android.text.TextDirectionHeuristics
48
import android.util.TypedValue
59
import android.view.View
10+
import android.view.View.TEXT_ALIGNMENT_CENTER
11+
import android.view.View.TEXT_ALIGNMENT_TEXT_END
12+
import android.view.View.TEXT_ALIGNMENT_VIEW_END
613
import android.view.ViewGroup
14+
import android.widget.TextView
715
import androidx.annotation.AttrRes
816
import androidx.annotation.ColorInt
17+
import androidx.core.text.TextDirectionHeuristicsCompat
18+
import androidx.core.widget.TextViewCompat
919

1020
@ColorInt
1121
fun Context.getColorFromAttr(
@@ -20,3 +30,48 @@ fun Context.getColorFromAttr(
2030
inline fun <reified LP: ViewGroup.LayoutParams> View.modifyLayoutParams(function: LP.() -> Unit) {
2131
layoutParams = (layoutParams as LP).apply { function() }
2232
}
33+
34+
fun TextView.needsCollapsing(
35+
availableWidthPx: Int,
36+
maxLines: Int
37+
): Boolean {
38+
if (availableWidthPx <= 0 || text.isNullOrEmpty()) return false
39+
40+
// The exact text that will be drawn (all-caps, password dots …)
41+
val textForLayout = transformationMethod?.getTransformation(text, this) ?: text
42+
43+
// Build a StaticLayout that mirrors this TextView’s wrap rules
44+
val builder = StaticLayout.Builder
45+
.obtain(textForLayout, 0, textForLayout.length, paint, availableWidthPx)
46+
.setIncludePad(includeFontPadding)
47+
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
48+
.setBreakStrategy(breakStrategy) // API 23+
49+
.setHyphenationFrequency(hyphenationFrequency)
50+
.setMaxLines(Int.MAX_VALUE)
51+
52+
// Alignment (honours RTL if textAlignment is END/VIEW_END)
53+
builder.setAlignment(
54+
when (textAlignment) {
55+
TEXT_ALIGNMENT_CENTER -> Layout.Alignment.ALIGN_CENTER
56+
TEXT_ALIGNMENT_VIEW_END,
57+
TEXT_ALIGNMENT_TEXT_END -> Layout.Alignment.ALIGN_OPPOSITE
58+
else -> Layout.Alignment.ALIGN_NORMAL
59+
}
60+
)
61+
62+
// Direction heuristic
63+
val dir = when (textDirection) {
64+
View.TEXT_DIRECTION_FIRST_STRONG_RTL -> TextDirectionHeuristics.FIRSTSTRONG_RTL
65+
View.TEXT_DIRECTION_RTL -> TextDirectionHeuristics.RTL
66+
View.TEXT_DIRECTION_LTR -> TextDirectionHeuristics.LTR
67+
else -> TextDirectionHeuristics.FIRSTSTRONG_LTR
68+
}
69+
builder.setTextDirection(dir)
70+
71+
builder.setEllipsize(ellipsize)
72+
73+
builder.setJustificationMode(justificationMode)
74+
75+
val layout = builder.build()
76+
return layout.lineCount > maxLines
77+
}

app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.migration.DatabaseMigrationManager
9494
import org.thoughtcrime.securesms.notifications.BackgroundPollManager
9595
import org.thoughtcrime.securesms.notifications.NotificationChannels
9696
import org.thoughtcrime.securesms.notifications.PushRegistrationHandler
97+
import org.thoughtcrime.securesms.pro.ProStatusManager
9798
import org.thoughtcrime.securesms.providers.BlobUtils
9899
import org.thoughtcrime.securesms.service.ExpiringMessageManager
99100
import org.thoughtcrime.securesms.service.KeyCachingService
@@ -188,6 +189,7 @@ class ApplicationContext : Application(), DefaultLifecycleObserver,
188189
@Inject lateinit var cleanupInvitationHandler: Lazy<CleanupInvitationHandler>
189190
@Inject lateinit var usernameUtils: Lazy<UsernameUtils>
190191
@Inject lateinit var pollerManager: Lazy<PollerManager>
192+
@Inject lateinit var proStatusManager: Lazy<ProStatusManager>
191193

192194
@Inject
193195
lateinit var backgroundPollManager: Lazy<BackgroundPollManager> // Exists here only to start upon app starts
@@ -285,7 +287,8 @@ class ApplicationContext : Application(), DefaultLifecycleObserver,
285287
clock = snodeClock.get(),
286288
preferences = textSecurePreferences.get(),
287289
deprecationManager = legacyGroupDeprecationManager.get(),
288-
usernameUtils = usernameUtils.get()
290+
usernameUtils = usernameUtils.get(),
291+
proStatusManager = proStatusManager.get()
289292
)
290293

291294
startKovenant()

app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@
2020
public class EmojiTextView extends AppCompatTextView {
2121
private final boolean scaleEmojis;
2222

23-
private static final char ELLIPSIS = '…';
24-
2523
private CharSequence previousText;
2624
private BufferType previousBufferType = BufferType.NORMAL;
2725
private float originalFontSize;
2826
private boolean sizeChangeInProgress;
29-
private int maxLength;
3027
private CharSequence overflowText;
3128
private CharSequence previousOverflowText;
3229

@@ -42,7 +39,6 @@ public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
4239
super(context, attrs, defStyleAttr);
4340

4441
scaleEmojis = true;
45-
maxLength = 1000;
4642
originalFontSize = getResources().getDimension(R.dimen.medium_font_size);
4743
}
4844

@@ -82,78 +78,12 @@ public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
8278

8379
if (candidates == null || candidates.size() == 0) {
8480
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
85-
86-
if (getEllipsize() == TextUtils.TruncateAt.END && maxLength > 0) {
87-
ellipsizeAnyTextForMaxLength();
88-
}
8981
} else {
9082
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, false);
9183
super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
92-
93-
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
94-
// We ellipsize them ourselves by manually truncating the appropriate section.
95-
if (getEllipsize() == TextUtils.TruncateAt.END) {
96-
if (maxLength > 0) {
97-
ellipsizeAnyTextForMaxLength();
98-
} else {
99-
ellipsizeEmojiTextForMaxLines();
100-
}
101-
}
102-
}
103-
}
104-
105-
public void setOverflowText(@Nullable CharSequence overflowText) {
106-
this.overflowText = overflowText;
107-
setText(previousText, BufferType.SPANNABLE);
108-
}
109-
110-
private void ellipsizeAnyTextForMaxLength() {
111-
if (maxLength > 0 && getText().length() > maxLength + 1) {
112-
SpannableStringBuilder newContent = new SpannableStringBuilder();
113-
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
114-
115-
EmojiParser.CandidateList newCandidates = EmojiProvider.getCandidates(newContent);
116-
117-
if (newCandidates == null || newCandidates.size() == 0) {
118-
super.setText(newContent, BufferType.NORMAL);
119-
} else {
120-
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, false);
121-
super.setText(emojified, BufferType.SPANNABLE);
122-
}
12384
}
12485
}
12586

126-
private void ellipsizeEmojiTextForMaxLines() {
127-
post(() -> {
128-
if (getLayout() == null) {
129-
ellipsizeEmojiTextForMaxLines();
130-
return;
131-
}
132-
133-
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
134-
if (maxLines <= 0 && maxLength < 0) {
135-
return;
136-
}
137-
138-
int lineCount = getLineCount();
139-
if (lineCount > maxLines) {
140-
int overflowStart = getLayout().getLineStart(maxLines - 1);
141-
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
142-
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
143-
144-
SpannableStringBuilder newContent = new SpannableStringBuilder();
145-
newContent.append(getText().subSequence(0, overflowStart))
146-
.append(ellipsized.subSequence(0, ellipsized.length()))
147-
.append(Optional.fromNullable(overflowText).or(""));
148-
149-
EmojiParser.CandidateList newCandidates = EmojiProvider.getCandidates(newContent);
150-
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, false);
151-
152-
super.setText(emojified, BufferType.SPANNABLE);
153-
}
154-
});
155-
}
156-
15787
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
15888
CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText);
15989
CharSequence finalText = (text == null || text.length() == 0 ? "" : text);

0 commit comments

Comments
 (0)