Complete text formatting.

This commit is contained in:
Cody Henthorne
2023-05-17 13:44:14 -04:00
committed by Greyson Parrelli
parent 534c5c3c64
commit a64bffd83a
20 changed files with 211 additions and 271 deletions

View File

@@ -15,8 +15,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
@@ -107,7 +105,6 @@ public final class FeatureFlags {
private static final String TEXT_FORMATTING = "android.textFormatting";
private static final String ANY_ADDRESS_PORTS_KILL_SWITCH = "android.calling.fieldTrial.anyAddressPortsKillSwitch";
private static final String CALLS_TAB = "android.calls.tab.2";
private static final String TEXT_FORMATTING_SPOILER_SEND = "android.textFormatting.spoilerSend";
private static final String AD_HOC_CALLING = "android.calling.ad.hoc";
private static final String EDIT_MESSAGE_RECEIVE = "android.editMessage.receive";
private static final String EDIT_MESSAGE_SEND = "android.editMessage.send";
@@ -170,7 +167,6 @@ public final class FeatureFlags {
TEXT_FORMATTING,
ANY_ADDRESS_PORTS_KILL_SWITCH,
CALLS_TAB,
TEXT_FORMATTING_SPOILER_SEND,
EDIT_MESSAGE_RECEIVE,
EDIT_MESSAGE_SEND
);
@@ -238,7 +234,6 @@ public final class FeatureFlags {
PAYMENTS_REQUEST_ACTIVATE_FLOW,
CDS_HARD_LIMIT,
TEXT_FORMATTING,
TEXT_FORMATTING_SPOILER_SEND,
EDIT_MESSAGE_RECEIVE,
EDIT_MESSAGE_SEND
);
@@ -587,13 +582,6 @@ public final class FeatureFlags {
return getBoolean(TEXT_FORMATTING, false);
}
/**
* Whether or not we should show spoiler text formatting option.
*/
public static boolean textFormattingSpoilerSend() {
return getBoolean(TEXT_FORMATTING_SPOILER_SEND, false);
}
/**
* Enable/disable RingRTC field trial for "AnyAddressPortsKillSwitch"
*/

View File

@@ -15,6 +15,7 @@ import android.widget.TextView;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
import java.lang.ref.WeakReference;
@@ -53,8 +54,7 @@ public class LongClickMovementMethod extends LinkMovementMethod {
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
@@ -68,14 +68,31 @@ public class LongClickMovementMethod extends LinkMovementMethod {
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
LongClickCopySpan longClickCopySpan[] = buffer.getSpans(off, off, LongClickCopySpan.class);
SpoilerAnnotation.SpoilerClickableSpan[] spoilerClickableSpans = buffer.getSpans(off, off, SpoilerAnnotation.SpoilerClickableSpan.class);
if (spoilerClickableSpans.length != 0) {
boolean spoilerRevealed = false;
for (SpoilerAnnotation.SpoilerClickableSpan spoilerClickSpan : spoilerClickableSpans) {
if (!spoilerClickSpan.getSpoilerRevealed() && action == MotionEvent.ACTION_DOWN) {
return true;
}
if (!spoilerClickSpan.getSpoilerRevealed() && action == MotionEvent.ACTION_UP) {
spoilerClickSpan.onClick(widget);
spoilerRevealed = true;
}
}
if (spoilerRevealed) {
return true;
}
}
LongClickCopySpan[] longClickCopySpan = buffer.getSpans(off, off, LongClickCopySpan.class);
if (longClickCopySpan.length != 0) {
LongClickCopySpan aSingleSpan = longClickCopySpan[0];
if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan),
buffer.getSpanEnd(aSingleSpan));
aSingleSpan.setHighlighted(true,
ContextCompat.getColor(widget.getContext(), R.color.touch_highlight));
Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan), buffer.getSpanEnd(aSingleSpan));
aSingleSpan.setHighlighted(true, ContextCompat.getColor(widget.getContext(), R.color.touch_highlight));
} else {
Selection.removeSelection(buffer);
aSingleSpan.setHighlighted(false, Color.TRANSPARENT);
@@ -89,8 +106,7 @@ public class LongClickMovementMethod extends LinkMovementMethod {
}
} else if (action == MotionEvent.ACTION_CANCEL) {
// Remove Selections.
LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer),
Selection.getSelectionEnd(buffer), LongClickCopySpan.class);
LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer), Selection.getSelectionEnd(buffer), LongClickCopySpan.class);
for (LongClickCopySpan aSpan : spans) {
aSpan.setHighlighted(false, Color.TRANSPARENT);
}

View File

@@ -1,75 +0,0 @@
package org.thoughtcrime.securesms.util
import android.text.Annotation
import android.text.Spannable
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
/**
* Filters the results of [getSpans] to exclude spans covered by an unrevealed spoiler when drawing or
* processing clicks. Since [getSpans] can also be called when making copies of spannables, we do not filter
* the call unless we know we are drawing or getting click spannables.
*/
class SpoilerFilteringSpannable(private val spannable: Spannable, private val inOnDrawProvider: InOnDrawProvider) : Spannable by spannable {
override fun <T : Any> getSpans(start: Int, end: Int, type: Class<T>): Array<T> {
val spans: Array<T> = spannable.getSpans(start, end, type)
if (spans.isEmpty() || !(inOnDrawProvider.isInOnDraw() || type == LongClickCopySpan::class.java)) {
return spans
}
if (spannable.getSpans(0, spannable.length, Annotation::class.java).none { SpoilerAnnotation.isSpoilerAnnotation(it) }) {
return spans
}
val spansToExclude = HashSet<Any>()
val spoilers: Map<Annotation, SpoilerAnnotation.SpoilerClickableSpan?> = SpoilerAnnotation.getSpoilerAndClickAnnotations(spannable, start, end)
val allOtherTheSpans: Map<T, Pair<Int, Int>> = spans
.filterNot { SpoilerAnnotation.isSpoilerAnnotation(it) || it is SpoilerAnnotation.SpoilerClickableSpan }
.associateWith { (spannable.getSpanStart(it) to spannable.getSpanEnd(it)) }
spoilers.forEach { (spoiler, click) ->
if (click?.spoilerRevealed == true) {
spansToExclude += spoiler
spansToExclude += click
} else {
val spoilerStart = spannable.getSpanStart(spoiler)
val spoilerEnd = spannable.getSpanEnd(spoiler)
for ((span, position) in allOtherTheSpans) {
if (position.first in spoilerStart..spoilerEnd) {
spansToExclude += span
} else if (position.second in spoilerStart..spoilerEnd) {
spansToExclude += span
}
}
}
}
return spans.filter(spansToExclude)
}
/**
* Kotlin does not handle generic JVM arrays well so instead of using all the nice collection functions
* we do a move desired objects down and overwrite undesired objects and then copy the array to trim
* it to the correct length. For our use case, it's okay to modify the original array.
*/
private fun <T : Any> Array<T>.filter(set: Set<Any>): Array<T> {
var index = 0
for (i in this.indices) {
this[index] = this[i]
if (!set.contains(this[index])) {
index++
}
}
return copyOfRange(0, index)
}
override fun toString(): String = spannable.toString()
override fun hashCode(): Int = spannable.hashCode()
override fun equals(other: Any?): Boolean = spannable == other
fun interface InOnDrawProvider {
fun isInOnDraw(): Boolean
}
}