From 27250ed42d45c58cbcc9593c4d580083c88cfe21 Mon Sep 17 00:00:00 2001 From: Xisheng-Zhao Date: Sun, 17 May 2026 18:48:20 +0800 Subject: [PATCH] fix issue mention links in markdown --- .../ai/multica/app/ComposePilotActivity.kt | 11 ++++- .../java/ai/multica/app/MarkdownRenderer.java | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ai/multica/app/ComposePilotActivity.kt b/app/src/main/java/ai/multica/app/ComposePilotActivity.kt index 2eb6936..8229cce 100644 --- a/app/src/main/java/ai/multica/app/ComposePilotActivity.kt +++ b/app/src/main/java/ai/multica/app/ComposePilotActivity.kt @@ -8797,6 +8797,7 @@ private fun IssueCommentCard( members: List = emptyList(), agents: List = emptyList(), currentUser: Models.User? = null, + onIssueIdClick: ((String) -> Unit)? = null, onIssueReferenceClick: ((String) -> Unit)? = null, expandedActions: Boolean, contentExpanded: Boolean, @@ -8890,6 +8891,7 @@ private fun IssueCommentCard( members = members, agents = agents, currentUser = currentUser, + onIssueIdClick = onIssueIdClick, onIssueReferenceClick = onIssueReferenceClick, ) if (collapseContent) { @@ -9891,6 +9893,7 @@ private fun PilotIssueDetail( members = data.members, agents = data.agents, currentUser = currentUser, + onIssueIdClick = { onOpenIssue(it) }, onIssueReferenceClick = { openIssueReference(it) }, ) } @@ -10037,6 +10040,7 @@ private fun PilotIssueDetail( members = data.members, agents = data.agents, currentUser = currentUser, + onIssueIdClick = { onOpenIssue(it) }, onIssueReferenceClick = { openIssueReference(it) }, expandedActions = expandedCommentActionsId == comment.id, contentExpanded = comment.id in expandedCommentContentIds, @@ -11448,12 +11452,13 @@ private fun MarkdownBlock( members: List = emptyList(), agents: List = emptyList(), currentUser: Models.User? = null, + onIssueIdClick: ((String) -> Unit)? = null, onIssueReferenceClick: ((String) -> Unit)? = null, ) { val textColor = MulticaColors.Text.toArgb() val mutedColor = MulticaColors.Muted.toArgb() val borderColor = MulticaColors.Border.toArgb() - val linkHandler = remember(members, agents, currentUser, onIssueReferenceClick) { + val linkHandler = remember(members, agents, currentUser, onIssueIdClick, onIssueReferenceClick) { object : MarkdownRenderer.LinkHandler { override fun resolveMentionLabel( mentionType: String, @@ -11470,6 +11475,10 @@ private fun MarkdownBlock( ) } + override fun openIssueId(issueId: String) { + if (issueId.isNotBlank()) onIssueIdClick?.invoke(issueId) + } + override fun openIssueIdentifier(identifier: String) { if (identifier.isNotBlank()) onIssueReferenceClick?.invoke(identifier) } diff --git a/app/src/main/java/ai/multica/app/MarkdownRenderer.java b/app/src/main/java/ai/multica/app/MarkdownRenderer.java index aa79897..7cfebed 100644 --- a/app/src/main/java/ai/multica/app/MarkdownRenderer.java +++ b/app/src/main/java/ai/multica/app/MarkdownRenderer.java @@ -3,6 +3,7 @@ package ai.multica.app; import android.app.AlertDialog; import android.content.Context; import android.graphics.Typeface; +import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -12,6 +13,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import android.view.MotionEvent; import android.view.ViewGroup; import android.view.View; import android.widget.HorizontalScrollView; @@ -30,6 +32,7 @@ final class MarkdownRenderer { interface LinkHandler { String resolveMentionLabel(String mentionType, String mentionId, String fallbackLabel); + void openIssueId(String issueId); void openIssueIdentifier(String identifier); } @@ -124,6 +127,7 @@ final class MarkdownRenderer { TextView bullet = text(context, "• " + trimmed.substring(2), 15, textColor); bullet.setText(applyInline(bullet.getText().toString(), linkHandler)); bullet.setMovementMethod(LinkMovementMethod.getInstance()); + enableClickableSpans(bullet); bullet.setPadding(dp(context, 8), dp(context, 2), 0, dp(context, 2)); parent.addView(bullet); } else { @@ -148,6 +152,7 @@ final class MarkdownRenderer { TextView p = text(context, String.join("\n", paragraph), 15, textColor); p.setText(applyInline(p.getText().toString(), linkHandler)); p.setMovementMethod(LinkMovementMethod.getInstance()); + enableClickableSpans(p); p.setPadding(0, dp(context, 2), 0, dp(context, 6)); parent.addView(p); paragraph.clear(); @@ -194,6 +199,7 @@ final class MarkdownRenderer { TextView tv = text(context, "", 14, r == 0 ? textColor : mutedColor); tv.setText(applyInline(cellText, linkHandler)); tv.setMovementMethod(LinkMovementMethod.getInstance()); + enableClickableSpans(tv); tv.setTypeface(r == 0 ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); tv.setPadding(dp(context, 10), dp(context, 8), dp(context, 10), dp(context, 8)); tv.setBackgroundColor(r == 0 ? palette.tableHeaderBackground : palette.tableCellBackground); @@ -325,6 +331,12 @@ final class MarkdownRenderer { private static void appendMarkdownLink(SpannableStringBuilder out, ParsedMarkdownLink link, LinkHandler linkHandler) { MentionUri mention = MentionUri.parse(link.url); if (mention != null) { + if ("issue".equals(mention.type)) { + appendLinkedText(out, link.label, view -> { + if (linkHandler != null) linkHandler.openIssueId(mention.id); + }); + return; + } String label = linkHandler == null ? IssueCommentRichText.mentionDisplayLabel(mention.type, mention.id, link.label, new ArrayList<>(), new ArrayList<>(), null) : linkHandler.resolveMentionLabel(mention.type, mention.id, link.label); @@ -364,6 +376,35 @@ final class MarkdownRenderer { } } + private static void enableClickableSpans(TextView textView) { + CharSequence text = textView.getText(); + if (!(text instanceof Spanned)) return; + Spanned spanned = (Spanned) text; + if (spanned.getSpans(0, spanned.length(), ClickableSpan.class).length == 0) return; + textView.setOnTouchListener((view, event) -> { + if (event.getAction() != MotionEvent.ACTION_UP && event.getAction() != MotionEvent.ACTION_DOWN) return false; + TextView touchedTextView = (TextView) view; + ClickableSpan span = clickableSpanAt(touchedTextView, event); + if (span == null) return false; + if (event.getAction() == MotionEvent.ACTION_UP) span.onClick(touchedTextView); + return true; + }); + } + + private static ClickableSpan clickableSpanAt(TextView textView, MotionEvent event) { + CharSequence text = textView.getText(); + Layout layout = textView.getLayout(); + if (!(text instanceof Spanned) || layout == null) return null; + int x = (int) event.getX() - textView.getTotalPaddingLeft() + textView.getScrollX(); + int y = (int) event.getY() - textView.getTotalPaddingTop() + textView.getScrollY(); + if (x < 0 || y < 0 || y > layout.getHeight()) return null; + int line = layout.getLineForVertical(y); + if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) return null; + int offset = layout.getOffsetForHorizontal(line, x); + ClickableSpan[] spans = ((Spanned) text).getSpans(offset, offset, ClickableSpan.class); + return spans.length == 0 ? null : spans[0]; + } + private static ParsedMarkdownLink parseMarkdownLink(String value, int start) { int labelEnd = value.indexOf("](", start + 1); if (labelEnd <= start + 1) return null;