Make links in comments clickable (#8894)

*  Make links in comments clickable

Detect URLs in comment text and render them as clickable links that
open in a new tab. Extends the existing mention parsing to also split
text elements by URL patterns, handling trailing punctuation and
mixed mention+URL content.

Closes #1602

* 📚 Add changelog entry for clickable links in comments

* 🐛 Fix URL elements dropped in comment input initialization

* 🐛 Keep empty text elements in parse-urls to preserve cursor anchors

The remove filter in parse-urls was stripping empty text elements
produced by str/split at URL boundaries. These elements are needed
as cursor anchor spans in the contenteditable input, without them
ESC keydown and visual layout broke.

Signed-off-by: eureka928 <meobius123@gmail.com>
This commit is contained in:
Dream 2026-04-13 03:55:53 -04:00 committed by GitHub
parent e7e5a19db7
commit 8dccb2a427
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 40 additions and 12 deletions

View File

@ -27,6 +27,7 @@
- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790)
- Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
- Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
### :bug: Bugs fixed

View File

@ -45,20 +45,34 @@
(def mentions-context (mf/create-context nil))
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
(def r-url-split #"https?://[^\s\)\]]+[^\s\)\]\.,;:!?]")
(def zero-width-space \u200B)
(defn- parse-comment
"Parse a comment into its elements (texts and mentions)"
[comment]
(d/interleave-all
(->> (str/split comment r-mentions-split)
(map #(hash-map :type :text :content %)))
(defn- parse-urls
"Split a text element into text and url sub-elements"
[element]
(if (= (:type element) :text)
(let [text (:content element)
parts (str/split text r-url-split)
urls (re-seq r-url-split text)]
(d/interleave-all
(map #(hash-map :type :text :content %) parts)
(map #(hash-map :type :url :content %) urls)))
[element]))
(->> (re-seq r-mentions comment)
(map (fn [[_ user id]]
{:type :mention
:content user
:data {:id id}})))))
(defn- parse-comment
"Parse a comment into its elements (texts, mentions and urls)"
[comment]
(->> (d/interleave-all
(->> (str/split comment r-mentions-split)
(map #(hash-map :type :text :content %)))
(->> (re-seq r-mentions comment)
(map (fn [[_ user id]]
{:type :mention
:content user
:data {:id id}}))))
(mapcat parse-urls)))
(defn- parse-nodes
"Parse the nodes to format a comment"
@ -146,7 +160,13 @@
[{:keys [content]}]
(let [comment-elements (mf/use-memo (mf/deps content) #(parse-comment content))]
(for [[idx {:keys [type content]}] (d/enumerate comment-elements)]
(case type
(if (= type :url)
[:a {:key idx
:href content
:target "_blank"
:rel "noopener noreferrer"
:class (stl/css :comment-link)}
content]
[:span
{:key idx
:class (stl/css-case
@ -177,6 +197,7 @@
(doseq [{:keys [type content data]} (parse-comment value)]
(case type
:text (dom/append-child! node (create-text-node content))
:url (dom/append-child! node (create-text-node content))
:mention (dom/append-child! node (create-mention-node (:id data) content))
nil)))))

View File

@ -418,6 +418,12 @@
color: var(--color-accent-primary);
}
.comment-link {
color: var(--color-accent-primary);
text-decoration: underline;
cursor: pointer;
}
.comments-mentions-empty {
font-size: deprecated.$fs-12;
color: var(--color-foreground-secondary);