From cc014571cc3403046663fd82f55acf0c8ba7d72b Mon Sep 17 00:00:00 2001 From: Paul Troeger <41645798+paultroeger@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:53:39 +0200 Subject: [PATCH] feat: add openinapp plugin to open chat links in shared neko instance (#682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add openinapp plugin to open chat links in shared neko instance * lint. * feat(openinapp): implement open link handler and route for opening links. * feat(openinapp): switched to http backend and removed example in docker-compose.yaml * lint. --------- Co-authored-by: Miroslav Šedivý --- client/src/components/chat.vue | 32 ++++++- client/src/components/markdown.ts | 26 +++++- client/src/components/settings.vue | 15 ++++ client/src/neko/events.ts | 7 ++ client/src/neko/index.ts | 7 ++ client/src/store/index.ts | 3 +- client/src/store/openinapp.ts | 29 ++++++ client/src/store/settings.ts | 6 ++ server/internal/http/legacy/wstobackend.go | 9 ++ server/internal/http/legacy/wstoclient.go | 16 ++++ server/internal/plugins/manager.go | 2 + server/internal/plugins/openinapp/config.go | 30 +++++++ server/internal/plugins/openinapp/manager.go | 94 ++++++++++++++++++++ server/internal/plugins/openinapp/plugin.go | 34 +++++++ server/internal/plugins/openinapp/types.go | 16 ++++ webpage/docs/configuration/plugins.md | 16 ++++ 16 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 client/src/store/openinapp.ts create mode 100644 server/internal/plugins/openinapp/config.go create mode 100644 server/internal/plugins/openinapp/manager.go create mode 100644 server/internal/plugins/openinapp/plugin.go create mode 100644 server/internal/plugins/openinapp/types.go diff --git a/client/src/components/chat.vue b/client/src/components/chat.vue index c8e127de..28ae988e 100644 --- a/client/src/components/chat.vue +++ b/client/src/components/chat.vue @@ -18,7 +18,11 @@ {{ member(message.id).displayname }} {{ timestamp(message.created) }} - +
  • @@ -225,6 +229,11 @@ display: block; } } + + .open-in-app { + margin-left: 0.3em; + cursor: pointer; + } } } @@ -460,6 +469,27 @@ target.parentElement.classList.add('active') event.preventDefault() } + + if (target.classList.contains('open-in-app')) { + const href = target.dataset.href + if (href) { + this.$accessor.openinapp.sendOpenLink(href) + } + event.preventDefault() + } + + if ( + this.$accessor.settings.links_in_app && + this.$accessor.openinapp.enabled && + this.$accessor.remote.hosting && + target.tagName.toLowerCase() === 'a' + ) { + const href = target.getAttribute('href') + if (href) { + this.$accessor.openinapp.sendOpenLink(href) + event.preventDefault() + } + } } onKeyDown(event: KeyboardEvent) { diff --git a/client/src/components/markdown.ts b/client/src/components/markdown.ts index 38c6c357..f0b62c6e 100644 --- a/client/src/components/markdown.ts +++ b/client/src/components/markdown.ts @@ -136,12 +136,21 @@ const rules: MarkdownRules = { } }, html(node, output, state) { - return htmlTag( + const href = md.sanitizeUrl(node.target) as string + const link = htmlTag( 'a', output(node.content, state), { href: md.sanitizeUrl(node.target) as string, target: '_blank' }, state, ) + if (!state.openInApp) return link + const inAppIcon = htmlTag( + 'i', + '', + { class: 'open-in-app fas fa-arrow-up-right-from-square', 'data-href': href }, + state, + ) + return link + inAppIcon }, }, url: { @@ -158,12 +167,21 @@ const rules: MarkdownRules = { } }, html(node, output, state) { - return htmlTag( + const href = md.sanitizeUrl(node.target) as string + const link = htmlTag( 'a', output(node.content, state), { href: md.sanitizeUrl(node.target) as string, target: '_blank' }, state, ) + if (!state.openInApp) return link + const inAppIcon = htmlTag( + 'i', + '', + { class: 'open-in-app fas fa-arrow-right-to-bracket', 'data-href': href }, + state, + ) + return link + inAppIcon }, }, strike: { @@ -257,12 +275,16 @@ export default class extends Vue { @Prop({ required: true }) source!: string + @Prop({ default: false }) + openInApp!: boolean + render(h: any) { const state: MarkdownState = { inline: true, inQuote: false, escapeHTML: true, cssModuleNames: null, + openInApp: this.openInApp, } return h({ template: `
    ${htmlOutput(parser(this.source, state), state)}
    ` }) } diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index 080aaa4a..5857b2cd 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -35,6 +35,13 @@
  • +
  • + Always open Links in App + +
  • {{ $t('setting.keyboard_layout') }}