mirror of
https://codeberg.org/Bazsalanszky/Infinity-For-Lemmy.git
synced 2024-11-10 04:37:25 +01:00
Linkify community and user names
This commit makes user and community names clickable in post/comments. Closes #71?
This commit is contained in:
parent
60f07f7707
commit
9872e6e806
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user