Skip to content

Commit

Permalink
Merge pull request #248 from stefan-niedermann/rewrite-mentions-and-s…
Browse files Browse the repository at this point in the history
…earch-highlight

feat(markdown): Rewrite MentionsPlugin and SearchHighlightPlugin
  • Loading branch information
stefan-niedermann authored Feb 2, 2024
2 parents f43018e + da412cc commit ea5decb
Show file tree
Hide file tree
Showing 50 changed files with 1,403 additions and 170 deletions.
2 changes: 0 additions & 2 deletions exception/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ afterEvaluate {
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'

api "com.github.nextcloud:Android-SingleSignOn:$sso_version"
Expand Down
7 changes: 6 additions & 1 deletion markdown/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ android {
namespace 'it.niedermann.android.markdown'

defaultConfig {
minSdk 22
minSdk 24
targetSdk 34

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -47,7 +47,10 @@ afterEvaluate {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'

implementation project(':ocs')

api "com.github.nextcloud:Android-SingleSignOn:$sso_version"
implementation 'com.github.nextcloud:android-common:0.14.0'
implementation 'com.github.stefan-niedermann:android-commons:1.0.0'

implementation 'androidx.appcompat:appcompat:1.6.1'
Expand Down Expand Up @@ -76,6 +79,8 @@ dependencies {
annotationProcessor 'io.noties:prism4j-bundler:2.0.0'
implementation 'org.jetbrains:annotations:24.1.0'

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
testImplementation 'junit:junit:4.13.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;

import com.nextcloud.android.sso.model.SingleSignOnAccount;

import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -21,12 +23,13 @@
public interface MarkdownEditor {

String TAG = MarkdownEditor.class.getSimpleName();
String LOG_WARNING_UNSUPPORTED_FEATURE = "This feature is not supported by the currently used implementation.";

/**
* @param prefix used to render relative image URLs
*/
default void setMarkdownImageUrlPrefix(@NonNull String prefix) {
Log.w(TAG, "This feature is not supported by the currently used implementation.");
Log.w(TAG, LOG_WARNING_UNSUPPORTED_FEATURE);
}

/**
Expand All @@ -40,10 +43,9 @@ default void setMarkdownImageUrlPrefix(@NonNull String prefix) {
void setMarkdownString(CharSequence text, @Nullable Runnable afterRender);

/**
* Will replace all `@mention`s of Nextcloud users with the avatar and given display name.
*
* @param mentions {@link Map} of mentions, where the key is the user id and the value is the display name
* @deprecated use {@link #setMarkdownString(CharSequence)}, mentions will get highlighted implicitly
*/
@Deprecated(forRemoval = true)
default void setMarkdownStringAndHighlightMentions(CharSequence text, @NonNull Map<String, String> mentions) {
setMarkdownString(text);
}
Expand All @@ -64,13 +66,20 @@ default void setMarkdownStringAndHighlightMentions(CharSequence text, @NonNull M

/**
* @param color which will be used for highlighting. See {@link #setSearchText(CharSequence)}
* @deprecated Use {@link #setCurrentSingleSignOnAccount(SingleSignOnAccount, int)}
*/
default void setSearchColor(@ColorInt int color) {
Log.w(TAG, "This feature is not supported by the currently used implementation.");
}
@Deprecated(forRemoval = true)
void setSearchColor(@ColorInt int color);

/**
* @param ssoAccount the account who wants to make the requests, e. g. to fetch avatars.
* If <code>null</code> is passed, some features like avatar loading might not work as expected.
* @param color the color that is the base of the current theming, e. g. the instance color
*/
void setCurrentSingleSignOnAccount(@Nullable SingleSignOnAccount ssoAccount, @ColorInt int color);

/**
* See {@link #setSearchText(CharSequence, Integer)}
* @see #setSearchText(CharSequence, Integer)
*/
default void setSearchText(@Nullable CharSequence searchText) {
setSearchText(searchText, null);
Expand All @@ -82,16 +91,30 @@ default void setSearchText(@Nullable CharSequence searchText) {
* @param searchText the term to highlight
* @param current highlights the occurrence of the {@param searchText} at this position special
*/
default void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
Log.w(TAG, "This feature is not supported by the currently used implementation.");
}
void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current);

/**
* Intercepts each click on a clickable element like {@link URLSpan}s
*
* @param callback Will be called on a click. When the {@param callback} returns <code>true</code>, the click will not be propagated further.
*/
default void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
Log.w(TAG, "This feature is not supported by the currently used implementation.");
Log.w(TAG, LOG_WARNING_UNSUPPORTED_FEATURE);
}

int getSelectionStart();

int getSelectionEnd();

int getVerticalScrollbarPosition();

void setVerticalScrollbarPosition(int position);

default void setSelection(int index) {
setSelection(index, index);
}

default void setSelection(int start, int stop) {
Log.w(TAG, LOG_WARNING_UNSUPPORTED_FEATURE);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.niedermann.android.markdown;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Paint;
import android.os.Build;
Expand All @@ -21,13 +22,17 @@
import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;

import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils;

import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -180,24 +185,22 @@ public static String replaceCheckboxesWithEmojis(@NonNull String content) {

@NonNull
private static Optional<String> getCheckboxEmoji(boolean checked) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final String[] emojis;
// Seriously what the fuck, Samsung?
// https://emojipedia.org/ballot-box-with-x/
if (Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase(Locale.getDefault()).contains("samsung")) {
emojis = checked
? new String[]{"✅", "☑️", "✔️"}
: new String[]{"❌", "\uD83D\uDD32️", "☐️"};
} else {
emojis = checked
? new String[]{"☒", "✅", "☑️", "✔️"}
: new String[]{"☐", "❌", "\uD83D\uDD32️", "☐️"};
}
final var paint = new Paint();
for (String emoji : emojis) {
if (paint.hasGlyph(emoji)) {
return Optional.of(emoji);
}
final String[] emojis;
// Seriously what the fuck, Samsung?
// https://emojipedia.org/ballot-box-with-x#designs
if (Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase(Locale.getDefault()).contains("samsung")) {
emojis = checked
? new String[]{"✅", "☑️", "✔️"}
: new String[]{"❌", "\uD83D\uDD32️", "☐️"};
} else {
emojis = checked
? new String[]{"☒", "✅", "☑️", "✔️"}
: new String[]{"☐", "❌", "\uD83D\uDD32️", "☐️"};
}
final var paint = new Paint();
for (String emoji : emojis) {
if (paint.hasGlyph(emoji)) {
return Optional.of(emoji);
}
}
return Optional.empty();
Expand Down Expand Up @@ -238,10 +241,7 @@ private static String runForEachCheckbox(@NonNull String markdownString, @NonNul
}

public static boolean isCheckboxLine(String line) {
if (lineStartsWithCheckbox(line) && line.trim().length() > EListType.DASH.checkboxChecked.length()) {
return true;
}
return false;
return lineStartsWithCheckbox(line) && line.trim().length() > EListType.DASH.checkboxChecked.length();
}

public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) {
Expand All @@ -262,12 +262,8 @@ public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) {

public static Optional<String> getListItemIfIsEmpty(@NonNull String line) {
final String trimmedLine = line.trim();
// TODO use Java 11 String::repeat
final var builder = new StringBuilder();
final int indention = line.indexOf(trimmedLine);
for (int i = 0; i < indention; i++) {
builder.append(" ");
}
final var builder = new StringBuilder(" ".repeat(indention));
for (final var listType : EListType.values()) {
if (trimmedLine.equals(listType.checkboxUnchecked)) {
return Optional.of(builder.append(listType.checkboxUncheckedWithTrailingSpace).toString());
Expand Down Expand Up @@ -549,7 +545,25 @@ public static boolean selectionIsInLink(@NonNull CharSequence text, int start, i
return false;
}

public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSequence searchText, @Nullable Integer current, @ColorInt int mainColor, @ColorInt int highlightColor, boolean darkTheme) {
/**
* @deprecated use {@link AndroidViewThemeUtils#highlightText(TextView, String, String)}
*/
@Deprecated(forRemoval = true)
public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSequence searchText, @Nullable Integer current, @ColorInt int color, @ColorInt int ignoredColor, boolean ignoredDarkTheme) {
try {
@SuppressLint("PrivateApi") final var context = (Context) Class.forName("android.app.ActivityThread")
.getMethod("currentApplication")
.invoke(null, (Object[]) null);

searchAndColor(Objects.requireNonNull(context), editable, searchText, color, current);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}

public static void searchAndColor(@NonNull Context context, @NonNull Spannable editable, @Nullable CharSequence searchText, @ColorInt int color, @Nullable Integer current) {
final var util = ThemeUtils.Companion.of(color);
if (searchText != null) {
final var m = Pattern
.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
Expand All @@ -559,7 +573,10 @@ public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSeq
while (m.find()) {
int start = m.start();
int end = m.end();
editable.setSpan(new SearchSpan(mainColor, highlightColor, (current != null && i == current), darkTheme), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
final var span = current == null || i == current
? new SearchSpan(util.getPrimary(context), util.getOnPrimary(context))
: new SearchSpan(util.getSecondary(context), util.getOnSecondary(context));
editable.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
i++;
}
}
Expand Down Expand Up @@ -610,10 +627,6 @@ public static String removeMarkdown(@Nullable String s) {
return "";
}
final String html = RENDERER.render(PARSER.parse(replaceCheckboxesWithEmojis(s)));
final Spanned spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
return spanned.toString().trim().replaceAll("\n\n", "\n");
}
return spanned.toString().trim();
return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT).toString().trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import java.util.Map;


@RestrictTo(RestrictTo.Scope.LIBRARY)
@Deprecated(forRemoval = true)
public class MentionUtil {

private MentionUtil() {
Expand Down Expand Up @@ -49,7 +51,7 @@ public static void setupMentions(@NonNull SingleSignOnAccount account, @NonNull
final int spanEnd = messageBuilder.getSpanEnd(span);
Glide.with(context)
.asBitmap()
.placeholder(R.drawable.ic_person_grey600_24dp)
.placeholder(R.drawable.ic_baseline_account_circle_24dp)
.load(account.url + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd) + "/" + span.getDrawable().getIntrinsicHeight())
.apply(RequestOptions.circleCropTransform())
.into(new CustomTarget<Bitmap>() {
Expand All @@ -75,7 +77,7 @@ private static SpannableStringBuilder replaceAtMentionsWithImagePlaceholderAndDi
final String mentionDisplayName = " " + mentions.get(userId);
int index = messageBuilder.toString().lastIndexOf(mentionId);
while (index >= 0) {
messageBuilder.setSpan(new MentionSpan(context, R.drawable.ic_person_grey600_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
messageBuilder.setSpan(new MentionSpan(context, R.drawable.ic_baseline_account_circle_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
messageBuilder.insert(index + mentionId.length(), mentionDisplayName);
index = messageBuilder.toString().substring(0, index).lastIndexOf(mentionId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package it.niedermann.android.markdown

import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import com.nextcloud.android.common.ui.theme.MaterialSchemes
import com.nextcloud.android.common.ui.theme.MaterialSchemes.Companion.fromColor
import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase

class ThemeUtils(schemes: MaterialSchemes) : ViewThemeUtilsBase(schemes) {

fun getPrimary(context: Context): Int {
return withScheme(context) { scheme -> scheme.primary }
}

fun getOnPrimary(context: Context): Int {
return withScheme(context) { scheme -> scheme.onPrimary }
}

fun getSecondary(context: Context): Int {
return withScheme(context) { scheme -> scheme.secondary }
}

fun getOnSecondary(context: Context): Int {
return withScheme(context) { scheme -> scheme.onSecondary }
}

fun tintDrawable(
context: Context,
drawable: Drawable
): Drawable {
return withScheme(context) { scheme ->
drawable.setTintList(ColorStateList.valueOf(scheme.onSurfaceVariant))
drawable
}
}

companion object {
private val cache = mutableMapOf<Int, ThemeUtils>()
fun of(color: Int): ThemeUtils {
return cache.computeIfAbsent(color) {
ThemeUtils(fromColor(color))
}
}
}

}
Loading

0 comments on commit ea5decb

Please sign in to comment.