Linkify community and user names

This commit makes user and community names clickable in post/comments. Closes #71?
This commit is contained in:
Bazsalanszky 2023-08-07 13:49:18 +02:00
parent 60f07f7707
commit 9872e6e806
7 changed files with 103 additions and 4 deletions

View File

@ -1,5 +1,6 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android'
} }
android { android {
@ -34,6 +35,11 @@ android {
versionNameSuffix ' (DEBUG)' versionNameSuffix ' (DEBUG)'
} }
} }
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11

View File

@ -690,9 +690,8 @@ public class ViewSubredditDetailActivity extends BaseActivity implements SortTyp
qualifiedName = LemmyUtils.actorID2FullName(communityData.getActorId()); qualifiedName = LemmyUtils.actorID2FullName(communityData.getActorId());
if (communityName == null) { if (communityName == null) {
communityName = communityData.getTitle(); communityName = communityData.getTitle();
setupVisibleElements();
} }
setupVisibleElements();
communityId = communityData.getId(); communityId = communityData.getId();
setupSubscribeChip(); setupSubscribeChip();

View File

@ -40,6 +40,7 @@ import eu.toldi.infinityforlemmy.markdown.MarkdownUtils;
import eu.toldi.infinityforlemmy.subreddit.FetchSubredditData; import eu.toldi.infinityforlemmy.subreddit.FetchSubredditData;
import eu.toldi.infinityforlemmy.subreddit.SubredditData; import eu.toldi.infinityforlemmy.subreddit.SubredditData;
import eu.toldi.infinityforlemmy.subreddit.SubredditViewModel; import eu.toldi.infinityforlemmy.subreddit.SubredditViewModel;
import eu.toldi.infinityforlemmy.utils.LemmyUtils;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
@ -97,7 +98,7 @@ public class SidebarFragment extends Fragment {
mAccessToken = getArguments().getString(EXTRA_ACCESS_TOKEN); mAccessToken = getArguments().getString(EXTRA_ACCESS_TOKEN);
subredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME); subredditName = getArguments().getString(EXTRA_SUBREDDIT_NAME);
communityQualifiedName = getArguments().getString(EXTRA_COMMUNITY_QUALIFIED_NAME); communityQualifiedName = getArguments().getString(EXTRA_COMMUNITY_QUALIFIED_NAME);
if (subredditName == null) { if (communityQualifiedName == null) {
Toast.makeText(activity, R.string.error_getting_community_name, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.error_getting_community_name, Toast.LENGTH_SHORT).show();
return rootView; return rootView;
} }
@ -166,7 +167,7 @@ public class SidebarFragment extends Fragment {
}); });
mSubredditViewModel = new ViewModelProvider(activity, mSubredditViewModel = new ViewModelProvider(activity,
new SubredditViewModel.Factory(activity.getApplication(), mRedditDataRoomDatabase, communityQualifiedName)) new SubredditViewModel.Factory(activity.getApplication(), mRedditDataRoomDatabase, LemmyUtils.qualifiedCommunityName2ActorId(communityQualifiedName)))
.get(SubredditViewModel.class); .get(SubredditViewModel.class);
mSubredditViewModel.getSubredditLiveData().observe(getViewLifecycleOwner(), subredditData -> { mSubredditViewModel.getSubredditLiveData().observe(getViewLifecycleOwner(), subredditData -> {
if (subredditData != null) { if (subredditData != null) {

View File

@ -52,6 +52,7 @@ public class MarkdownUtils {
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(context)) .usePlugin(TableEntryPlugin.create(context))
.usePlugin(ClickableGlideImagesPlugin.create(context)) .usePlugin(ClickableGlideImagesPlugin.create(context))
.usePlugin(new MarkwonLemmyLinkPlugin())
.build(); .build();
} }
@ -71,6 +72,7 @@ public class MarkdownUtils {
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(context)) .usePlugin(TableEntryPlugin.create(context))
.usePlugin(GlideImagesPlugin.create(context)) .usePlugin(GlideImagesPlugin.create(context))
.usePlugin(new MarkwonLemmyLinkPlugin())
.build(); .build();
} }

View File

@ -0,0 +1,88 @@
package eu.toldi.infinityforlemmy.markdown
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.URLSpan
import android.text.util.Linkify
import eu.toldi.infinityforlemmy.utils.LemmyUtils
import io.noties.markwon.*
import io.noties.markwon.core.CorePlugin
import io.noties.markwon.core.CoreProps
import org.commonmark.node.Link
import java.util.regex.Pattern
// Source : https://github.com/dessalines/jerboa/blob/main/app/src/main/java/com/jerboa/util/markwon/MarkwonLemmyLinkPlugin.kt
class MarkwonLemmyLinkPlugin : AbstractMarkwonPlugin() {
companion object {
/**
* pattern that matches all valid communities; intended to be loose
*/
const val communityPatternFragment: String = """[a-zA-Z0-9_]{3,}"""
/**
* pattern to match all valid instances
*/
const val instancePatternFragment: String =
"""([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}"""
/**
* pattern to match all valid usernames
*/
const val userPatternFragment: String = """[a-zA-Z0-9_]{3,}"""
/**
* Pattern to match lemmy's unique community pattern, e.g. !commmunity[@instance]
*/
val lemmyCommunityPattern: Pattern =
Pattern.compile("(?<!\\S)!($communityPatternFragment)(?:@($instancePatternFragment))?\\b")
/**
* Pattern to match lemmy's unique user pattern, e.g. @user[@instance]
*/
val lemmyUserPattern: Pattern =
Pattern.compile("(?<!\\S)@($userPatternFragment)(?:@($instancePatternFragment))?\\b")
}
override fun configure(registry: MarkwonPlugin.Registry) {
registry.require(CorePlugin::class.java) { it.addOnTextAddedListener(LemmyTextAddedListener()) }
}
private class LemmyTextAddedListener : CorePlugin.OnTextAddedListener {
override fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int) {
// we will be using the link that is used by markdown (instead of directly applying URLSpan)
val spanFactory = visitor.configuration().spansFactory().get(
Link::class.java,
) ?: return
// don't re-use builder (thread safety achieved for
// render calls from different threads and ... better performance)
val builder = SpannableStringBuilder(text)
if (addLinks(builder)) {
// target URL span specifically
val spans = builder.getSpans(0, builder.length, URLSpan::class.java)
if (!spans.isNullOrEmpty()) {
val renderProps = visitor.renderProps()
val spannableBuilder = visitor.builder()
for (span in spans) {
CoreProps.LINK_DESTINATION[renderProps] = if (span.url.startsWith("!")) { LemmyUtils.qualifiedCommunityName2ActorId(span.url.drop(1)) } else { LemmyUtils.qualifiedUserName2ActorId(span.url.drop(1)) }
SpannableBuilder.setSpans(
spannableBuilder,
spanFactory.getSpans(visitor.configuration(), renderProps),
start + builder.getSpanStart(span),
start + builder.getSpanEnd(span),
)
}
}
}
}
fun addLinks(text: Spannable): Boolean {
val communityLinkAdded = Linkify.addLinks(text, lemmyCommunityPattern, null)
val userLinkAdded = Linkify.addLinks(text, lemmyUserPattern, null)
return communityLinkAdded || userLinkAdded
}
}
}

View File

@ -1352,4 +1352,6 @@
<string name="instance_cannot_be_empty">The instance field cannot be left empty.</string> <string name="instance_cannot_be_empty">The instance field cannot be left empty.</string>
<string name="username_cannot_be_empty">The username field cannot be left empty.</string> <string name="username_cannot_be_empty">The username field cannot be left empty.</string>
<string name="password_cannot_be_empty">The password field cannot be left empty.</string> <string name="password_cannot_be_empty">The password field cannot be left empty.</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>

View File

@ -9,6 +9,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.2' classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files