From 2b247dd0f38b5b9c776ec6df866028b63031c021 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 27 Jan 2024 14:54:40 +0100 Subject: [PATCH 01/44] Update issue comments --- go.mod | 2 + go.sum | 4 + modules/context/response.go | 13 + package-lock.json | 59 ++- package.json | 5 +- routers/web/web.go | 3 + routers/web/websocket/websocket.go | 43 +++ templates/base/head.tmpl | 2 +- web_src/js/features/common-global.js | 2 +- web_src/js/htmx.js | 31 ++ web_src/js/ws.js | 512 +++++++++++++++++++++++++++ webpack.config.js | 1 + 12 files changed, 667 insertions(+), 10 deletions(-) create mode 100644 routers/web/websocket/websocket.go create mode 100644 web_src/js/htmx.js create mode 100644 web_src/js/ws.js diff --git a/go.mod b/go.mod index 7a752ec874cfc..8ea2dfb7be604 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 + github.com/olahol/melody v1.1.4 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc6 @@ -210,6 +211,7 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index b3b8ad8ce48f9..3a76d3e813983 100644 --- a/go.sum +++ b/go.sum @@ -514,6 +514,8 @@ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -696,6 +698,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E= +github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= diff --git a/modules/context/response.go b/modules/context/response.go index 2f271f211b83a..d1b61500afd92 100644 --- a/modules/context/response.go +++ b/modules/context/response.go @@ -4,6 +4,9 @@ package context import ( + "bufio" + "errors" + "net" "net/http" web_types "code.gitea.io/gitea/modules/web/types" @@ -30,6 +33,14 @@ type Response struct { status int befores []func(ResponseWriter) beforeExecuted bool + hijacker http.Hijacker +} + +func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if r.hijacker == nil { + return nil, nil, errors.New("http.Hijacker not implemented by underlying http.ResponseWriter") + } + return r.hijacker.Hijack() } // Write writes bytes to HTTP endpoint @@ -95,9 +106,11 @@ func WrapResponseWriter(resp http.ResponseWriter) *Response { if v, ok := resp.(*Response); ok { return v } + hijacker, _ := resp.(http.Hijacker) return &Response{ ResponseWriter: resp, status: 0, befores: make([]func(ResponseWriter), 0), + hijacker: hijacker, } } diff --git a/package-lock.json b/package-lock.json index b299967eba59b..e04ee3b140985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "esbuild-loader": "4.0.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", + "htmx.org": "1.9.10", "jquery": "3.7.1", "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", @@ -50,7 +51,6 @@ "vue-loader": "17.3.1", "vue3-calendar-heatmap": "2.0.5", "webpack": "5.89.0", - "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { @@ -83,7 +83,8 @@ "svgo": "3.1.0", "updates": "15.0.4", "vite-string-plugin": "1.1.2", - "vitest": "1.1.0" + "vitest": "1.1.0", + "webpack-cli": "5.1.4" }, "engines": { "node": ">= 18.0.0" @@ -545,6 +546,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, "engines": { "node": ">=10.0.0" } @@ -2711,6 +2713,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, "engines": { "node": ">=14.15.0" }, @@ -2723,6 +2726,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, "engines": { "node": ">=14.15.0" }, @@ -2735,6 +2739,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, "engines": { "node": ">=14.15.0" }, @@ -3447,6 +3452,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -3460,6 +3466,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -3505,7 +3512,8 @@ "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3585,6 +3593,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4658,6 +4667,7 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, "bin": { "envinfo": "dist/cli.js" }, @@ -5460,6 +5470,7 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, "engines": { "node": ">= 4.9.1" } @@ -5561,6 +5572,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, "bin": { "flat": "cli.js" } @@ -5661,6 +5673,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6076,6 +6089,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6158,6 +6172,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/htmx.org": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz", + "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA==" + }, "node_modules/http-proxy-agent": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", @@ -6282,6 +6301,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -6363,6 +6383,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -6446,6 +6467,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -6726,12 +6748,14 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -7004,6 +7028,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -8713,6 +8738,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "engines": { "node": ">=6" } @@ -8786,6 +8812,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -8802,6 +8829,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -8809,7 +8837,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "1.10.1", @@ -8885,6 +8914,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -8896,6 +8926,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -8908,6 +8939,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -8919,6 +8951,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -8933,6 +8966,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -9433,6 +9467,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, "dependencies": { "resolve": "^1.20.0" }, @@ -9596,6 +9631,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -9612,6 +9648,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -9623,6 +9660,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "engines": { "node": ">=8" } @@ -9922,6 +9960,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, "dependencies": { "kind-of": "^6.0.2" }, @@ -9933,6 +9972,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -9944,6 +9984,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -10517,6 +10558,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -11549,6 +11591,7 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -11593,6 +11636,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, "engines": { "node": ">=14" } @@ -11601,6 +11645,7 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -11735,6 +11780,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -11799,7 +11845,8 @@ "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true }, "node_modules/wrap-ansi": { "version": "9.0.0", diff --git a/package.json b/package.json index 801e85db83a7e..29e83ba307e55 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "esbuild-loader": "4.0.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", + "htmx.org": "1.9.10", "jquery": "3.7.1", "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", @@ -49,7 +50,6 @@ "vue-loader": "17.3.1", "vue3-calendar-heatmap": "2.0.5", "webpack": "5.89.0", - "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { @@ -82,7 +82,8 @@ "svgo": "3.1.0", "updates": "15.0.4", "vite-string-plugin": "1.1.2", - "vitest": "1.1.0" + "vitest": "1.1.0", + "webpack-cli": "5.1.4" }, "browserslist": [ "defaults", diff --git a/routers/web/web.go b/routers/web/web.go index ff0ce0c2586bb..cf995b644128d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -41,6 +41,7 @@ import ( "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/routers/web/user/setting/security" + "code.gitea.io/gitea/routers/web/websocket" auth_service "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -541,6 +542,8 @@ func registerRoutes(m *web.Route) { m.Any("/user/events", routing.MarkLongPolling, events.Events) + websocket.Init(m) + m.Group("/login/oauth", func() { m.Get("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth) diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go new file mode 100644 index 0000000000000..7437588e99b2e --- /dev/null +++ b/routers/web/websocket/websocket.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package websocket + +import ( + "time" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web" + + "github.com/olahol/melody" +) + +var m *melody.Melody + +func Init(r *web.Route) { + m = melody.New() + r.Any("/ui-updates", WebSocket) + m.HandleMessage(HandleMessage) + + go func() { + for { + // TODO: send proper updated html + err := m.Broadcast([]byte("
hello world!
")) + if err != nil { + break + } + time.Sleep(5 * time.Second) + } + }() +} + +func WebSocket(ctx *context.Context) { + err := m.HandleRequest(ctx.Resp, ctx.Req) + if err != nil { + ctx.ServerError("HandleRequest", err) + } +} + +func HandleMessage(s *melody.Session, msg []byte) { + // TODO: Handle incoming messages +} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 876b42d512b18..8b43c0019bff4 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -29,7 +29,7 @@ {{template "base/head_style" .}} {{template "custom/header" .}} - + {{ctx.DataRaceCheck $.Context}} {{template "custom/body_outer_pre" .}} diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 0b00eb8e8ea74..5f8b3881d7c12 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -12,7 +12,7 @@ import {showTemporaryTooltip} from '../modules/tippy.js'; import {confirmModal} from './comp/ConfirmModal.js'; import {showErrorToast} from '../modules/toast.js'; import {request, POST} from '../modules/fetch.js'; - +import '../htmx.js'; const {appUrl, appSubUrl, csrfToken, i18n} = window.config; export function initGlobalFormDirtyLeaveConfirm() { diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js new file mode 100644 index 0000000000000..183498cfde37f --- /dev/null +++ b/web_src/js/htmx.js @@ -0,0 +1,31 @@ +import * as htmx from "htmx.org"; +import { showErrorToast } from "./modules/toast.js"; +import { ws } from "./ws.js"; + +window.htmx = htmx; + +// TODO: https://github.com/bigskysoftware/htmx/issues/1690 +ws(); +// import("htmx.org/dist/ext/ws.js"); + +console.log("htmx.js loaded", htmx.version, htmx); + +// https://htmx.org/reference/#config +htmx.config.requestClass = "is-loading"; +htmx.config.scrollIntoViewOnBoost = false; + +// https://htmx.org/events/#htmx:sendError +document.body.addEventListener("htmx:sendError", (event) => { + // TODO: add translations + showErrorToast( + `Network error when calling ${event.detail.requestConfig.path}` + ); +}); + +// https://htmx.org/events/#htmx:responseError +document.body.addEventListener("htmx:responseError", (event) => { + // TODO: add translations + showErrorToast( + `Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}` + ); +}); diff --git a/web_src/js/ws.js b/web_src/js/ws.js new file mode 100644 index 0000000000000..2eb72837be587 --- /dev/null +++ b/web_src/js/ws.js @@ -0,0 +1,512 @@ +/* +WebSockets Extension +============================ +This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions. +*/ + +export function ws() { + /** @type {import("../htmx").HtmxInternalApi} */ + var api; + + htmx.defineExtension("ws", { + /** + * init is called once, when this extension is first registered. + * @param {import("../htmx").HtmxInternalApi} apiRef + */ + init: function (apiRef) { + // Store reference to internal API + api = apiRef; + + // Default function for creating new EventSource objects + if (!htmx.createWebSocket) { + htmx.createWebSocket = createWebSocket; + } + + // Default setting for reconnect delay + if (!htmx.config.wsReconnectDelay) { + htmx.config.wsReconnectDelay = "full-jitter"; + } + }, + + /** + * onEvent handles all events passed to this extension. + * + * @param {string} name + * @param {Event} evt + */ + onEvent: function (name, evt) { + switch (name) { + // Try to close the socket when elements are removed + case "htmx:beforeCleanupElement": + var internalData = api.getInternalData(evt.target); + + if (internalData.webSocket) { + internalData.webSocket.close(); + } + return; + + // Try to create websockets when elements are processed + case "htmx:beforeProcessNode": + var parent = evt.target; + + forEach( + queryAttributeOnThisOrChildren(parent, "ws-connect"), + function (child) { + ensureWebSocket(child); + } + ); + forEach( + queryAttributeOnThisOrChildren(parent, "ws-send"), + function (child) { + ensureWebSocketSend(child); + } + ); + } + }, + }); + + function splitOnWhitespace(trigger) { + return trigger.trim().split(/\s+/); + } + + function getLegacyWebsocketURL(elt) { + var legacySSEValue = api.getAttributeValue(elt, "hx-ws"); + if (legacySSEValue) { + var values = splitOnWhitespace(legacySSEValue); + for (var i = 0; i < values.length; i++) { + var value = values[i].split(/:(.+)/); + if (value[0] === "connect") { + return value[1]; + } + } + } + } + + /** + * ensureWebSocket creates a new WebSocket on the designated element, using + * the element's "ws-connect" attribute. + * @param {HTMLElement} socketElt + * @returns + */ + function ensureWebSocket(socketElt) { + // If the element containing the WebSocket connection no longer exists, then + // do not connect/reconnect the WebSocket. + if (!api.bodyContains(socketElt)) { + return; + } + + // Get the source straight from the element's value + var wssSource = api.getAttributeValue(socketElt, "ws-connect"); + + if (wssSource == null || wssSource === "") { + var legacySource = getLegacyWebsocketURL(socketElt); + if (legacySource == null) { + return; + } else { + wssSource = legacySource; + } + } + + // Guarantee that the wssSource value is a fully qualified URL + if (wssSource.indexOf("/") === 0) { + var base_part = + location.hostname + (location.port ? ":" + location.port : ""); + if (location.protocol === "https:") { + wssSource = "wss://" + base_part + wssSource; + } else if (location.protocol === "http:") { + wssSource = "ws://" + base_part + wssSource; + } + } + + var socketWrapper = createWebsocketWrapper(socketElt, function () { + return htmx.createWebSocket(wssSource); + }); + + socketWrapper.addEventListener("message", function (event) { + if (maybeCloseWebSocketSource(socketElt)) { + return; + } + + var response = event.data; + if ( + !api.triggerEvent(socketElt, "htmx:wsBeforeMessage", { + message: response, + socketWrapper: socketWrapper.publicInterface, + }) + ) { + return; + } + + api.withExtensions(socketElt, function (extension) { + response = extension.transformResponse(response, null, socketElt); + }); + + var settleInfo = api.makeSettleInfo(socketElt); + var fragment = api.makeFragment(response); + + if (fragment.children.length) { + var children = Array.from(fragment.children); + for (var i = 0; i < children.length; i++) { + api.oobSwap( + api.getAttributeValue(children[i], "hx-swap-oob") || "true", + children[i], + settleInfo + ); + } + } + + api.settleImmediately(settleInfo.tasks); + api.triggerEvent(socketElt, "htmx:wsAfterMessage", { + message: response, + socketWrapper: socketWrapper.publicInterface, + }); + }); + + // Put the WebSocket into the HTML Element's custom data. + api.getInternalData(socketElt).webSocket = socketWrapper; + } + + /** + * @typedef {Object} WebSocketWrapper + * @property {WebSocket} socket + * @property {Array<{message: string, sendElt: Element}>} messageQueue + * @property {number} retryCount + * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state + * @property {(message: string, sendElt: Element) => void} send + * @property {(event: string, handler: Function) => void} addEventListener + * @property {() => void} handleQueuedMessages + * @property {() => void} init + * @property {() => void} close + */ + /** + * + * @param socketElt + * @param socketFunc + * @returns {WebSocketWrapper} + */ + function createWebsocketWrapper(socketElt, socketFunc) { + var wrapper = { + socket: null, + messageQueue: [], + retryCount: 0, + + /** @type {Object} */ + events: {}, + + addEventListener: function (event, handler) { + if (this.socket) { + this.socket.addEventListener(event, handler); + } + + if (!this.events[event]) { + this.events[event] = []; + } + + this.events[event].push(handler); + }, + + sendImmediately: function (message, sendElt) { + if (!this.socket) { + api.triggerErrorEvent(); + } + if ( + !sendElt || + api.triggerEvent(sendElt, "htmx:wsBeforeSend", { + message: message, + socketWrapper: this.publicInterface, + }) + ) { + this.socket.send(message); + sendElt && + api.triggerEvent(sendElt, "htmx:wsAfterSend", { + message: message, + socketWrapper: this.publicInterface, + }); + } + }, + + send: function (message, sendElt) { + if (this.socket.readyState !== this.socket.OPEN) { + this.messageQueue.push({ message: message, sendElt: sendElt }); + } else { + this.sendImmediately(message, sendElt); + } + }, + + handleQueuedMessages: function () { + while (this.messageQueue.length > 0) { + var queuedItem = this.messageQueue[0]; + if (this.socket.readyState === this.socket.OPEN) { + this.sendImmediately(queuedItem.message, queuedItem.sendElt); + this.messageQueue.shift(); + } else { + break; + } + } + }, + + init: function () { + if (this.socket && this.socket.readyState === this.socket.OPEN) { + // Close discarded socket + this.socket.close(); + } + + // Create a new WebSocket and event handlers + /** @type {WebSocket} */ + var socket = socketFunc(); + + // The event.type detail is added for interface conformance with the + // other two lifecycle events (open and close) so a single handler method + // can handle them polymorphically, if required. + api.triggerEvent(socketElt, "htmx:wsConnecting", { + event: { type: "connecting" }, + }); + + this.socket = socket; + + socket.onopen = function (e) { + wrapper.retryCount = 0; + api.triggerEvent(socketElt, "htmx:wsOpen", { + event: e, + socketWrapper: wrapper.publicInterface, + }); + wrapper.handleQueuedMessages(); + }; + + socket.onclose = function (e) { + // If socket should not be connected, stop further attempts to establish connection + // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. + if ( + !maybeCloseWebSocketSource(socketElt) && + [1006, 1012, 1013].indexOf(e.code) >= 0 + ) { + var delay = getWebSocketReconnectDelay(wrapper.retryCount); + setTimeout(function () { + wrapper.retryCount += 1; + wrapper.init(); + }, delay); + } + + // Notify client code that connection has been closed. Client code can inspect `event` field + // to determine whether closure has been valid or abnormal + api.triggerEvent(socketElt, "htmx:wsClose", { + event: e, + socketWrapper: wrapper.publicInterface, + }); + }; + + socket.onerror = function (e) { + api.triggerErrorEvent(socketElt, "htmx:wsError", { + error: e, + socketWrapper: wrapper, + }); + maybeCloseWebSocketSource(socketElt); + }; + + var events = this.events; + Object.keys(events).forEach(function (k) { + events[k].forEach(function (e) { + socket.addEventListener(k, e); + }); + }); + }, + + close: function () { + this.socket.close(); + }, + }; + + wrapper.init(); + + wrapper.publicInterface = { + send: wrapper.send.bind(wrapper), + sendImmediately: wrapper.sendImmediately.bind(wrapper), + queue: wrapper.messageQueue, + }; + + return wrapper; + } + + /** + * ensureWebSocketSend attaches trigger handles to elements with + * "ws-send" attribute + * @param {HTMLElement} elt + */ + function ensureWebSocketSend(elt) { + var legacyAttribute = api.getAttributeValue(elt, "hx-ws"); + if (legacyAttribute && legacyAttribute !== "send") { + return; + } + + var webSocketParent = api.getClosestMatch(elt, hasWebSocket); + processWebSocketSend(webSocketParent, elt); + } + + /** + * hasWebSocket function checks if a node has webSocket instance attached + * @param {HTMLElement} node + * @returns {boolean} + */ + function hasWebSocket(node) { + return api.getInternalData(node).webSocket != null; + } + + /** + * processWebSocketSend adds event listeners to the
element so that + * messages can be sent to the WebSocket server when the form is submitted. + * @param {HTMLElement} socketElt + * @param {HTMLElement} sendElt + */ + function processWebSocketSend(socketElt, sendElt) { + var nodeData = api.getInternalData(sendElt); + var triggerSpecs = api.getTriggerSpecs(sendElt); + triggerSpecs.forEach(function (ts) { + api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) { + if (maybeCloseWebSocketSource(socketElt)) { + return; + } + + /** @type {WebSocketWrapper} */ + var socketWrapper = api.getInternalData(socketElt).webSocket; + var headers = api.getHeaders(sendElt, api.getTarget(sendElt)); + var results = api.getInputValues(sendElt, "post"); + var errors = results.errors; + var rawParameters = results.values; + var expressionVars = api.getExpressionVars(sendElt); + var allParameters = api.mergeObjects(rawParameters, expressionVars); + var filteredParameters = api.filterValues(allParameters, sendElt); + + var sendConfig = { + parameters: filteredParameters, + unfilteredParameters: allParameters, + headers: headers, + errors: errors, + + triggeringEvent: evt, + messageBody: undefined, + socketWrapper: socketWrapper.publicInterface, + }; + + if (!api.triggerEvent(elt, "htmx:wsConfigSend", sendConfig)) { + return; + } + + if (errors && errors.length > 0) { + api.triggerEvent(elt, "htmx:validation:halted", errors); + return; + } + + var body = sendConfig.messageBody; + if (body === undefined) { + var toSend = Object.assign({}, sendConfig.parameters); + if (sendConfig.headers) toSend["HEADERS"] = headers; + body = JSON.stringify(toSend); + } + + socketWrapper.send(body, elt); + + if (evt && api.shouldCancel(evt, elt)) { + evt.preventDefault(); + } + }); + }); + } + + /** + * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects. + * @param {number} retryCount // The number of retries that have already taken place + * @returns {number} + */ + function getWebSocketReconnectDelay(retryCount) { + /** @type {"full-jitter" | ((retryCount:number) => number)} */ + var delay = htmx.config.wsReconnectDelay; + if (typeof delay === "function") { + return delay(retryCount); + } + if (delay === "full-jitter") { + var exp = Math.min(retryCount, 6); + var maxDelay = 1000 * Math.pow(2, exp); + return maxDelay * Math.random(); + } + + logError( + 'htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"' + ); + } + + /** + * maybeCloseWebSocketSource checks to the if the element that created the WebSocket + * still exists in the DOM. If NOT, then the WebSocket is closed and this function + * returns TRUE. If the element DOES EXIST, then no action is taken, and this function + * returns FALSE. + * + * @param {*} elt + * @returns + */ + function maybeCloseWebSocketSource(elt) { + if (!api.bodyContains(elt)) { + api.getInternalData(elt).webSocket.close(); + return true; + } + return false; + } + + /** + * createWebSocket is the default method for creating new WebSocket objects. + * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed. + * + * @param {string} url + * @returns WebSocket + */ + function createWebSocket(url) { + var sock = new WebSocket(url, []); + sock.binaryType = htmx.config.wsBinaryType; + return sock; + } + + /** + * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. + * + * @param {HTMLElement} elt + * @param {string} attributeName + */ + function queryAttributeOnThisOrChildren(elt, attributeName) { + var result = []; + + // If the parent element also contains the requested attribute, then add it to the results too. + if ( + api.hasAttribute(elt, attributeName) || + api.hasAttribute(elt, "hx-ws") + ) { + result.push(elt); + } + + // Search all child nodes that match the requested attribute + elt + .querySelectorAll( + "[" + + attributeName + + "], [data-" + + attributeName + + "], [data-hx-ws], [hx-ws]" + ) + .forEach(function (node) { + result.push(node); + }); + + return result; + } + + /** + * @template T + * @param {T[]} arr + * @param {(T) => void} func + */ + function forEach(arr, func) { + if (arr) { + for (var i = 0; i < arr.length; i++) { + func(arr[i]); + } + } + } +} diff --git a/webpack.config.js b/webpack.config.js index 448dc640036c4..0d8418938f0c7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -214,6 +214,7 @@ export default { }, override: { 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 + 'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause" }, emitError: true, allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)', From 5005245c39f096d82807492e9f64d49deaf985c1 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 27 Jan 2024 15:12:07 +0100 Subject: [PATCH 02/44] move dep --- package-lock.json | 53 ++++++----------------------------------------- package.json | 4 ++-- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index e04ee3b140985..d73fc5df5a8fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "vue-loader": "17.3.1", "vue3-calendar-heatmap": "2.0.5", "webpack": "5.89.0", + "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { @@ -83,8 +84,7 @@ "svgo": "3.1.0", "updates": "15.0.4", "vite-string-plugin": "1.1.2", - "vitest": "1.1.0", - "webpack-cli": "5.1.4" + "vitest": "1.1.0" }, "engines": { "node": ">= 18.0.0" @@ -546,7 +546,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -2713,7 +2712,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, "engines": { "node": ">=14.15.0" }, @@ -2726,7 +2724,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, "engines": { "node": ">=14.15.0" }, @@ -2739,7 +2736,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, "engines": { "node": ">=14.15.0" }, @@ -3452,7 +3448,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -3466,7 +3461,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -3512,8 +3506,7 @@ "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3593,7 +3586,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4667,7 +4659,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", - "dev": true, "bin": { "envinfo": "dist/cli.js" }, @@ -5470,7 +5461,6 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, "engines": { "node": ">= 4.9.1" } @@ -5572,7 +5562,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, "bin": { "flat": "cli.js" } @@ -5673,7 +5662,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6089,7 +6077,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6301,7 +6288,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -6383,7 +6369,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, "engines": { "node": ">=10.13.0" } @@ -6467,7 +6452,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -6748,14 +6732,12 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7028,7 +7010,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8738,7 +8719,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -8812,7 +8792,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -8829,7 +8808,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -8837,8 +8815,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", @@ -8914,7 +8891,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -8926,7 +8902,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -8939,7 +8914,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -8951,7 +8925,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -8966,7 +8939,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -9467,7 +9439,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, "dependencies": { "resolve": "^1.20.0" }, @@ -9631,7 +9602,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -9648,7 +9618,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -9660,7 +9629,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -9960,7 +9928,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "dependencies": { "kind-of": "^6.0.2" }, @@ -9972,7 +9939,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -9984,7 +9950,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -10558,7 +10523,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -11591,7 +11555,6 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -11636,7 +11599,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, "engines": { "node": ">=14" } @@ -11645,7 +11607,6 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -11780,7 +11741,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -11845,8 +11805,7 @@ "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" }, "node_modules/wrap-ansi": { "version": "9.0.0", diff --git a/package.json b/package.json index 29e83ba307e55..5c6de1dd628b7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "vue-loader": "17.3.1", "vue3-calendar-heatmap": "2.0.5", "webpack": "5.89.0", + "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { @@ -82,8 +83,7 @@ "svgo": "3.1.0", "updates": "15.0.4", "vite-string-plugin": "1.1.2", - "vitest": "1.1.0", - "webpack-cli": "5.1.4" + "vitest": "1.1.0" }, "browserslist": [ "defaults", From 7cf50ef276edceda44e332ced694e3070c7a60db Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sun, 28 Jan 2024 22:44:07 +0100 Subject: [PATCH 03/44] fix htmx ws loading --- web_src/js/htmx.js | 8 +- web_src/js/ws.js | 512 --------------------------------------------- webpack.config.js | 3 + 3 files changed, 4 insertions(+), 519 deletions(-) delete mode 100644 web_src/js/ws.js diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 183498cfde37f..9a15c54d912f8 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -1,15 +1,9 @@ import * as htmx from "htmx.org"; import { showErrorToast } from "./modules/toast.js"; -import { ws } from "./ws.js"; +import "htmx.org/dist/ext/ws.js"; window.htmx = htmx; -// TODO: https://github.com/bigskysoftware/htmx/issues/1690 -ws(); -// import("htmx.org/dist/ext/ws.js"); - -console.log("htmx.js loaded", htmx.version, htmx); - // https://htmx.org/reference/#config htmx.config.requestClass = "is-loading"; htmx.config.scrollIntoViewOnBoost = false; diff --git a/web_src/js/ws.js b/web_src/js/ws.js deleted file mode 100644 index 2eb72837be587..0000000000000 --- a/web_src/js/ws.js +++ /dev/null @@ -1,512 +0,0 @@ -/* -WebSockets Extension -============================ -This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions. -*/ - -export function ws() { - /** @type {import("../htmx").HtmxInternalApi} */ - var api; - - htmx.defineExtension("ws", { - /** - * init is called once, when this extension is first registered. - * @param {import("../htmx").HtmxInternalApi} apiRef - */ - init: function (apiRef) { - // Store reference to internal API - api = apiRef; - - // Default function for creating new EventSource objects - if (!htmx.createWebSocket) { - htmx.createWebSocket = createWebSocket; - } - - // Default setting for reconnect delay - if (!htmx.config.wsReconnectDelay) { - htmx.config.wsReconnectDelay = "full-jitter"; - } - }, - - /** - * onEvent handles all events passed to this extension. - * - * @param {string} name - * @param {Event} evt - */ - onEvent: function (name, evt) { - switch (name) { - // Try to close the socket when elements are removed - case "htmx:beforeCleanupElement": - var internalData = api.getInternalData(evt.target); - - if (internalData.webSocket) { - internalData.webSocket.close(); - } - return; - - // Try to create websockets when elements are processed - case "htmx:beforeProcessNode": - var parent = evt.target; - - forEach( - queryAttributeOnThisOrChildren(parent, "ws-connect"), - function (child) { - ensureWebSocket(child); - } - ); - forEach( - queryAttributeOnThisOrChildren(parent, "ws-send"), - function (child) { - ensureWebSocketSend(child); - } - ); - } - }, - }); - - function splitOnWhitespace(trigger) { - return trigger.trim().split(/\s+/); - } - - function getLegacyWebsocketURL(elt) { - var legacySSEValue = api.getAttributeValue(elt, "hx-ws"); - if (legacySSEValue) { - var values = splitOnWhitespace(legacySSEValue); - for (var i = 0; i < values.length; i++) { - var value = values[i].split(/:(.+)/); - if (value[0] === "connect") { - return value[1]; - } - } - } - } - - /** - * ensureWebSocket creates a new WebSocket on the designated element, using - * the element's "ws-connect" attribute. - * @param {HTMLElement} socketElt - * @returns - */ - function ensureWebSocket(socketElt) { - // If the element containing the WebSocket connection no longer exists, then - // do not connect/reconnect the WebSocket. - if (!api.bodyContains(socketElt)) { - return; - } - - // Get the source straight from the element's value - var wssSource = api.getAttributeValue(socketElt, "ws-connect"); - - if (wssSource == null || wssSource === "") { - var legacySource = getLegacyWebsocketURL(socketElt); - if (legacySource == null) { - return; - } else { - wssSource = legacySource; - } - } - - // Guarantee that the wssSource value is a fully qualified URL - if (wssSource.indexOf("/") === 0) { - var base_part = - location.hostname + (location.port ? ":" + location.port : ""); - if (location.protocol === "https:") { - wssSource = "wss://" + base_part + wssSource; - } else if (location.protocol === "http:") { - wssSource = "ws://" + base_part + wssSource; - } - } - - var socketWrapper = createWebsocketWrapper(socketElt, function () { - return htmx.createWebSocket(wssSource); - }); - - socketWrapper.addEventListener("message", function (event) { - if (maybeCloseWebSocketSource(socketElt)) { - return; - } - - var response = event.data; - if ( - !api.triggerEvent(socketElt, "htmx:wsBeforeMessage", { - message: response, - socketWrapper: socketWrapper.publicInterface, - }) - ) { - return; - } - - api.withExtensions(socketElt, function (extension) { - response = extension.transformResponse(response, null, socketElt); - }); - - var settleInfo = api.makeSettleInfo(socketElt); - var fragment = api.makeFragment(response); - - if (fragment.children.length) { - var children = Array.from(fragment.children); - for (var i = 0; i < children.length; i++) { - api.oobSwap( - api.getAttributeValue(children[i], "hx-swap-oob") || "true", - children[i], - settleInfo - ); - } - } - - api.settleImmediately(settleInfo.tasks); - api.triggerEvent(socketElt, "htmx:wsAfterMessage", { - message: response, - socketWrapper: socketWrapper.publicInterface, - }); - }); - - // Put the WebSocket into the HTML Element's custom data. - api.getInternalData(socketElt).webSocket = socketWrapper; - } - - /** - * @typedef {Object} WebSocketWrapper - * @property {WebSocket} socket - * @property {Array<{message: string, sendElt: Element}>} messageQueue - * @property {number} retryCount - * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state - * @property {(message: string, sendElt: Element) => void} send - * @property {(event: string, handler: Function) => void} addEventListener - * @property {() => void} handleQueuedMessages - * @property {() => void} init - * @property {() => void} close - */ - /** - * - * @param socketElt - * @param socketFunc - * @returns {WebSocketWrapper} - */ - function createWebsocketWrapper(socketElt, socketFunc) { - var wrapper = { - socket: null, - messageQueue: [], - retryCount: 0, - - /** @type {Object} */ - events: {}, - - addEventListener: function (event, handler) { - if (this.socket) { - this.socket.addEventListener(event, handler); - } - - if (!this.events[event]) { - this.events[event] = []; - } - - this.events[event].push(handler); - }, - - sendImmediately: function (message, sendElt) { - if (!this.socket) { - api.triggerErrorEvent(); - } - if ( - !sendElt || - api.triggerEvent(sendElt, "htmx:wsBeforeSend", { - message: message, - socketWrapper: this.publicInterface, - }) - ) { - this.socket.send(message); - sendElt && - api.triggerEvent(sendElt, "htmx:wsAfterSend", { - message: message, - socketWrapper: this.publicInterface, - }); - } - }, - - send: function (message, sendElt) { - if (this.socket.readyState !== this.socket.OPEN) { - this.messageQueue.push({ message: message, sendElt: sendElt }); - } else { - this.sendImmediately(message, sendElt); - } - }, - - handleQueuedMessages: function () { - while (this.messageQueue.length > 0) { - var queuedItem = this.messageQueue[0]; - if (this.socket.readyState === this.socket.OPEN) { - this.sendImmediately(queuedItem.message, queuedItem.sendElt); - this.messageQueue.shift(); - } else { - break; - } - } - }, - - init: function () { - if (this.socket && this.socket.readyState === this.socket.OPEN) { - // Close discarded socket - this.socket.close(); - } - - // Create a new WebSocket and event handlers - /** @type {WebSocket} */ - var socket = socketFunc(); - - // The event.type detail is added for interface conformance with the - // other two lifecycle events (open and close) so a single handler method - // can handle them polymorphically, if required. - api.triggerEvent(socketElt, "htmx:wsConnecting", { - event: { type: "connecting" }, - }); - - this.socket = socket; - - socket.onopen = function (e) { - wrapper.retryCount = 0; - api.triggerEvent(socketElt, "htmx:wsOpen", { - event: e, - socketWrapper: wrapper.publicInterface, - }); - wrapper.handleQueuedMessages(); - }; - - socket.onclose = function (e) { - // If socket should not be connected, stop further attempts to establish connection - // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. - if ( - !maybeCloseWebSocketSource(socketElt) && - [1006, 1012, 1013].indexOf(e.code) >= 0 - ) { - var delay = getWebSocketReconnectDelay(wrapper.retryCount); - setTimeout(function () { - wrapper.retryCount += 1; - wrapper.init(); - }, delay); - } - - // Notify client code that connection has been closed. Client code can inspect `event` field - // to determine whether closure has been valid or abnormal - api.triggerEvent(socketElt, "htmx:wsClose", { - event: e, - socketWrapper: wrapper.publicInterface, - }); - }; - - socket.onerror = function (e) { - api.triggerErrorEvent(socketElt, "htmx:wsError", { - error: e, - socketWrapper: wrapper, - }); - maybeCloseWebSocketSource(socketElt); - }; - - var events = this.events; - Object.keys(events).forEach(function (k) { - events[k].forEach(function (e) { - socket.addEventListener(k, e); - }); - }); - }, - - close: function () { - this.socket.close(); - }, - }; - - wrapper.init(); - - wrapper.publicInterface = { - send: wrapper.send.bind(wrapper), - sendImmediately: wrapper.sendImmediately.bind(wrapper), - queue: wrapper.messageQueue, - }; - - return wrapper; - } - - /** - * ensureWebSocketSend attaches trigger handles to elements with - * "ws-send" attribute - * @param {HTMLElement} elt - */ - function ensureWebSocketSend(elt) { - var legacyAttribute = api.getAttributeValue(elt, "hx-ws"); - if (legacyAttribute && legacyAttribute !== "send") { - return; - } - - var webSocketParent = api.getClosestMatch(elt, hasWebSocket); - processWebSocketSend(webSocketParent, elt); - } - - /** - * hasWebSocket function checks if a node has webSocket instance attached - * @param {HTMLElement} node - * @returns {boolean} - */ - function hasWebSocket(node) { - return api.getInternalData(node).webSocket != null; - } - - /** - * processWebSocketSend adds event listeners to the element so that - * messages can be sent to the WebSocket server when the form is submitted. - * @param {HTMLElement} socketElt - * @param {HTMLElement} sendElt - */ - function processWebSocketSend(socketElt, sendElt) { - var nodeData = api.getInternalData(sendElt); - var triggerSpecs = api.getTriggerSpecs(sendElt); - triggerSpecs.forEach(function (ts) { - api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) { - if (maybeCloseWebSocketSource(socketElt)) { - return; - } - - /** @type {WebSocketWrapper} */ - var socketWrapper = api.getInternalData(socketElt).webSocket; - var headers = api.getHeaders(sendElt, api.getTarget(sendElt)); - var results = api.getInputValues(sendElt, "post"); - var errors = results.errors; - var rawParameters = results.values; - var expressionVars = api.getExpressionVars(sendElt); - var allParameters = api.mergeObjects(rawParameters, expressionVars); - var filteredParameters = api.filterValues(allParameters, sendElt); - - var sendConfig = { - parameters: filteredParameters, - unfilteredParameters: allParameters, - headers: headers, - errors: errors, - - triggeringEvent: evt, - messageBody: undefined, - socketWrapper: socketWrapper.publicInterface, - }; - - if (!api.triggerEvent(elt, "htmx:wsConfigSend", sendConfig)) { - return; - } - - if (errors && errors.length > 0) { - api.triggerEvent(elt, "htmx:validation:halted", errors); - return; - } - - var body = sendConfig.messageBody; - if (body === undefined) { - var toSend = Object.assign({}, sendConfig.parameters); - if (sendConfig.headers) toSend["HEADERS"] = headers; - body = JSON.stringify(toSend); - } - - socketWrapper.send(body, elt); - - if (evt && api.shouldCancel(evt, elt)) { - evt.preventDefault(); - } - }); - }); - } - - /** - * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects. - * @param {number} retryCount // The number of retries that have already taken place - * @returns {number} - */ - function getWebSocketReconnectDelay(retryCount) { - /** @type {"full-jitter" | ((retryCount:number) => number)} */ - var delay = htmx.config.wsReconnectDelay; - if (typeof delay === "function") { - return delay(retryCount); - } - if (delay === "full-jitter") { - var exp = Math.min(retryCount, 6); - var maxDelay = 1000 * Math.pow(2, exp); - return maxDelay * Math.random(); - } - - logError( - 'htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"' - ); - } - - /** - * maybeCloseWebSocketSource checks to the if the element that created the WebSocket - * still exists in the DOM. If NOT, then the WebSocket is closed and this function - * returns TRUE. If the element DOES EXIST, then no action is taken, and this function - * returns FALSE. - * - * @param {*} elt - * @returns - */ - function maybeCloseWebSocketSource(elt) { - if (!api.bodyContains(elt)) { - api.getInternalData(elt).webSocket.close(); - return true; - } - return false; - } - - /** - * createWebSocket is the default method for creating new WebSocket objects. - * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed. - * - * @param {string} url - * @returns WebSocket - */ - function createWebSocket(url) { - var sock = new WebSocket(url, []); - sock.binaryType = htmx.config.wsBinaryType; - return sock; - } - - /** - * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. - * - * @param {HTMLElement} elt - * @param {string} attributeName - */ - function queryAttributeOnThisOrChildren(elt, attributeName) { - var result = []; - - // If the parent element also contains the requested attribute, then add it to the results too. - if ( - api.hasAttribute(elt, attributeName) || - api.hasAttribute(elt, "hx-ws") - ) { - result.push(elt); - } - - // Search all child nodes that match the requested attribute - elt - .querySelectorAll( - "[" + - attributeName + - "], [data-" + - attributeName + - "], [data-hx-ws], [hx-ws]" - ) - .forEach(function (node) { - result.push(node); - }); - - return result; - } - - /** - * @template T - * @param {T[]} arr - * @param {(T) => void} func - */ - function forEach(arr, func) { - if (arr) { - for (var i = 0; i < arr.length; i++) { - func(arr[i]); - } - } - } -} diff --git a/webpack.config.js b/webpack.config.js index 0d8418938f0c7..b67722ada14f1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -178,6 +178,9 @@ export default { ], }, plugins: [ + new webpack.ProvidePlugin({ + htmx: 'htmx.org', + }), new DefinePlugin({ __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production From 3b3563724468625f3f1ecd2a9b3c0ece232fa5ab Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 10:42:23 +0100 Subject: [PATCH 04/44] add listeners --- modules/eventsource/manager.go | 3 +- modules/eventsource/messenger.go | 4 +- routers/web/websocket/websocket.go | 129 ++++++++++++++++++++++++++++- templates/base/head.tmpl | 2 +- 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/modules/eventsource/manager.go b/modules/eventsource/manager.go index 7ed2a829038f3..f68e1515ae029 100644 --- a/modules/eventsource/manager.go +++ b/modules/eventsource/manager.go @@ -34,8 +34,7 @@ func (m *Manager) Register(uid int64) <-chan *Event { m.mutex.Lock() messenger, ok := m.messengers[uid] if !ok { - messenger = NewMessenger(uid) - m.messengers[uid] = messenger + m.messengers[uid] = NewMessenger() } select { case m.connection <- struct{}{}: diff --git a/modules/eventsource/messenger.go b/modules/eventsource/messenger.go index 6df26716be661..e1dd2d36b31f6 100644 --- a/modules/eventsource/messenger.go +++ b/modules/eventsource/messenger.go @@ -8,14 +8,12 @@ import "sync" // Messenger is a per uid message store type Messenger struct { mutex sync.Mutex - uid int64 channels []chan *Event } // NewMessenger creates a messenger for a particular uid -func NewMessenger(uid int64) *Messenger { +func NewMessenger() *Messenger { return &Messenger{ - uid: uid, channels: [](chan *Event){}, } } diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index 7437588e99b2e..e6e3b9385a076 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -4,20 +4,31 @@ package websocket import ( + "fmt" "time" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/eventsource" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/auth" "github.com/olahol/melody" ) var m *melody.Melody +type SessionData struct { + unregister func() +} + func Init(r *web.Route) { m = melody.New() - r.Any("/ui-updates", WebSocket) - m.HandleMessage(HandleMessage) + r.Any("/-/ws", webSocket) + m.HandleConnect(handleConnect) + m.HandleMessage(handleMessage) + m.HandleDisconnect(handleDisconnect) go func() { for { @@ -31,13 +42,123 @@ func Init(r *web.Route) { }() } -func WebSocket(ctx *context.Context) { +func webSocket(ctx *context.Context) { err := m.HandleRequest(ctx.Resp, ctx.Req) if err != nil { ctx.ServerError("HandleRequest", err) } } -func HandleMessage(s *melody.Session, msg []byte) { +func handleConnect(s *melody.Session) { + ctx := context.GetWebContext(s.Request) + + // Listen to connection close and un-register messageChan + notify := ctx.Done() + ctx.Resp.Flush() + + if !ctx.IsSigned { + // Return unauthorized status event + event := &eventsource.Event{ + Name: "close", + Data: "unauthorized", + } + _, _ = event.WriteTo(ctx.Resp) + ctx.Resp.Flush() + return + } + + shutdownCtx := graceful.GetManager().ShutdownContext() + + uid := ctx.Doer.ID + + messageChan := eventsource.GetManager().Register(uid) + + sessionData := &SessionData{ + unregister: func() { + eventsource.GetManager().Unregister(uid, messageChan) + // ensure the messageChan is closed + for { + _, ok := <-messageChan + if !ok { + break + } + } + }, + } + + s.Set("data", sessionData) + + timer := time.NewTicker(30 * time.Second) + +loop: + for { + select { + case <-notify: + go sessionData.unregister() + break loop + case <-shutdownCtx.Done(): + go sessionData.unregister() + break loop + case event, ok := <-messageChan: + if !ok { + break loop + } + + // Handle logout + if event.Name == "logout" { + if ctx.Session.ID() == event.Data { + _, _ = (&eventsource.Event{ + Name: "logout", + Data: "here", + }).WriteTo(ctx.Resp) + ctx.Resp.Flush() + go sessionData.unregister() + auth.HandleSignOut(ctx) + break loop + } + // Replace the event - we don't want to expose the session ID to the user + event = &eventsource.Event{ + Name: "logout", + Data: "elsewhere", + } + } + + _, err := event.WriteTo(ctx.Resp) + if err != nil { + log.Error("Unable to write to EventStream for user %s: %v", ctx.Doer.Name, err) + go sessionData.unregister() + break loop + } + ctx.Resp.Flush() + } + } + timer.Stop() +} + +func handleMessage(s *melody.Session, msg []byte) { // TODO: Handle incoming messages } + +func getSessionData(s *melody.Session) (*SessionData, error) { + _data, ok := s.Get("data") + if !ok { + return nil, fmt.Errorf("no session data") + } + + data, ok := _data.(*SessionData) + if !ok { + return nil, fmt.Errorf("invalid session data") + } + + return data, nil +} + +func handleDisconnect(s *melody.Session) { + data, err := getSessionData(s) + if err != nil { + log.Error("Unable to get session data: %v", err) + return + } + + data.unregister() +} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 8b43c0019bff4..fbc4505ce9b38 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -29,7 +29,7 @@ {{template "base/head_style" .}} {{template "custom/header" .}} - + {{ctx.DataRaceCheck $.Context}} {{template "custom/body_outer_pre" .}} From d77073b3b86ff63fc853098988d2f3f9c3f1be14 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 11:34:26 +0100 Subject: [PATCH 05/44] add websocket service --- routers/web/websocket/websocket.go | 130 +--------------------------- services/websocket/notifier.go | 28 ++++++ services/websocket/websocket.go | 134 +++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 126 deletions(-) create mode 100644 services/websocket/notifier.go create mode 100644 services/websocket/websocket.go diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index e6e3b9385a076..060fc7df50577 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -4,31 +4,23 @@ package websocket import ( - "fmt" "time" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/eventsource" - "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/web/auth" + "code.gitea.io/gitea/services/websocket" "github.com/olahol/melody" ) var m *melody.Melody -type SessionData struct { - unregister func() -} - func Init(r *web.Route) { m = melody.New() r.Any("/-/ws", webSocket) - m.HandleConnect(handleConnect) - m.HandleMessage(handleMessage) - m.HandleDisconnect(handleDisconnect) + m.HandleConnect(websocket.HandleConnect) + m.HandleMessage(websocket.HandleMessage) + m.HandleDisconnect(websocket.HandleDisconnect) go func() { for { @@ -48,117 +40,3 @@ func webSocket(ctx *context.Context) { ctx.ServerError("HandleRequest", err) } } - -func handleConnect(s *melody.Session) { - ctx := context.GetWebContext(s.Request) - - // Listen to connection close and un-register messageChan - notify := ctx.Done() - ctx.Resp.Flush() - - if !ctx.IsSigned { - // Return unauthorized status event - event := &eventsource.Event{ - Name: "close", - Data: "unauthorized", - } - _, _ = event.WriteTo(ctx.Resp) - ctx.Resp.Flush() - return - } - - shutdownCtx := graceful.GetManager().ShutdownContext() - - uid := ctx.Doer.ID - - messageChan := eventsource.GetManager().Register(uid) - - sessionData := &SessionData{ - unregister: func() { - eventsource.GetManager().Unregister(uid, messageChan) - // ensure the messageChan is closed - for { - _, ok := <-messageChan - if !ok { - break - } - } - }, - } - - s.Set("data", sessionData) - - timer := time.NewTicker(30 * time.Second) - -loop: - for { - select { - case <-notify: - go sessionData.unregister() - break loop - case <-shutdownCtx.Done(): - go sessionData.unregister() - break loop - case event, ok := <-messageChan: - if !ok { - break loop - } - - // Handle logout - if event.Name == "logout" { - if ctx.Session.ID() == event.Data { - _, _ = (&eventsource.Event{ - Name: "logout", - Data: "here", - }).WriteTo(ctx.Resp) - ctx.Resp.Flush() - go sessionData.unregister() - auth.HandleSignOut(ctx) - break loop - } - // Replace the event - we don't want to expose the session ID to the user - event = &eventsource.Event{ - Name: "logout", - Data: "elsewhere", - } - } - - _, err := event.WriteTo(ctx.Resp) - if err != nil { - log.Error("Unable to write to EventStream for user %s: %v", ctx.Doer.Name, err) - go sessionData.unregister() - break loop - } - ctx.Resp.Flush() - } - } - timer.Stop() -} - -func handleMessage(s *melody.Session, msg []byte) { - // TODO: Handle incoming messages -} - -func getSessionData(s *melody.Session) (*SessionData, error) { - _data, ok := s.Get("data") - if !ok { - return nil, fmt.Errorf("no session data") - } - - data, ok := _data.(*SessionData) - if !ok { - return nil, fmt.Errorf("invalid session data") - } - - return data, nil -} - -func handleDisconnect(s *melody.Session) { - data, err := getSessionData(s) - if err != nil { - log.Error("Unable to get session data: %v", err) - return - } - - data.unregister() -} diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go new file mode 100644 index 0000000000000..2eaf2d5f000e4 --- /dev/null +++ b/services/websocket/notifier.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package websocket + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + notify_service "code.gitea.io/gitea/services/notify" +) + +type webhookNotifier struct { + notify_service.NullNotifier +} + +var _ notify_service.Notifier = &webhookNotifier{} + +// NewNotifier create a new webhooksNotifier notifier +func NewNotifier() notify_service.Notifier { + return &webhookNotifier{} +} + +func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { + // TODO +} diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go new file mode 100644 index 0000000000000..b49db142531d6 --- /dev/null +++ b/services/websocket/websocket.go @@ -0,0 +1,134 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package websocket + +import ( + "fmt" + "time" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/eventsource" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/routers/web/auth" + "github.com/olahol/melody" +) + +type SessionData struct { + unregister func() +} + +func HandleConnect(s *melody.Session) { + ctx := context.GetWebContext(s.Request) + + // Listen to connection close and un-register messageChan + notify := ctx.Done() + ctx.Resp.Flush() + + if !ctx.IsSigned { + // Return unauthorized status event + event := &eventsource.Event{ + Name: "close", + Data: "unauthorized", + } + _, _ = event.WriteTo(ctx.Resp) + ctx.Resp.Flush() + return + } + + shutdownCtx := graceful.GetManager().ShutdownContext() + + uid := ctx.Doer.ID + + messageChan := eventsource.GetManager().Register(uid) + + sessionData := &SessionData{ + unregister: func() { + eventsource.GetManager().Unregister(uid, messageChan) + // ensure the messageChan is closed + for { + _, ok := <-messageChan + if !ok { + break + } + } + }, + } + + s.Set("data", sessionData) + + timer := time.NewTicker(30 * time.Second) + +loop: + for { + select { + case <-notify: + go sessionData.unregister() + break loop + case <-shutdownCtx.Done(): + go sessionData.unregister() + break loop + case event, ok := <-messageChan: + if !ok { + break loop + } + + // Handle logout + if event.Name == "logout" { + if ctx.Session.ID() == event.Data { + _, _ = (&eventsource.Event{ + Name: "logout", + Data: "here", + }).WriteTo(ctx.Resp) + ctx.Resp.Flush() + go sessionData.unregister() + auth.HandleSignOut(ctx) + break loop + } + // Replace the event - we don't want to expose the session ID to the user + event = &eventsource.Event{ + Name: "logout", + Data: "elsewhere", + } + } + + _, err := event.WriteTo(ctx.Resp) + if err != nil { + log.Error("Unable to write to EventStream for user %s: %v", ctx.Doer.Name, err) + go sessionData.unregister() + break loop + } + ctx.Resp.Flush() + } + } + timer.Stop() +} + +func HandleMessage(s *melody.Session, msg []byte) { + // TODO: Handle incoming messages +} + +func getSessionData(s *melody.Session) (*SessionData, error) { + _data, ok := s.Get("data") + if !ok { + return nil, fmt.Errorf("no session data") + } + + data, ok := _data.(*SessionData) + if !ok { + return nil, fmt.Errorf("invalid session data") + } + + return data, nil +} + +func HandleDisconnect(s *melody.Session) { + data, err := getSessionData(s) + if err != nil { + log.Error("Unable to get session data: %v", err) + return + } + + data.unregister() +} From 17f4faf2ae0b8c4994d2c916323f2ac2159e8040 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 14:11:05 +0100 Subject: [PATCH 06/44] improve websocket service --- routers/web/websocket/websocket.go | 17 +++-------------- services/websocket/notifier.go | 30 +++++++++++++++++++++++++++--- services/websocket/session.go | 26 ++++++++++++++++++++++++++ services/websocket/websocket.go | 21 +-------------------- 4 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 services/websocket/session.go diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index 060fc7df50577..c7cfc0e4ed2ed 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -4,13 +4,12 @@ package websocket import ( - "time" + "github.com/olahol/melody" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web" + notify_service "code.gitea.io/gitea/services/notify" "code.gitea.io/gitea/services/websocket" - - "github.com/olahol/melody" ) var m *melody.Melody @@ -21,17 +20,7 @@ func Init(r *web.Route) { m.HandleConnect(websocket.HandleConnect) m.HandleMessage(websocket.HandleMessage) m.HandleDisconnect(websocket.HandleDisconnect) - - go func() { - for { - // TODO: send proper updated html - err := m.Broadcast([]byte("
hello world!
")) - if err != nil { - break - } - time.Sleep(5 * time.Second) - } - }() + notify_service.RegisterNotifier(websocket.NewNotifier(m)) } func webSocket(ctx *context.Context) { diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 2eaf2d5f000e4..6f0ed9dc16244 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -10,19 +10,43 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" notify_service "code.gitea.io/gitea/services/notify" + "github.com/olahol/melody" ) type webhookNotifier struct { notify_service.NullNotifier + m *melody.Melody } var _ notify_service.Notifier = &webhookNotifier{} // NewNotifier create a new webhooksNotifier notifier -func NewNotifier() notify_service.Notifier { - return &webhookNotifier{} +func NewNotifier(m *melody.Melody) notify_service.Notifier { + return &webhookNotifier{ + m: m, + } } func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { - // TODO + // TODO: use proper message + msg := []byte("
hello world!
") + + n.m.BroadcastFilter(msg, func(s *melody.Session) bool { + sessionData, err := getSessionData(s) + if err != nil { + return false + } + + if sessionData.uid == doer.ID { + return true + } + + for _, mention := range mentions { + if mention.ID == sessionData.uid { + return true + } + } + + return false + }) } diff --git a/services/websocket/session.go b/services/websocket/session.go new file mode 100644 index 0000000000000..9627e361c1b46 --- /dev/null +++ b/services/websocket/session.go @@ -0,0 +1,26 @@ +package websocket + +import ( + "fmt" + + "github.com/olahol/melody" +) + +type sessionData struct { + uid int64 + unregister func() +} + +func getSessionData(s *melody.Session) (*sessionData, error) { + _data, ok := s.Get("data") + if !ok { + return nil, fmt.Errorf("no session data") + } + + data, ok := _data.(*sessionData) + if !ok { + return nil, fmt.Errorf("invalid session data") + } + + return data, nil +} diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index b49db142531d6..1ed70ef83d83c 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,7 +4,6 @@ package websocket import ( - "fmt" "time" "code.gitea.io/gitea/modules/context" @@ -15,10 +14,6 @@ import ( "github.com/olahol/melody" ) -type SessionData struct { - unregister func() -} - func HandleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) @@ -43,7 +38,7 @@ func HandleConnect(s *melody.Session) { messageChan := eventsource.GetManager().Register(uid) - sessionData := &SessionData{ + sessionData := &sessionData{ unregister: func() { eventsource.GetManager().Unregister(uid, messageChan) // ensure the messageChan is closed @@ -109,20 +104,6 @@ func HandleMessage(s *melody.Session, msg []byte) { // TODO: Handle incoming messages } -func getSessionData(s *melody.Session) (*SessionData, error) { - _data, ok := s.Get("data") - if !ok { - return nil, fmt.Errorf("no session data") - } - - data, ok := _data.(*SessionData) - if !ok { - return nil, fmt.Errorf("invalid session data") - } - - return data, nil -} - func HandleDisconnect(s *melody.Session) { data, err := getSessionData(s) if err != nil { From 948f1bdf9eb25f077fcb9dc6276446b4fd34d8b3 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 16:41:20 +0100 Subject: [PATCH 07/44] cleanup --- services/websocket/notifier.go | 6 ++- services/websocket/session.go | 6 ++- services/websocket/websocket.go | 87 +-------------------------------- 3 files changed, 11 insertions(+), 88 deletions(-) diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 6f0ed9dc16244..9d1b1d48c6702 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -9,6 +9,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" notify_service "code.gitea.io/gitea/services/notify" "github.com/olahol/melody" ) @@ -31,7 +32,7 @@ func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod // TODO: use proper message msg := []byte("
hello world!
") - n.m.BroadcastFilter(msg, func(s *melody.Session) bool { + err := n.m.BroadcastFilter(msg, func(s *melody.Session) bool { sessionData, err := getSessionData(s) if err != nil { return false @@ -49,4 +50,7 @@ func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod return false }) + if err != nil { + log.Error("Failed to broadcast message: %v", err) + } } diff --git a/services/websocket/session.go b/services/websocket/session.go index 9627e361c1b46..4c682ec10a1fe 100644 --- a/services/websocket/session.go +++ b/services/websocket/session.go @@ -1,3 +1,6 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package websocket import ( @@ -7,8 +10,7 @@ import ( ) type sessionData struct { - uid int64 - unregister func() + uid int64 } func getSessionData(s *melody.Session) (*sessionData, error) { diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 1ed70ef83d83c..37694d3953da7 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,100 +4,23 @@ package websocket import ( - "time" - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/eventsource" - "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/routers/web/auth" "github.com/olahol/melody" ) func HandleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) - // Listen to connection close and un-register messageChan - notify := ctx.Done() - ctx.Resp.Flush() - if !ctx.IsSigned { // Return unauthorized status event - event := &eventsource.Event{ - Name: "close", - Data: "unauthorized", - } - _, _ = event.WriteTo(ctx.Resp) - ctx.Resp.Flush() return } - shutdownCtx := graceful.GetManager().ShutdownContext() - uid := ctx.Doer.ID - - messageChan := eventsource.GetManager().Register(uid) - sessionData := &sessionData{ - unregister: func() { - eventsource.GetManager().Unregister(uid, messageChan) - // ensure the messageChan is closed - for { - _, ok := <-messageChan - if !ok { - break - } - } - }, + uid: uid, } - s.Set("data", sessionData) - - timer := time.NewTicker(30 * time.Second) - -loop: - for { - select { - case <-notify: - go sessionData.unregister() - break loop - case <-shutdownCtx.Done(): - go sessionData.unregister() - break loop - case event, ok := <-messageChan: - if !ok { - break loop - } - - // Handle logout - if event.Name == "logout" { - if ctx.Session.ID() == event.Data { - _, _ = (&eventsource.Event{ - Name: "logout", - Data: "here", - }).WriteTo(ctx.Resp) - ctx.Resp.Flush() - go sessionData.unregister() - auth.HandleSignOut(ctx) - break loop - } - // Replace the event - we don't want to expose the session ID to the user - event = &eventsource.Event{ - Name: "logout", - Data: "elsewhere", - } - } - - _, err := event.WriteTo(ctx.Resp) - if err != nil { - log.Error("Unable to write to EventStream for user %s: %v", ctx.Doer.Name, err) - go sessionData.unregister() - break loop - } - ctx.Resp.Flush() - } - } - timer.Stop() } func HandleMessage(s *melody.Session, msg []byte) { @@ -105,11 +28,5 @@ func HandleMessage(s *melody.Session, msg []byte) { } func HandleDisconnect(s *melody.Session) { - data, err := getSessionData(s) - if err != nil { - log.Error("Unable to get session data: %v", err) - return - } - - data.unregister() + // TODO: Handle disconnect } From 1d6a34d3725d80b925aef83d46b77e03ab7e1bb1 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 20:30:04 +0100 Subject: [PATCH 08/44] update templates --- services/websocket/notifier.go | 23 +- services/websocket/websocket.go | 2 + .../repo/issue/view_content/comment.tmpl | 674 +++++++++++++++++ .../repo/issue/view_content/comments.tmpl | 685 +----------------- 4 files changed, 703 insertions(+), 681 deletions(-) create mode 100644 templates/repo/issue/view_content/comment.tmpl diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 9d1b1d48c6702..11fd6c0c629fc 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -4,11 +4,15 @@ package websocket import ( + "bytes" "context" + "fmt" + "html/template" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" notify_service "code.gitea.io/gitea/services/notify" "github.com/olahol/melody" @@ -19,7 +23,10 @@ type webhookNotifier struct { m *melody.Melody } -var _ notify_service.Notifier = &webhookNotifier{} +var ( + _ notify_service.Notifier = &webhookNotifier{} + tplIssueComment base.TplName = "repo/issue/view" +) // NewNotifier create a new webhooksNotifier notifier func NewNotifier(m *melody.Melody) notify_service.Notifier { @@ -28,11 +35,21 @@ func NewNotifier(m *melody.Melody) notify_service.Notifier { } } +var addElementHTML = "
%s
" + func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { // TODO: use proper message - msg := []byte("
hello world!
") + var content bytes.Buffer + + tmpl := new(template.Template) + if err := tmpl.ExecuteTemplate(&content, string(tplIssueComment), comment); err != nil { + log.Error("Template: %v", err) + return + } + + msg := fmt.Sprintf(addElementHTML, ".timeline-item.comment.form", "test") - err := n.m.BroadcastFilter(msg, func(s *melody.Session) bool { + err := n.m.BroadcastFilter([]byte(msg), func(s *melody.Session) bool { sessionData, err := getSessionData(s) if err != nil { return false diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 37694d3953da7..c9abe36d63f2a 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -21,6 +21,8 @@ func HandleConnect(s *melody.Session) { uid: uid, } s.Set("data", sessionData) + + // TODO: handle logouts } func HandleMessage(s *melody.Session, msg []byte) { diff --git a/templates/repo/issue/view_content/comment.tmpl b/templates/repo/issue/view_content/comment.tmpl new file mode 100644 index 0000000000000..8de78a00cfb18 --- /dev/null +++ b/templates/repo/issue/view_content/comment.tmpl @@ -0,0 +1,674 @@ +{{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} + + +{{if eq .Type 0}} +
+ {{if .OriginalAuthor}} + + {{ctx.AvatarUtils.Avatar nil 40}} + + {{else}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + + {{end}} +
+
+
+ {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} + + + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + + {{else}} + {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + + {{end}} +
+
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not $.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{end}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} +
+
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{end}} +
+ {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{end}} +
+
+{{else if eq .Type 1}} +
+ {{svg "octicon-dot-fill"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 2}} +
+ {{svg "octicon-circle-slash"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 28}} +
+ {{svg "octicon-git-merge"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} + {{if eq $.Issue.PullRequest.Status 3}} + {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 3 5 6}} + {{$refFrom:= ""}} + {{if ne .RefRepoID .Issue.RepoID}} + {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} + {{end}} + {{$refTr := "repo.issues.ref_issue_from"}} + {{if .Issue.IsPull}} + {{$refTr = "repo.issues.ref_pull_from"}} + {{else if eq .RefAction 1}} + {{$refTr = "repo.issues.ref_closing_from"}} + {{else if eq .RefAction 2}} + {{$refTr = "repo.issues.ref_reopening_from"}} + {{end}} + {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if eq .RefAction 3}}{{end}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} + + {{if eq .RefAction 3}}{{end}} + + +
+{{else if eq .Type 4}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + +
+ {{svg "octicon-git-commit"}} + {{.Content | Str2html}} +
+
+{{else if eq .Type 7}} + {{if or .AddedLabels .RemovedLabels}} +
+ {{svg "octicon-tag"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if and .AddedLabels (not .RemovedLabels)}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} + {{else if and (not .AddedLabels) .RemovedLabels}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{end}} + +
+ {{end}} +{{else if eq .Type 8}} +
+ {{svg "octicon-milestone"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} + +
+{{else if and (eq .Type 9) (gt .AssigneeID 0)}} +
+ {{svg "octicon-person"}} + {{if .RemovedAssignee}} + {{template "shared/user/avatarlink" dict "user" .Assignee}} + + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .Assignee.ID}} + {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} + + {{else}} + {{template "shared/user/avatarlink" dict "user" .Assignee}} + + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} + + {{end}} +
+{{else if eq .Type 10}} +
+ {{svg "octicon-pencil"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} + +
+{{else if eq .Type 11}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} + +
+{{else if eq .Type 12}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} + +
+{{else if eq .Type 13}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + {{.Content|Sec2Time}} + {{end}} +
+
+{{else if eq .Type 14}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + {{.Content|Sec2Time}} + {{end}} +
+
+{{else if eq .Type 15}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} + +
+{{else if eq .Type 16}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} + +
+{{else if eq .Type 17}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$parsedDeadline := StringUtils.Split .Content "|"}} + {{if eq (len $parsedDeadline) 2}} + {{$from := DateTime "long" (index $parsedDeadline 1)}} + {{$to := DateTime "long" (index $parsedDeadline 0)}} + {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 18}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} + +
+{{else if eq .Type 19}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} + + {{end}} +
+{{else if eq .Type 20}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} + + {{end}} +
+{{else if eq .Type 22}} +
+
+ {{if .OriginalAuthor}} + {{else}} + {{/* Some timeline avatars need a offset to correctly allign with their speech + bubble. The condition depends on review type and for positive reviews whether + there is a comment element or not */}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + + {{end}} + {{svg (printf "octicon-%s" .Review.Type.Icon)}} + + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + + {{if eq .Review.Type 1}} + {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} + {{else if eq .Review.Type 2}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{else if eq .Review.Type 3}} + {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{end}} + {{if .Review.Dismissed}} +
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
+ {{end}} +
+
+ {{if or .Content .Attachments}} +
+
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + + {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} + +
+
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not $.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} + {{end}} +
+
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{end}} +
+ {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{end}} +
+
+ {{end}} + + {{if .Review.CodeComments}} +
+ {{range $filename, $lines := .Review.CodeComments}} + {{range $line, $comms := $lines}} + {{template "repo/issue/view_content/conversation" dict "." $ "comments" $comms}} + {{end}} + {{end}} +
+ {{end}} +
+{{else if eq .Type 23}} +
+ {{svg "octicon-lock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if .Content}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} + + {{else}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} + + {{end}} +
+{{else if eq .Type 24}} +
+ {{svg "octicon-key"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} + +
+{{else if eq .Type 25}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{.Poster.Name}} + {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + +
+{{else if eq .Type 26}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + + {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + - {{.Content|Sec2Time}} + {{end}} +
+
+{{else if eq .Type 27}} +
+ {{svg "octicon-eye"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if (gt .AssigneeID 0)}} + {{if .RemovedAssignee}} + {{if eq .PosterID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} + {{else}} + + {{$teamName := "Ghost Team"}} + {{if .AssigneeTeam}} + {{$teamName = .AssigneeTeam.Name}} + {{end}} + {{if .RemovedAssignee}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} + {{end}} + {{end}} + +
+{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} +
+ {{svg "octicon-repo-push"}} + + {{template "shared/user/authorlink" .Poster}} + {{if .IsForcePush}} + {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} + {{end}} + + {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} + + {{ctx.Locale.Tr "repo.issues.force_push_compare"}} + + {{end}} +
+ {{if not .IsForcePush}} + {{template "repo/commits_list_small" dict "comment" . "root" $}} + {{end}} +{{else if eq .Type 30}} + {{if not $.UnitProjectsGlobalDisabled}} +
+ {{svg "octicon-project"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$oldProjectDisplayHtml := "Unknown Project"}} + {{if .OldProject}} + {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} + {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} + {{end}} + {{$newProjectDisplayHtml := "Unknown Project"}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} + {{end}} + {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} + {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} + {{else if gt .OldProjectID 0}} + {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} + {{else if gt .ProjectID 0}} + {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} + {{end}} + +
+ {{end}} +{{else if eq .Type 32}} +
+
+ + + + {{svg "octicon-x" 16}} + + {{template "shared/user/authorlink" .Poster}} + {{$reviewerName := ""}} + {{if eq .Review.OriginalAuthor ""}} + {{$reviewerName = .Review.Reviewer.Name}} + {{else}} + {{$reviewerName = .Review.OriginalAuthor}} + {{end}} + {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} + +
+ {{if .Content}} +
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{ctx.Locale.Tr "action.review_dismissed_reason"}} + +
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
+
+
+ {{end}} +
+{{else if eq .Type 33}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if and .OldRef .NewRef}} + {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{else if .OldRef}} + {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} + {{end}} + +
+{{else if or (eq .Type 34) (eq .Type 35)}} +
+ {{svg "octicon-git-merge" 16}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} + +
+{{else if or (eq .Type 36) (eq .Type 37)}} +
+ {{svg "octicon-pin" 16}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} + +
+{{end}} \ No newline at end of file diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index ade0ea34cfa46..c65fc82f57bed 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -1,683 +1,12 @@ {{template "base/alert"}} {{range .Issue.Comments}} - {{if call $.ShouldShowCommentType .Type}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} - - - {{if eq .Type 0}} -
- {{if .OriginalAuthor}} - - {{ctx.AvatarUtils.Avatar nil 40}} - - {{else}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} -
-
-
- {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} - - - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - - {{else}} - {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} - - {{end}} -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{end}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} -
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} - {{end}} -
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}} -
-
- {{else if eq .Type 1}} -
- {{svg "octicon-dot-fill"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} - {{end}} - -
- {{else if eq .Type 2}} -
- {{svg "octicon-circle-slash"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} - {{end}} - -
- {{else if eq .Type 28}} -
- {{svg "octicon-git-merge"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} - {{if eq $.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} - {{end}} - -
- {{else if eq .Type 3 5 6}} - {{$refFrom:= ""}} - {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} - {{end}} - {{$refTr := "repo.issues.ref_issue_from"}} - {{if .Issue.IsPull}} - {{$refTr = "repo.issues.ref_pull_from"}} - {{else if eq .RefAction 1}} - {{$refTr = "repo.issues.ref_closing_from"}} - {{else if eq .RefAction 2}} - {{$refTr = "repo.issues.ref_reopening_from"}} - {{end}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if eq .RefAction 3}}{{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} - - {{if eq .RefAction 3}}{{end}} - - -
- {{else if eq .Type 4}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} - -
- {{svg "octicon-git-commit"}} - {{.Content | Str2html}} -
-
- {{else if eq .Type 7}} - {{if or .AddedLabels .RemovedLabels}} -
- {{svg "octicon-tag"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} - {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} - {{end}} - -
- {{end}} - {{else if eq .Type 8}} -
- {{svg "octicon-milestone"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} - -
- {{else if and (eq .Type 9) (gt .AssigneeID 0)}} -
- {{svg "octicon-person"}} - {{if .RemovedAssignee}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - - {{else}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - - {{end}} -
- {{else if eq .Type 10}} -
- {{svg "octicon-pencil"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} - -
- {{else if eq .Type 11}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} - -
- {{else if eq .Type 12}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} - -
- {{else if eq .Type 13}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
-
- {{else if eq .Type 14}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
-
- {{else if eq .Type 15}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} - -
- {{else if eq .Type 16}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} - -
- {{else if eq .Type 17}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$parsedDeadline := StringUtils.Split .Content "|"}} - {{if eq (len $parsedDeadline) 2}} - {{$from := DateTime "long" (index $parsedDeadline 1)}} - {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} - {{end}} - -
- {{else if eq .Type 18}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} - -
- {{else if eq .Type 19}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} - - {{end}} -
- {{else if eq .Type 20}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} - - {{end}} -
- {{else if eq .Type 22}} -
-
- {{if .OriginalAuthor}} - {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech - bubble. The condition depends on review type and for positive reviews whether - there is a comment element or not */}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} - {{svg (printf "octicon-%s" .Review.Type.Icon)}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - - {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} - {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{end}} - {{if .Review.Dismissed}} -
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
- {{end}} -
-
- {{if or .Content .Attachments}} -
-
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - - {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} - -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} - {{end}} -
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} - {{end}} -
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}} -
-
- {{end}} - - {{if .Review.CodeComments}} -
- {{range $filename, $lines := .Review.CodeComments}} - {{range $line, $comms := $lines}} - {{template "repo/issue/view_content/conversation" dict "." $ "comments" $comms}} - {{end}} - {{end}} -
- {{end}} -
- {{else if eq .Type 23}} -
- {{svg "octicon-lock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if .Content}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} - - {{else}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} - - {{end}} -
- {{else if eq .Type 24}} -
- {{svg "octicon-key"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} - -
- {{else if eq .Type 25}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{.Poster.Name}} - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - -
- {{else if eq .Type 26}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} - -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - - {{.Content|Sec2Time}} - {{end}} -
-
- {{else if eq .Type 27}} -
- {{svg "octicon-eye"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if (gt .AssigneeID 0)}} - {{if .RemovedAssignee}} - {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - {{else}} - - {{$teamName := "Ghost Team"}} - {{if .AssigneeTeam}} - {{$teamName = .AssigneeTeam.Name}} - {{end}} - {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} - {{end}} - {{end}} - -
- {{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} - - {{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}} - {{continue}} - {{end}} -
- {{svg "octicon-repo-push"}} - - {{template "shared/user/authorlink" .Poster}} - {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} - {{end}} - - {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - - {{ctx.Locale.Tr "repo.issues.force_push_compare"}} - - {{end}} -
- {{if not .IsForcePush}} - {{template "repo/commits_list_small" dict "comment" . "root" $}} - {{end}} - {{else if eq .Type 30}} - {{if not $.UnitProjectsGlobalDisabled}} -
- {{svg "octicon-project"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$oldProjectDisplayHtml := "Unknown Project"}} - {{if .OldProject}} - {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} - {{end}} - {{$newProjectDisplayHtml := "Unknown Project"}} - {{if .Project}} - {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} - {{end}} - {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} - {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} - {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} - {{end}} - -
- {{end}} - {{else if eq .Type 32}} -
-
- - - - {{svg "octicon-x" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{$reviewerName := ""}} - {{if eq .Review.OriginalAuthor ""}} - {{$reviewerName = .Review.Reviewer.Name}} - {{else}} - {{$reviewerName = .Review.OriginalAuthor}} - {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} - -
- {{if .Content}} -
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{ctx.Locale.Tr "action.review_dismissed_reason"}} - -
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
-
-
- {{end}} -
- {{else if eq .Type 33}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} - {{end}} - -
- {{else if or (eq .Type 34) (eq .Type 35)}} -
- {{svg "octicon-git-merge" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} - -
- {{else if or (eq .Type 36) (eq .Type 37)}} -
- {{svg "octicon-pin" 16}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} - -
+ {{if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} + {{- /* If PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered. */ -}} + {{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}} + {{continue}} {{end}} {{end}} + {{if call $.ShouldShowCommentType .Type}} + {{template "repo/issue/view_content/comment" .}} + {{end}} {{end}} From 21bcb1be3d0c6367055faebaf877e17b14669854 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 21:22:28 +0100 Subject: [PATCH 09/44] adust template --- templates/repo/issue/view_content/comments.tmpl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index c65fc82f57bed..038c62a67b187 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -1,12 +1,13 @@ {{template "base/alert"}} {{range .Issue.Comments}} - {{if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} - {{- /* If PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered. */ -}} - {{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}} - {{continue}} - {{end}} - {{end}} {{if call $.ShouldShowCommentType .Type}} + {{if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} + + {{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}} + {{continue}} + {{end}} + {{end}} + {{template "repo/issue/view_content/comment" .}} {{end}} {{end}} From fb49e6b60c372b3f31f46abd30312fbc4a303064 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 21:37:27 +0100 Subject: [PATCH 10/44] undo some changes --- modules/eventsource/manager.go | 3 ++- modules/eventsource/messenger.go | 4 +++- web_src/js/features/common-global.js | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/eventsource/manager.go b/modules/eventsource/manager.go index f68e1515ae029..7ed2a829038f3 100644 --- a/modules/eventsource/manager.go +++ b/modules/eventsource/manager.go @@ -34,7 +34,8 @@ func (m *Manager) Register(uid int64) <-chan *Event { m.mutex.Lock() messenger, ok := m.messengers[uid] if !ok { - m.messengers[uid] = NewMessenger() + messenger = NewMessenger(uid) + m.messengers[uid] = messenger } select { case m.connection <- struct{}{}: diff --git a/modules/eventsource/messenger.go b/modules/eventsource/messenger.go index e1dd2d36b31f6..6df26716be661 100644 --- a/modules/eventsource/messenger.go +++ b/modules/eventsource/messenger.go @@ -8,12 +8,14 @@ import "sync" // Messenger is a per uid message store type Messenger struct { mutex sync.Mutex + uid int64 channels []chan *Event } // NewMessenger creates a messenger for a particular uid -func NewMessenger() *Messenger { +func NewMessenger(uid int64) *Messenger { return &Messenger{ + uid: uid, channels: [](chan *Event){}, } } diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 5f8b3881d7c12..e8b546970fcbc 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -13,6 +13,7 @@ import {confirmModal} from './comp/ConfirmModal.js'; import {showErrorToast} from '../modules/toast.js'; import {request, POST} from '../modules/fetch.js'; import '../htmx.js'; + const {appUrl, appSubUrl, csrfToken, i18n} = window.config; export function initGlobalFormDirtyLeaveConfirm() { From 4c0309356ec78be49422c922e95741a839231951 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 22:13:32 +0100 Subject: [PATCH 11/44] sort imports --- routers/web/websocket/websocket.go | 4 ++-- services/websocket/websocket.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index c7cfc0e4ed2ed..89776bf684a09 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -4,12 +4,12 @@ package websocket import ( - "github.com/olahol/melody" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web" notify_service "code.gitea.io/gitea/services/notify" "code.gitea.io/gitea/services/websocket" + + "github.com/olahol/melody" ) var m *melody.Melody diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index c9abe36d63f2a..32377599d1152 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -5,6 +5,7 @@ package websocket import ( "code.gitea.io/gitea/modules/context" + "github.com/olahol/melody" ) From 7173bea1d7c4f8886eb7fe8089f7b07857fbe1d4 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 3 Feb 2024 23:24:40 +0100 Subject: [PATCH 12/44] render comment template --- services/websocket/notifier.go | 89 +- .../repo/issue/view_content/comment.tmpl | 1187 +++++++++-------- .../repo/issue/view_content/comments.tmpl | 2 +- 3 files changed, 666 insertions(+), 612 deletions(-) diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 11fd6c0c629fc..44c4d8111a024 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -7,49 +7,57 @@ import ( "bytes" "context" "fmt" - "html/template" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" + web_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" "github.com/olahol/melody" ) type webhookNotifier struct { notify_service.NullNotifier - m *melody.Melody + m *melody.Melody + rnd *templates.HTMLRender } -var ( - _ notify_service.Notifier = &webhookNotifier{} - tplIssueComment base.TplName = "repo/issue/view" -) +var tplIssueComment base.TplName = "repo/issue/view_content/comment" // NewNotifier create a new webhooksNotifier notifier func NewNotifier(m *melody.Melody) notify_service.Notifier { return &webhookNotifier{ - m: m, + m: m, + rnd: templates.HTMLRenderer(), } } var addElementHTML = "
%s
" -func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { - // TODO: use proper message - var content bytes.Buffer +func (n *webhookNotifier) filterSessions(fn func(*melody.Session) bool) []*melody.Session { + sessions, err := n.m.Sessions() + if err != nil { + log.Error("Failed to get sessions: %v", err) + return nil + } - tmpl := new(template.Template) - if err := tmpl.ExecuteTemplate(&content, string(tplIssueComment), comment); err != nil { - log.Error("Template: %v", err) - return + _sessions := make([]*melody.Session, 0, len(sessions)) + for _, s := range sessions { + if fn(s) { + _sessions = append(_sessions, s) + } } - msg := fmt.Sprintf(addElementHTML, ".timeline-item.comment.form", "test") + return _sessions +} - err := n.m.BroadcastFilter([]byte(msg), func(s *melody.Session) bool { +func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { + sessions := n.filterSessions(func(s *melody.Session) bool { sessionData, err := getSessionData(s) if err != nil { return false @@ -60,14 +68,57 @@ func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod } for _, mention := range mentions { - if mention.ID == sessionData.uid { + if sessionData.uid == mention.ID { return true } } return false }) - if err != nil { - log.Error("Failed to broadcast message: %v", err) + + for _, s := range sessions { + var content bytes.Buffer + + webCtx := web_context.GetWebContext(s.Request) + + t, err := webCtx.Render.TemplateLookup(string(tplIssueComment), webCtx.TemplateContext) + if err != nil { + log.Error("Failed to lookup template: %v", err) + return + } + + issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: webCtx.Repo.RepoLink, + }, + Metas: repo.ComposeMetas(ctx), + GitRepo: webCtx.Repo.GitRepo, + Ctx: ctx, + }, issue.Content) + if err != nil { + log.Error("Failed to render issue content: %v", err) + return + } + + ctxData := map[string]any{} + ctxData["Repository"] = repo + ctxData["Issue"] = issue + ctxData["IsSigned"] = true + + data := map[string]any{} + data["ctxData"] = ctxData + data["Comment"] = comment + + if err := t.Execute(&content, data); err != nil { + log.Error("Template: %v", err) + return + } + + msg := fmt.Sprintf(addElementHTML, ".timeline-item.comment.form", content.String()) + err = s.Write([]byte(msg)) + if err != nil { + log.Error("Failed to write to session: %v", err) + } + } } diff --git a/templates/repo/issue/view_content/comment.tmpl b/templates/repo/issue/view_content/comment.tmpl index 8de78a00cfb18..383b3e2d1dd16 100644 --- a/templates/repo/issue/view_content/comment.tmpl +++ b/templates/repo/issue/view_content/comment.tmpl @@ -1,674 +1,677 @@ -{{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} +{{$:=.ctxData}} - -{{if eq .Type 0}} -
- {{if .OriginalAuthor}} - - {{ctx.AvatarUtils.Avatar nil 40}} - - {{else}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} -
-
-
- {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} - - - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - - {{else}} - {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - +{{with .Comment}} + {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} + + {{if eq .Type 0}} +
+ {{if .OriginalAuthor}} + + {{ctx.AvatarUtils.Avatar nil 40}} + + {{else}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + + {{end}} +
+
+
+ {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} + + + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + + {{else}} + {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + {{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} - - {{end}} -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{end}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} +
+
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not $.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{end}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} +
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} +
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} {{end}}
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} {{end}}
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}}
-
-{{else if eq .Type 1}} -
- {{svg "octicon-dot-fill"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 2}} -
- {{svg "octicon-circle-slash"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 28}} -
- {{svg "octicon-git-merge"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} - {{if eq $.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 3 5 6}} - {{$refFrom:= ""}} - {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} - {{end}} - {{$refTr := "repo.issues.ref_issue_from"}} - {{if .Issue.IsPull}} - {{$refTr = "repo.issues.ref_pull_from"}} - {{else if eq .RefAction 1}} - {{$refTr = "repo.issues.ref_closing_from"}} - {{else if eq .RefAction 2}} - {{$refTr = "repo.issues.ref_reopening_from"}} - {{end}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if eq .RefAction 3}}{{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} - - {{if eq .RefAction 3}}{{end}} - - -
-{{else if eq .Type 4}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} - -
- {{svg "octicon-git-commit"}} - {{.Content | Str2html}} -
-
-{{else if eq .Type 7}} - {{if or .AddedLabels .RemovedLabels}} + {{else if eq .Type 1}}
- {{svg "octicon-tag"}} + {{svg "octicon-dot-fill"}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} - {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} {{end}}
- {{end}} -{{else if eq .Type 8}} -
- {{svg "octicon-milestone"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} - -
-{{else if and (eq .Type 9) (gt .AssigneeID 0)}} -
- {{svg "octicon-person"}} - {{if .RemovedAssignee}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} + {{else if eq .Type 2}} +
+ {{svg "octicon-circle-slash"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} + {{template "shared/user/authorlink" .Poster}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} {{end}} - {{else}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} +
+ {{else if eq .Type 28}} +
+ {{svg "octicon-git-merge"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} + {{template "shared/user/authorlink" .Poster}} + {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} + {{if eq $.Issue.PullRequest.Status 3}} + {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} {{end}} - {{end}} -
-{{else if eq .Type 10}} -
- {{svg "octicon-pencil"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} - -
-{{else if eq .Type 11}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} - -
-{{else if eq .Type 12}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} - -
-{{else if eq .Type 13}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}}
-
-{{else if eq .Type 14}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} + {{else if eq .Type 3 5 6}} + {{$refFrom:= ""}} + {{if ne .RefRepoID .Issue.RepoID}} + {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} + {{end}} + {{$refTr := "repo.issues.ref_issue_from"}} + {{if .Issue.IsPull}} + {{$refTr = "repo.issues.ref_pull_from"}} + {{else if eq .RefAction 1}} + {{$refTr = "repo.issues.ref_closing_from"}} + {{else if eq .RefAction 2}} + {{$refTr = "repo.issues.ref_reopening_from"}} + {{end}} + {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if eq .RefAction 3}}{{end}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} + + {{if eq .RefAction 3}}{{end}} + +
-
-{{else if eq .Type 15}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} - -
-{{else if eq .Type 16}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} - -
-{{else if eq .Type 17}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$parsedDeadline := StringUtils.Split .Content "|"}} - {{if eq (len $parsedDeadline) 2}} - {{$from := DateTime "long" (index $parsedDeadline 1)}} - {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 18}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} - -
-{{else if eq .Type 19}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} + {{else if eq .Type 4}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} +
- {{svg "octicon-plus"}} + {{svg "octicon-git-commit"}} + {{.Content | Str2html}} +
+
+ {{else if eq .Type 7}} + {{if or .AddedLabels .RemovedLabels}} +
+ {{svg "octicon-tag"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{if eq .DependentIssue.RepoID .Issue.RepoID}} - #{{.DependentIssue.Index}} {{.DependentIssue.Title}} - {{else}} - {{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}} - {{end}} - + {{template "shared/user/authorlink" .Poster}} + {{if and .AddedLabels (not .RemovedLabels)}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} + {{else if and (not .AddedLabels) .RemovedLabels}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{end}}
{{end}} -
-{{else if eq .Type 20}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} -
- {{svg "octicon-trash"}} + {{else if eq .Type 8}} +
+ {{svg "octicon-milestone"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} + +
+ {{else if and (eq .Type 9) (gt .AssigneeID 0)}} +
+ {{svg "octicon-person"}} + {{if .RemovedAssignee}} + {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{if eq .DependentIssue.RepoID .Issue.RepoID}} - #{{.DependentIssue.Index}} {{.DependentIssue.Title}} - {{else}} - {{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}} - {{end}} - + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .Assignee.ID}} + {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} -
- {{end}} -
-{{else if eq .Type 22}} -
-
- {{if .OriginalAuthor}} {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech - bubble. The condition depends on review type and for positive reviews whether - there is a comment element or not */}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - + {{template "shared/user/avatarlink" dict "user" .Assignee}} + + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} + {{end}} - {{svg (printf "octicon-%s" .Review.Type.Icon)}} +
+ {{else if eq .Type 10}} +
+ {{svg "octicon-pencil"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} + +
+ {{else if eq .Type 11}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} + +
+ {{else if eq .Type 12}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} + +
+ {{else if eq .Type 13}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} {{else}} - {{template "shared/user/authorlink" .Poster}} + {{.Content|Sec2Time}} {{end}} - - {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} - {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} +
+
+ {{else if eq .Type 14}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{.Content|Sec2Time}} {{end}} - {{if .Review.Dismissed}} -
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
+
+
+ {{else if eq .Type 15}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} + +
+ {{else if eq .Type 16}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} + +
+ {{else if eq .Type 17}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$parsedDeadline := StringUtils.Split .Content "|"}} + {{if eq (len $parsedDeadline) 2}} + {{$from := DateTime "long" (index $parsedDeadline 1)}} + {{$to := DateTime "long" (index $parsedDeadline 0)}} + {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} {{end}}
- {{if or .Content .Attachments}} -
-
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{else if eq .Type 18}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} + +
+ {{else if eq .Type 19}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} + + {{end}} +
+ {{else if eq .Type 20}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} + + {{end}} +
+ {{else if eq .Type 22}} +
+
+ {{if .OriginalAuthor}} + {{else}} + {{/* Some timeline avatars need a offset to correctly allign with their speech + bubble. The condition depends on review type and for positive reviews whether + there is a comment element or not */}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + + {{end}} + {{svg (printf "octicon-%s" .Review.Type.Icon)}} + + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + + {{if eq .Review.Type 1}} + {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} + {{else if eq .Review.Type 2}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{else if eq .Review.Type 3}} + {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{end}} + {{if .Review.Dismissed}} +
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
+ {{end}} +
+
+ {{if or .Content .Attachments}} +
+
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + + {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} + +
+
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not $.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} + {{end}} +
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} - {{end}} -
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} +
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} {{end}}
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} {{end}}
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}}
-
- {{end}} + {{end}} - {{if .Review.CodeComments}} -
- {{range $filename, $lines := .Review.CodeComments}} - {{range $line, $comms := $lines}} - {{template "repo/issue/view_content/conversation" dict "." $ "comments" $comms}} + {{if .Review.CodeComments}} +
+ {{range $filename, $lines := .Review.CodeComments}} + {{range $line, $comms := $lines}} + {{template "repo/issue/view_content/conversation" dict "." $ "comments" $comms}} + {{end}} {{end}} +
{{end}}
- {{end}} -
-{{else if eq .Type 23}} -
- {{svg "octicon-lock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if .Content}} + {{else if eq .Type 23}} +
+ {{svg "octicon-lock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if .Content}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} + + {{else}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} + + {{end}} +
+ {{else if eq .Type 24}} +
+ {{svg "octicon-key"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} - {{else}} +
+ {{else if eq .Type 25}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} + {{.Poster.Name}} + {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - {{end}} -
-{{else if eq .Type 24}} -
- {{svg "octicon-key"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} - -
-{{else if eq .Type 25}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{.Poster.Name}} - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - -
-{{else if eq .Type 26}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} +
+ {{else if eq .Type 26}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} - -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - - {{.Content|Sec2Time}} - {{end}} + {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + - {{.Content|Sec2Time}} + {{end}} +
-
-{{else if eq .Type 27}} -
- {{svg "octicon-eye"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if (gt .AssigneeID 0)}} - {{if .RemovedAssignee}} - {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} + {{else if eq .Type 27}} +
+ {{svg "octicon-eye"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if (gt .AssigneeID 0)}} + {{if .RemovedAssignee}} + {{if eq .PosterID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{end}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} {{end}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - {{else}} - - {{$teamName := "Ghost Team"}} - {{if .AssigneeTeam}} - {{$teamName = .AssigneeTeam.Name}} + + {{$teamName := "Ghost Team"}} + {{if .AssigneeTeam}} + {{$teamName = .AssigneeTeam.Name}} + {{end}} + {{if .RemovedAssignee}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} + {{end}} {{end}} - {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} + +
+ {{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} +
+ {{svg "octicon-repo-push"}} + + {{template "shared/user/authorlink" .Poster}} + {{if .IsForcePush}} + {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} + {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} {{end}} + + {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} + + {{ctx.Locale.Tr "repo.issues.force_push_compare"}} + {{end}} - -
-{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} -
- {{svg "octicon-repo-push"}} - - {{template "shared/user/authorlink" .Poster}} - {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} - {{end}} - - {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - - {{ctx.Locale.Tr "repo.issues.force_push_compare"}} - +
+ {{if not .IsForcePush}} + {{template "repo/commits_list_small" dict "comment" . "root" $}} {{end}} -
- {{if not .IsForcePush}} - {{template "repo/commits_list_small" dict "comment" . "root" $}} - {{end}} -{{else if eq .Type 30}} - {{if not $.UnitProjectsGlobalDisabled}} -
- {{svg "octicon-project"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$oldProjectDisplayHtml := "Unknown Project"}} - {{if .OldProject}} - {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} - {{end}} - {{$newProjectDisplayHtml := "Unknown Project"}} - {{if .Project}} - {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} - {{end}} - {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} - {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} - {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} - {{end}} - -
- {{end}} -{{else if eq .Type 32}} -
+ {{else if eq .Type 30}} + {{if not $.UnitProjectsGlobalDisabled}}
- - - - {{svg "octicon-x" 16}} + {{svg "octicon-project"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{$reviewerName := ""}} - {{if eq .Review.OriginalAuthor ""}} - {{$reviewerName = .Review.Reviewer.Name}} - {{else}} - {{$reviewerName = .Review.OriginalAuthor}} + {{$oldProjectDisplayHtml := "Unknown Project"}} + {{if .OldProject}} + {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} + {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} + {{end}} + {{$newProjectDisplayHtml := "Unknown Project"}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} + {{end}} + {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} + {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} + {{else if gt .OldProjectID 0}} + {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} + {{else if gt .ProjectID 0}} + {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
- {{if .Content}} -
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{ctx.Locale.Tr "action.review_dismissed_reason"}} - -
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} + {{else if eq .Type 32}} +
+
+ + + + {{svg "octicon-x" 16}} + + {{template "shared/user/authorlink" .Poster}} + {{$reviewerName := ""}} + {{if eq .Review.OriginalAuthor ""}} + {{$reviewerName = .Review.Reviewer.Name}} + {{else}} + {{$reviewerName = .Review.OriginalAuthor}} + {{end}} + {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} + +
+ {{if .Content}} +
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + {{end}} + + {{ctx.Locale.Tr "action.review_dismissed_reason"}} + +
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
-
- {{end}} -
-{{else if eq .Type 33}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} {{end}} - -
-{{else if or (eq .Type 34) (eq .Type 35)}} -
- {{svg "octicon-git-merge" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} - -
-{{else if or (eq .Type 36) (eq .Type 37)}} -
- {{svg "octicon-pin" 16}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} - -
-{{end}} \ No newline at end of file +
+ {{else if eq .Type 33}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if and .OldRef .NewRef}} + {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{else if .OldRef}} + {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} + {{end}} + +
+ {{else if or (eq .Type 34) (eq .Type 35)}} +
+ {{svg "octicon-git-merge" 16}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} + +
+ {{else if or (eq .Type 36) (eq .Type 37)}} +
+ {{svg "octicon-pin" 16}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} + +
+ {{end}} +{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 038c62a67b187..eb9b21ce94a6e 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -8,6 +8,6 @@ {{end}} {{end}} - {{template "repo/issue/view_content/comment" .}} + {{template "repo/issue/view_content/comment" dict "ctxData" $ "Comment" .}} {{end}} {{end}} From bc2a3524605d05b2f63bebbfc8d6e9d377bbc23b Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 11 Feb 2024 14:11:51 +0100 Subject: [PATCH 13/44] improve session data and rendering --- services/websocket/notifier.go | 166 ++- services/websocket/session.go | 15 +- services/websocket/websocket.go | 22 +- .../repo/issue/view_content/comment.tmpl | 1186 ++++++++--------- .../repo/issue/view_content/comments.tmpl | 2 +- 5 files changed, 759 insertions(+), 632 deletions(-) diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 44c4d8111a024..5e6b482205451 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -9,6 +9,8 @@ import ( "fmt" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -21,7 +23,7 @@ import ( "github.com/olahol/melody" ) -type webhookNotifier struct { +type websocketNotifier struct { notify_service.NullNotifier m *melody.Melody rnd *templates.HTMLRender @@ -31,15 +33,19 @@ var tplIssueComment base.TplName = "repo/issue/view_content/comment" // NewNotifier create a new webhooksNotifier notifier func NewNotifier(m *melody.Melody) notify_service.Notifier { - return &webhookNotifier{ + return &websocketNotifier{ m: m, rnd: templates.HTMLRenderer(), } } -var addElementHTML = "
%s
" +var ( + htmxAddElementEnd = "
%s
" + // htmxUpdateElement = "
%s
" + htmxRemoveElement = "
" +) -func (n *webhookNotifier) filterSessions(fn func(*melody.Session) bool) []*melody.Session { +func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData) bool) []*melody.Session { sessions, err := n.m.Sessions() if err != nil { log.Error("Failed to get sessions: %v", err) @@ -48,7 +54,12 @@ func (n *webhookNotifier) filterSessions(fn func(*melody.Session) bool) []*melod _sessions := make([]*melody.Session, 0, len(sessions)) for _, s := range sessions { - if fn(s) { + data, err := getSessionData(s) + if err != nil { + continue + } + + if fn(s, data) { _sessions = append(_sessions, s) } } @@ -56,30 +67,33 @@ func (n *webhookNotifier) filterSessions(fn func(*melody.Session) bool) []*melod return _sessions } -func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { - sessions := n.filterSessions(func(s *melody.Session) bool { - sessionData, err := getSessionData(s) - if err != nil { +func (n *websocketNotifier) filterIssueSessions(repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { + return n.filterSessions(func(s *melody.Session, data *sessionData) bool { + // if the user is watching the issue, they will get notifications + if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { return false } - if sessionData.uid == doer.ID { + // if the repo is public, the user will get notifications + if !repo.IsPrivate { return true } - for _, mention := range mentions { - if sessionData.uid == mention.ID { - return true - } - } + // if the repo is private, the user will get notifications if they have access to the repo - return false + // TODO: check if the user has access to the repo + return data.userID == issue.PosterID }) +} + +func (n *websocketNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { + sessions := n.filterIssueSessions(repo, issue) for _, s := range sessions { var content bytes.Buffer webCtx := web_context.GetWebContext(s.Request) + webCtx.Repo.Repository = repo t, err := webCtx.Render.TemplateLookup(string(tplIssueComment), webCtx.TemplateContext) if err != nil { @@ -87,19 +101,37 @@ func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod return } - issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: webCtx.Repo.RepoLink, - }, - Metas: repo.ComposeMetas(ctx), - GitRepo: webCtx.Repo.GitRepo, - Ctx: ctx, - }, issue.Content) - if err != nil { - log.Error("Failed to render issue content: %v", err) + if err := comment.LoadPoster(ctx); err != nil { + log.Error("Failed to load comment poster: %v", err) return } + if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { + if err := comment.LoadAttachments(ctx); err != nil { + log.Error("Failed to load comment attachments: %v", err) + return + } + + comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: webCtx.Repo.RepoLink, + }, + Metas: webCtx.Repo.Repository.ComposeMetas(ctx), + GitRepo: webCtx.Repo.GitRepo, + Ctx: webCtx, + }, comment.Content) + if err != nil { + log.Error("Failed to render comment content: %v", err) + return + } + comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor()) + if err != nil { + log.Error("Failed to get role descriptor: %v", err) + return + } + + } + ctxData := map[string]any{} ctxData["Repository"] = repo ctxData["Issue"] = issue @@ -114,11 +146,91 @@ func (n *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod return } - msg := fmt.Sprintf(addElementHTML, ".timeline-item.comment.form", content.String()) + msg := fmt.Sprintf(htmxAddElementEnd, ".timeline-item.comment.form", content.String()) err = s.Write([]byte(msg)) if err != nil { log.Error("Failed to write to session: %v", err) } + } +} +func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { + sessions := n.filterIssueSessions(c.Issue.Repo, c.Issue) + + for _, s := range sessions { + msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) + err := s.Write([]byte(msg)) + if err != nil { + log.Error("Failed to write to session: %v", err) + } } } + +// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue +func roleDescriptor(ctx context.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { + roleDescriptor := issues_model.RoleDescriptor{} + + if hasOriginalAuthor { + return roleDescriptor, nil + } + + perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) + if err != nil { + return roleDescriptor, err + } + + // If the poster is the actual poster of the issue, enable Poster role. + roleDescriptor.IsPoster = issue.IsPoster(poster.ID) + + // Check if the poster is owner of the repo. + if perm.IsOwner() { + // If the poster isn't an admin, enable the owner role. + if !poster.IsAdmin { + roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner + return roleDescriptor, nil + } + + // Otherwise check if poster is the real repo admin. + ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster) + if err != nil { + return roleDescriptor, err + } + if ok { + roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner + return roleDescriptor, nil + } + } + + // If repo is organization, check Member role + if err := repo.LoadOwner(ctx); err != nil { + return roleDescriptor, err + } + if repo.Owner.IsOrganization() { + if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil { + return roleDescriptor, err + } else if isMember { + roleDescriptor.RoleInRepo = issues_model.RoleRepoMember + return roleDescriptor, nil + } + } + + // If the poster is the collaborator of the repo + if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil { + return roleDescriptor, err + } else if isCollaborator { + roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator + return roleDescriptor, nil + } + + hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID) + if err != nil { + return roleDescriptor, err + } else if hasMergedPR { + roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor + } else if issue.IsPull { + // only display first time contributor in the first opening pull request + roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor + } + + return roleDescriptor, nil +} diff --git a/services/websocket/session.go b/services/websocket/session.go index 4c682ec10a1fe..435204f4991d0 100644 --- a/services/websocket/session.go +++ b/services/websocket/session.go @@ -5,12 +5,25 @@ package websocket import ( "fmt" + "net/url" "github.com/olahol/melody" ) type sessionData struct { - uid int64 + userID int64 + isSigned bool + onURL string +} + +func (d *sessionData) isOnURL(_u1 string) bool { + if d.onURL == "" { + return true + } + + u1, _ := url.Parse(d.onURL) + u2, _ := url.Parse(_u1) + return u1.Path == u2.Path } func getSessionData(s *melody.Session) (*sessionData, error) { diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 32377599d1152..d47e00233e9f5 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -12,22 +12,26 @@ import ( func HandleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) - if !ctx.IsSigned { - // Return unauthorized status event - return - } + data := &sessionData{} - uid := ctx.Doer.ID - sessionData := &sessionData{ - uid: uid, + if ctx.IsSigned { + data.isSigned = true + data.userID = ctx.Doer.ID } - s.Set("data", sessionData) + + s.Set("data", data) // TODO: handle logouts } func HandleMessage(s *melody.Session, msg []byte) { - // TODO: Handle incoming messages + data, err := getSessionData(s) + if err != nil { + return + } + + // TODO: only handle specific url message + data.onURL = string(msg) } func HandleDisconnect(s *melody.Session) { diff --git a/templates/repo/issue/view_content/comment.tmpl b/templates/repo/issue/view_content/comment.tmpl index 383b3e2d1dd16..eef5f2f2b6690 100644 --- a/templates/repo/issue/view_content/comment.tmpl +++ b/templates/repo/issue/view_content/comment.tmpl @@ -1,677 +1,675 @@ -{{$:=.ctxData}} +{{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} +{{$RepoLink:=$RepoLink}} -{{with .Comment}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} - - {{if eq .Type 0}} -
- {{if .OriginalAuthor}} - - {{ctx.AvatarUtils.Avatar nil 40}} - - {{else}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} -
-
-
- {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} - - - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - - {{else}} - {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} - - {{end}} -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + +{{if eq .Type 0}} +
+ {{if .OriginalAuthor}} + + {{ctx.AvatarUtils.Avatar nil 40}} + + {{else}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + + {{end}} +
+
+
+ {{if .OriginalAuthor}} + + {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if ctx.Repository.OriginalURL}} + + + ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + + {{else}} + {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + {{end}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} -
+ + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + + {{end}}
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} +
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not ctx.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID)}} {{end}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" ctx "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and ctx.IsSigned (eq ctx.SignedUserID .PosterID))}}
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} +
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" ctx "Attachments" .Attachments "Content" .RenderedContent}} {{end}}
+ {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID) "Reactions" $reactions}} + {{end}}
- {{else if eq .Type 1}} +
+{{else if eq .Type 1}} +
+ {{svg "octicon-dot-fill"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 2}} +
+ {{svg "octicon-circle-slash"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if .Issue.IsPull}} + {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 28}} +
+ {{svg "octicon-git-merge"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$link := printf "%s/commit/%s" ctx.Repository.Link (ctx.Issue.PullRequest.MergedCommitID|PathEscape)}} + {{if eq ctx.Issue.PullRequest.Status 3}} + {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha ctx.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" (ctx.BaseTarget|Escape)) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha ctx.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" (ctx.BaseTarget|Escape)) $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 3 5 6}} + {{$refFrom:= ""}} + {{if ne .RefRepoID .Issue.RepoID}} + {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} + {{end}} + {{$refTr := "repo.issues.ref_issue_from"}} + {{if .Issue.IsPull}} + {{$refTr = "repo.issues.ref_pull_from"}} + {{else if eq .RefAction 1}} + {{$refTr = "repo.issues.ref_closing_from"}} + {{else if eq .RefAction 2}} + {{$refTr = "repo.issues.ref_reopening_from"}} + {{end}} + {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if eq .RefAction 3}}{{end}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} + + {{if eq .RefAction 3}}{{end}} + + +
+{{else if eq .Type 4}} +
+ {{svg "octicon-bookmark"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + +
+ {{svg "octicon-git-commit"}} + {{.Content | Str2html}} +
+
+{{else if eq .Type 7}} + {{if or .AddedLabels .RemovedLabels}}
- {{svg "octicon-dot-fill"}} + {{svg "octicon-tag"}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} + {{if and .AddedLabels (not .RemovedLabels)}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels ctx .AddedLabels $RepoLink) $createdStr | Safe}} + {{else if and (not .AddedLabels) .RemovedLabels}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels ctx .RemovedLabels $RepoLink) $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels ctx .AddedLabels $RepoLink) (RenderLabels ctx .RemovedLabels $RepoLink) $createdStr | Safe}} {{end}}
- {{else if eq .Type 2}} -
- {{svg "octicon-circle-slash"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} +{{else if eq .Type 8}} +
+ {{svg "octicon-milestone"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} + +
+{{else if and (eq .Type 9) (gt .AssigneeID 0)}} +
+ {{svg "octicon-person"}} + {{if .RemovedAssignee}} + {{template "shared/user/avatarlink" dict "user" .Assignee}} - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .Assignee.ID}} + {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} {{end}} -
- {{else if eq .Type 28}} -
- {{svg "octicon-git-merge"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{else}} + {{template "shared/user/avatarlink" dict "user" .Assignee}} - {{template "shared/user/authorlink" .Poster}} - {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} - {{if eq $.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{template "shared/user/authorlink" .Assignee}} + {{if eq .Poster.ID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} {{end}} -
- {{else if eq .Type 3 5 6}} - {{$refFrom:= ""}} - {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} - {{end}} - {{$refTr := "repo.issues.ref_issue_from"}} - {{if .Issue.IsPull}} - {{$refTr = "repo.issues.ref_pull_from"}} - {{else if eq .RefAction 1}} - {{$refTr = "repo.issues.ref_closing_from"}} - {{else if eq .RefAction 2}} - {{$refTr = "repo.issues.ref_reopening_from"}} {{end}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if eq .RefAction 3}}{{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} - - {{if eq .RefAction 3}}{{end}} - - +
+{{else if eq .Type 10}} +
+ {{svg "octicon-pencil"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji ctx) (.NewTitle|RenderEmoji ctx) $createdStr | Safe}} + +
+{{else if eq .Type 11}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} + +
+{{else if eq .Type 12}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} + +
+{{else if eq .Type 13}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" ctx "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + {{.Content|Sec2Time}} + {{end}}
- {{else if eq .Type 4}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} - -
- {{svg "octicon-git-commit"}} - {{.Content | Str2html}} -
+
+{{else if eq .Type 14}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" ctx "comment" .}} +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + {{.Content|Sec2Time}} + {{end}}
- {{else if eq .Type 7}} - {{if or .AddedLabels .RemovedLabels}} -
- {{svg "octicon-tag"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} +
+{{else if eq .Type 15}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} + +
+{{else if eq .Type 16}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} + +
+{{else if eq .Type 17}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$parsedDeadline := StringUtils.Split .Content "|"}} + {{if eq (len $parsedDeadline) 2}} + {{$from := DateTime "long" (index $parsedDeadline 1)}} + {{$to := DateTime "long" (index $parsedDeadline 0)}} + {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} + {{end}} + +
+{{else if eq .Type 18}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} + +
+{{else if eq .Type 19}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} +
+ {{svg "octicon-plus"}} - {{template "shared/user/authorlink" .Poster}} - {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} - {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} - {{end}} + + {{if eq .DependentIssue.RepoID .Issue.RepoID}} + #{{.DependentIssue.Index}} {{.DependentIssue.Title}} + {{else}} + {{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}} + {{end}} +
{{end}} - {{else if eq .Type 8}} -
- {{svg "octicon-milestone"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} - -
- {{else if and (eq .Type 9) (gt .AssigneeID 0)}} -
- {{svg "octicon-person"}} - {{if .RemovedAssignee}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} +
+{{else if eq .Type 20}} +
+ {{svg "octicon-package-dependents"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} + + {{if .DependentIssue}} +
+ {{svg "octicon-trash"}} - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} + + {{if eq .DependentIssue.RepoID .Issue.RepoID}} + #{{.DependentIssue.Index}} {{.DependentIssue.Title}} + {{else}} + {{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}} + {{end}} + +
+ {{end}} +
+{{else if eq .Type 22}} +
+
+ {{if .OriginalAuthor}} {{else}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - + {{/* Some timeline avatars need a offset to correctly allign with their speech + bubble. The condition depends on review type and for positive reviews whether + there is a comment element or not */}} + + {{ctx.AvatarUtils.Avatar .Poster 40}} + {{end}} -
- {{else if eq .Type 10}} -
- {{svg "octicon-pencil"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} - -
- {{else if eq .Type 11}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} - -
- {{else if eq .Type 12}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} - -
- {{else if eq .Type 13}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{svg (printf "octicon-%s" .Review.Type.Icon)}} - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} + {{if .OriginalAuthor}} + + {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if ctx.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} {{else}} - {{.Content|Sec2Time}} + {{template "shared/user/authorlink" .Poster}} {{end}} -
-
- {{else if eq .Type 14}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} + + {{if eq .Review.Type 1}} + {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} + {{else if eq .Review.Type 2}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{else if eq .Review.Type 3}} + {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} {{else}} - {{.Content|Sec2Time}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} {{end}} -
-
- {{else if eq .Type 15}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} - -
- {{else if eq .Type 16}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} - -
- {{else if eq .Type 17}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$parsedDeadline := StringUtils.Split .Content "|"}} - {{if eq (len $parsedDeadline) 2}} - {{$from := DateTime "long" (index $parsedDeadline 1)}} - {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} + {{if .Review.Dismissed}} +
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
{{end}}
- {{else if eq .Type 18}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} - -
- {{else if eq .Type 19}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} - - {{end}} -
- {{else if eq .Type 20}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} -
- {{svg "octicon-trash"}} - - - {{if eq .DependentIssue.RepoID .Issue.RepoID}} - #{{.DependentIssue.Index}} {{.DependentIssue.Title}} + {{if or .Content .Attachments}} +
+
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{if .OriginalAuthor}} + + {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if ctx.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} {{else}} - {{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}} - {{end}} - - -
- {{end}} -
- {{else if eq .Type 22}} -
-
- {{if .OriginalAuthor}} - {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech - bubble. The condition depends on review type and for positive reviews whether - there is a comment element or not */}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} - {{svg (printf "octicon-%s" .Review.Type.Icon)}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - - {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} - {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{end}} - {{if .Review.Dismissed}} -
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
- {{end}} -
-
- {{if or .Content .Attachments}} -
-
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - + {{template "shared/user/authorlink" .Poster}} {{end}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} - -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} - {{end}} -
+ {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} +
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} +
+ {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not ctx.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID)}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" ctx "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and ctx.IsSigned (eq ctx.SignedUserID .PosterID))}} {{end}}
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}}
-
- {{end}} - - {{if .Review.CodeComments}} -
- {{range $filename, $lines := .Review.CodeComments}} - {{range $line, $comms := $lines}} - {{template "repo/issue/view_content/conversation" dict "." $ "comments" $comms}} +
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} + {{end}} +
+
{{.Content}}
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" ctx "Attachments" .Attachments "Content" .RenderedContent}} {{end}} +
+ {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID) "Reactions" $reactions}} {{end}}
- {{end}}
- {{else if eq .Type 23}} -
- {{svg "octicon-lock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if .Content}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} - - {{else}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} - + {{end}} + + {{if .Review.CodeComments}} +
+ {{range $filename, $lines := .Review.CodeComments}} + {{range $line, $comms := $lines}} + {{template "repo/issue/view_content/conversation" dict "." ctx "comments" $comms}} + {{end}} {{end}}
- {{else if eq .Type 24}} -
- {{svg "octicon-key"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} +
+{{else if eq .Type 23}} +
+ {{svg "octicon-lock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if .Content}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} -
- {{else if eq .Type 25}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{.Poster.Name}} - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - -
- {{else if eq .Type 26}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{else}} {{template "shared/user/authorlink" .Poster}} - - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - - {{.Content|Sec2Time}} - {{end}} -
+ {{end}} +
+{{else if eq .Type 24}} +
+ {{svg "octicon-key"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} + +
+{{else if eq .Type 25}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{.Poster.Name}} + {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + +
+{{else if eq .Type 26}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + + {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + +
+ {{svg "octicon-clock"}} + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{.RenderedContent}} + {{else}} + - {{.Content|Sec2Time}} + {{end}}
- {{else if eq .Type 27}} -
- {{svg "octicon-eye"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if (gt .AssigneeID 0)}} - {{if .RemovedAssignee}} - {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} +
+{{else if eq .Type 27}} +
+ {{svg "octicon-eye"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if (gt .AssigneeID 0)}} + {{if .RemovedAssignee}} + {{if eq .PosterID .AssigneeID}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} {{end}} {{else}} - - {{$teamName := "Ghost Team"}} - {{if .AssigneeTeam}} - {{$teamName = .AssigneeTeam.Name}} - {{end}} - {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} - {{end}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} {{end}} - -
- {{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} -
- {{svg "octicon-repo-push"}} - - {{template "shared/user/authorlink" .Poster}} - {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} + {{else}} + + {{$teamName := "Ghost Team"}} + {{if .AssigneeTeam}} + {{$teamName = .AssigneeTeam.Name}} + {{end}} + {{if .RemovedAssignee}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} {{end}} - - {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - - {{ctx.Locale.Tr "repo.issues.force_push_compare"}} - {{end}} -
- {{if not .IsForcePush}} - {{template "repo/commits_list_small" dict "comment" . "root" $}} + +
+{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} +
+ {{svg "octicon-repo-push"}} + + {{template "shared/user/authorlink" .Poster}} + {{if .IsForcePush}} + {{ctx.Locale.Tr "repo.issues.force_push_codes" (ctx.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) ((ctx.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) ((ctx.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} + {{end}} + + {{if and .IsForcePush ctx.Issue.PullRequest.BaseRepo.Name}} + + {{ctx.Locale.Tr "repo.issues.force_push_compare"}} + {{end}} - {{else if eq .Type 30}} - {{if not $.UnitProjectsGlobalDisabled}} +
+ {{if not .IsForcePush}} + {{template "repo/commits_list_small" dict "comment" . "root" $}} + {{end}} +{{else if eq .Type 30}} + {{if not ctx.UnitProjectsGlobalDisabled}} +
+ {{svg "octicon-project"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$oldProjectDisplayHtml := "Unknown Project"}} + {{if .OldProject}} + {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} + {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} + {{end}} + {{$newProjectDisplayHtml := "Unknown Project"}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} + {{end}} + {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} + {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} + {{else if gt .OldProjectID 0}} + {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} + {{else if gt .ProjectID 0}} + {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} + {{end}} + +
+ {{end}} +{{else if eq .Type 32}} +
- {{svg "octicon-project"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + + + + {{svg "octicon-x" 16}} {{template "shared/user/authorlink" .Poster}} - {{$oldProjectDisplayHtml := "Unknown Project"}} - {{if .OldProject}} - {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} - {{end}} - {{$newProjectDisplayHtml := "Unknown Project"}} - {{if .Project}} - {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} - {{end}} - {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} - {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} - {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} + {{$reviewerName := ""}} + {{if eq .Review.OriginalAuthor ""}} + {{$reviewerName = .Review.Reviewer.Name}} + {{else}} + {{$reviewerName = .Review.OriginalAuthor}} {{end}} + {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
- {{end}} - {{else if eq .Type 32}} -
-
- - - - {{svg "octicon-x" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{$reviewerName := ""}} - {{if eq .Review.OriginalAuthor ""}} - {{$reviewerName = .Review.Reviewer.Name}} - {{else}} - {{$reviewerName = .Review.OriginalAuthor}} - {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} - -
- {{if .Content}} -
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - + {{if .Content}} +
+
+
+ {{if gt .Poster.ID 0}} + + {{ctx.AvatarUtils.Avatar .Poster 24}} + + {{end}} + + {{ctx.Locale.Tr "action.review_dismissed_reason"}} + +
+
+
+ {{if .RenderedContent}} + {{.RenderedContent|Str2html}} + {{else}} + {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}} - - {{ctx.Locale.Tr "action.review_dismissed_reason"}} - -
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
+
+ {{end}} +
+{{else if eq .Type 33}} +
+ {{svg "octicon-git-branch"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if and .OldRef .NewRef}} + {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{else if .OldRef}} + {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} + {{else}} + {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} {{end}} -
- {{else if eq .Type 33}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} - {{end}} - -
- {{else if or (eq .Type 34) (eq .Type 35)}} -
- {{svg "octicon-git-merge" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} - -
- {{else if or (eq .Type 36) (eq .Type 37)}} -
- {{svg "octicon-pin" 16}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} - -
- {{end}} + +
+{{else if or (eq .Type 34) (eq .Type 35)}} +
+ {{svg "octicon-git-merge" 16}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} + +
+{{else if or (eq .Type 36) (eq .Type 37)}} +
+ {{svg "octicon-pin" 16}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} + {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} + +
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index eb9b21ce94a6e..038c62a67b187 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -8,6 +8,6 @@ {{end}} {{end}} - {{template "repo/issue/view_content/comment" dict "ctxData" $ "Comment" .}} + {{template "repo/issue/view_content/comment" .}} {{end}} {{end}} From b2f4aaedfecdfe71c2071d178d980205ff8e4451 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 11 Feb 2024 14:26:53 +0100 Subject: [PATCH 14/44] cleanup --- services/websocket/issue_comment_notifier.go | 46 ++ services/websocket/notifier.go | 194 +---- services/websocket/session.go | 2 +- services/websocket/websocket.go | 41 +- .../repo/issue/view_content/comment.tmpl | 675 ------------------ 5 files changed, 89 insertions(+), 869 deletions(-) create mode 100644 services/websocket/issue_comment_notifier.go delete mode 100644 templates/repo/issue/view_content/comment.tmpl diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go new file mode 100644 index 0000000000000..a7205b4c7e4ed --- /dev/null +++ b/services/websocket/issue_comment_notifier.go @@ -0,0 +1,46 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package websocket + +import ( + "context" + "fmt" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "github.com/olahol/melody" +) + +func (n *websocketNotifier) filterIssueSessions(repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { + return n.filterSessions(func(s *melody.Session, data *sessionData) bool { + // if the user is watching the issue, they will get notifications + if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { + return false + } + + // if the repo is public, the user will get notifications + if !repo.IsPrivate { + return true + } + + // if the repo is private, the user will get notifications if they have access to the repo + + // TODO: check if the user has access to the repo + return data.userID == issue.PosterID + }) +} + +func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { + sessions := n.filterIssueSessions(c.Issue.Repo, c.Issue) + + for _, s := range sessions { + msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) + err := s.Write([]byte(msg)) + if err != nil { + log.Error("Failed to write to session: %v", err) + } + } +} diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 5e6b482205451..10e3c82f9ab42 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -1,23 +1,10 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package websocket import ( - "bytes" - "context" - "fmt" - - issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" - web_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" "github.com/olahol/melody" @@ -29,8 +16,6 @@ type websocketNotifier struct { rnd *templates.HTMLRender } -var tplIssueComment base.TplName = "repo/issue/view_content/comment" - // NewNotifier create a new webhooksNotifier notifier func NewNotifier(m *melody.Melody) notify_service.Notifier { return &websocketNotifier{ @@ -39,11 +24,10 @@ func NewNotifier(m *melody.Melody) notify_service.Notifier { } } -var ( - htmxAddElementEnd = "
%s
" - // htmxUpdateElement = "
%s
" - htmxRemoveElement = "
" -) +// htmxAddElementEnd = "
%s
" +// htmxUpdateElement = "
%s
" + +var htmxRemoveElement = "
" func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData) bool) []*melody.Session { sessions, err := n.m.Sessions() @@ -66,171 +50,3 @@ func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData return _sessions } - -func (n *websocketNotifier) filterIssueSessions(repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { - return n.filterSessions(func(s *melody.Session, data *sessionData) bool { - // if the user is watching the issue, they will get notifications - if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { - return false - } - - // if the repo is public, the user will get notifications - if !repo.IsPrivate { - return true - } - - // if the repo is private, the user will get notifications if they have access to the repo - - // TODO: check if the user has access to the repo - return data.userID == issue.PosterID - }) -} - -func (n *websocketNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) { - sessions := n.filterIssueSessions(repo, issue) - - for _, s := range sessions { - var content bytes.Buffer - - webCtx := web_context.GetWebContext(s.Request) - webCtx.Repo.Repository = repo - - t, err := webCtx.Render.TemplateLookup(string(tplIssueComment), webCtx.TemplateContext) - if err != nil { - log.Error("Failed to lookup template: %v", err) - return - } - - if err := comment.LoadPoster(ctx); err != nil { - log.Error("Failed to load comment poster: %v", err) - return - } - - if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { - if err := comment.LoadAttachments(ctx); err != nil { - log.Error("Failed to load comment attachments: %v", err) - return - } - - comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: webCtx.Repo.RepoLink, - }, - Metas: webCtx.Repo.Repository.ComposeMetas(ctx), - GitRepo: webCtx.Repo.GitRepo, - Ctx: webCtx, - }, comment.Content) - if err != nil { - log.Error("Failed to render comment content: %v", err) - return - } - comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor()) - if err != nil { - log.Error("Failed to get role descriptor: %v", err) - return - } - - } - - ctxData := map[string]any{} - ctxData["Repository"] = repo - ctxData["Issue"] = issue - ctxData["IsSigned"] = true - - data := map[string]any{} - data["ctxData"] = ctxData - data["Comment"] = comment - - if err := t.Execute(&content, data); err != nil { - log.Error("Template: %v", err) - return - } - - msg := fmt.Sprintf(htmxAddElementEnd, ".timeline-item.comment.form", content.String()) - err = s.Write([]byte(msg)) - if err != nil { - log.Error("Failed to write to session: %v", err) - } - } -} - -func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { - sessions := n.filterIssueSessions(c.Issue.Repo, c.Issue) - - for _, s := range sessions { - msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) - err := s.Write([]byte(msg)) - if err != nil { - log.Error("Failed to write to session: %v", err) - } - } -} - -// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue -func roleDescriptor(ctx context.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { - roleDescriptor := issues_model.RoleDescriptor{} - - if hasOriginalAuthor { - return roleDescriptor, nil - } - - perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) - if err != nil { - return roleDescriptor, err - } - - // If the poster is the actual poster of the issue, enable Poster role. - roleDescriptor.IsPoster = issue.IsPoster(poster.ID) - - // Check if the poster is owner of the repo. - if perm.IsOwner() { - // If the poster isn't an admin, enable the owner role. - if !poster.IsAdmin { - roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner - return roleDescriptor, nil - } - - // Otherwise check if poster is the real repo admin. - ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster) - if err != nil { - return roleDescriptor, err - } - if ok { - roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner - return roleDescriptor, nil - } - } - - // If repo is organization, check Member role - if err := repo.LoadOwner(ctx); err != nil { - return roleDescriptor, err - } - if repo.Owner.IsOrganization() { - if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil { - return roleDescriptor, err - } else if isMember { - roleDescriptor.RoleInRepo = issues_model.RoleRepoMember - return roleDescriptor, nil - } - } - - // If the poster is the collaborator of the repo - if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil { - return roleDescriptor, err - } else if isCollaborator { - roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator - return roleDescriptor, nil - } - - hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID) - if err != nil { - return roleDescriptor, err - } else if hasMergedPR { - roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor - } else if issue.IsPull { - // only display first time contributor in the first opening pull request - roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor - } - - return roleDescriptor, nil -} diff --git a/services/websocket/session.go b/services/websocket/session.go index 435204f4991d0..6214c449586ed 100644 --- a/services/websocket/session.go +++ b/services/websocket/session.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package websocket diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index d47e00233e9f5..384f180633b55 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -1,14 +1,26 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package websocket import ( + "fmt" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" "github.com/olahol/melody" ) +type websocketMessage struct { + Action string `json:"action"` + Data string `json:"data"` +} + +type subscribeMessageData struct { + URL string `json:"url"` +} + func HandleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) @@ -24,14 +36,35 @@ func HandleConnect(s *melody.Session) { // TODO: handle logouts } -func HandleMessage(s *melody.Session, msg []byte) { +func HandleMessage(s *melody.Session, _msg []byte) { data, err := getSessionData(s) if err != nil { return } - // TODO: only handle specific url message - data.onURL = string(msg) + msg := &websocketMessage{} + err = json.Unmarshal(_msg, msg) + if err != nil { + return + } + + switch msg.Action { + case "subscribe": + err := handleSubscribeMessage(data, msg.Data) + if err != nil { + return + } + } +} + +func handleSubscribeMessage(data *sessionData, _data any) error { + msgData, ok := _data.(*subscribeMessageData) + if !ok { + return fmt.Errorf("invalid message data") + } + + data.onURL = msgData.URL + return nil } func HandleDisconnect(s *melody.Session) { diff --git a/templates/repo/issue/view_content/comment.tmpl b/templates/repo/issue/view_content/comment.tmpl deleted file mode 100644 index eef5f2f2b6690..0000000000000 --- a/templates/repo/issue/view_content/comment.tmpl +++ /dev/null @@ -1,675 +0,0 @@ -{{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} -{{$RepoLink:=$RepoLink}} - - -{{if eq .Type 0}} -
- {{if .OriginalAuthor}} - - {{ctx.AvatarUtils.Avatar nil 40}} - - {{else}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} -
-
-
- {{if .OriginalAuthor}} - - {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if ctx.Repository.OriginalURL}} - - - ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - - {{else}} - {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} - - {{end}} -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not ctx.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID)}} - {{end}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" ctx "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and ctx.IsSigned (eq ctx.SignedUserID .PosterID))}} -
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" ctx "Attachments" .Attachments "Content" .RenderedContent}} - {{end}} -
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID) "Reactions" $reactions}} - {{end}} -
-
-{{else if eq .Type 1}} -
- {{svg "octicon-dot-fill"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 2}} -
- {{svg "octicon-circle-slash"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 28}} -
- {{svg "octicon-git-merge"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$link := printf "%s/commit/%s" ctx.Repository.Link (ctx.Issue.PullRequest.MergedCommitID|PathEscape)}} - {{if eq ctx.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha ctx.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" (ctx.BaseTarget|Escape)) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha ctx.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" (ctx.BaseTarget|Escape)) $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 3 5 6}} - {{$refFrom:= ""}} - {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} - {{end}} - {{$refTr := "repo.issues.ref_issue_from"}} - {{if .Issue.IsPull}} - {{$refTr = "repo.issues.ref_pull_from"}} - {{else if eq .RefAction 1}} - {{$refTr = "repo.issues.ref_closing_from"}} - {{else if eq .RefAction 2}} - {{$refTr = "repo.issues.ref_reopening_from"}} - {{end}} - {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if eq .RefAction 3}}{{end}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} - - {{if eq .RefAction 3}}{{end}} - - -
-{{else if eq .Type 4}} -
- {{svg "octicon-bookmark"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} - -
- {{svg "octicon-git-commit"}} - {{.Content | Str2html}} -
-
-{{else if eq .Type 7}} - {{if or .AddedLabels .RemovedLabels}} -
- {{svg "octicon-tag"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels ctx .AddedLabels $RepoLink) $createdStr | Safe}} - {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels ctx .RemovedLabels $RepoLink) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels ctx .AddedLabels $RepoLink) (RenderLabels ctx .RemovedLabels $RepoLink) $createdStr | Safe}} - {{end}} - -
- {{end}} -{{else if eq .Type 8}} -
- {{svg "octicon-milestone"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} - -
-{{else if and (eq .Type 9) (gt .AssigneeID 0)}} -
- {{svg "octicon-person"}} - {{if .RemovedAssignee}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - - {{else}} - {{template "shared/user/avatarlink" dict "user" .Assignee}} - - {{template "shared/user/authorlink" .Assignee}} - {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - - {{end}} -
-{{else if eq .Type 10}} -
- {{svg "octicon-pencil"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji ctx) (.NewTitle|RenderEmoji ctx) $createdStr | Safe}} - -
-{{else if eq .Type 11}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} - -
-{{else if eq .Type 12}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} - -
-{{else if eq .Type 13}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" ctx "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
-
-{{else if eq .Type 14}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" ctx "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
-
-{{else if eq .Type 15}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} - -
-{{else if eq .Type 16}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} - -
-{{else if eq .Type 17}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$parsedDeadline := StringUtils.Split .Content "|"}} - {{if eq (len $parsedDeadline) 2}} - {{$from := DateTime "long" (index $parsedDeadline 1)}} - {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} - {{end}} - -
-{{else if eq .Type 18}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} - -
-{{else if eq .Type 19}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} - - {{end}} -
-{{else if eq .Type 20}} -
- {{svg "octicon-package-dependents"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} - - {{if .DependentIssue}} - - {{end}} -
-{{else if eq .Type 22}} -
-
- {{if .OriginalAuthor}} - {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech - bubble. The condition depends on review type and for positive reviews whether - there is a comment element or not */}} - - {{ctx.AvatarUtils.Avatar .Poster 40}} - - {{end}} - {{svg (printf "octicon-%s" .Review.Type.Icon)}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if ctx.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - - {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} - {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{end}} - {{if .Review.Dismissed}} -
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
- {{end}} -
-
- {{if or .Content .Attachments}} -
-
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{if .OriginalAuthor}} - - {{svg (MigrationIcon ctx.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - - {{if ctx.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" (ctx.Repository.OriginalURL|Escape) (ctx.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - - {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} - -
-
- {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not ctx.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID)}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" ctx "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and ctx.IsSigned (eq ctx.SignedUserID .PosterID))}} - {{end}} -
-
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
{{.Content}}
-
- {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" ctx "Attachments" .Attachments "Content" .RenderedContent}} - {{end}} -
- {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" ctx "ActionURL" (printf "%s/comments/%d/reactions" $RepoLink .ID) "Reactions" $reactions}} - {{end}} -
-
- {{end}} - - {{if .Review.CodeComments}} -
- {{range $filename, $lines := .Review.CodeComments}} - {{range $line, $comms := $lines}} - {{template "repo/issue/view_content/conversation" dict "." ctx "comments" $comms}} - {{end}} - {{end}} -
- {{end}} -
-{{else if eq .Type 23}} -
- {{svg "octicon-lock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - {{if .Content}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} - - {{else}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} - - {{end}} -
-{{else if eq .Type 24}} -
- {{svg "octicon-key"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} - -
-{{else if eq .Type 25}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{.Poster.Name}} - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - -
-{{else if eq .Type 26}} -
- {{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} - -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - - {{.Content|Sec2Time}} - {{end}} -
-
-{{else if eq .Type 27}} -
- {{svg "octicon-eye"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if (gt .AssigneeID 0)}} - {{if .RemovedAssignee}} - {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} - {{end}} - {{else}} - - {{$teamName := "Ghost Team"}} - {{if .AssigneeTeam}} - {{$teamName = .AssigneeTeam.Name}} - {{end}} - {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} - {{end}} - {{end}} - -
-{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}} -
- {{svg "octicon-repo-push"}} - - {{template "shared/user/authorlink" .Poster}} - {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" (ctx.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) ((ctx.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) ((ctx.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} - {{end}} - - {{if and .IsForcePush ctx.Issue.PullRequest.BaseRepo.Name}} - - {{ctx.Locale.Tr "repo.issues.force_push_compare"}} - - {{end}} -
- {{if not .IsForcePush}} - {{template "repo/commits_list_small" dict "comment" . "root" $}} - {{end}} -{{else if eq .Type 30}} - {{if not ctx.UnitProjectsGlobalDisabled}} -
- {{svg "octicon-project"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{$oldProjectDisplayHtml := "Unknown Project"}} - {{if .OldProject}} - {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} - {{end}} - {{$newProjectDisplayHtml := "Unknown Project"}} - {{if .Project}} - {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} - {{end}} - {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} - {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} - {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} - {{end}} - -
- {{end}} -{{else if eq .Type 32}} -
-
- - - - {{svg "octicon-x" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{$reviewerName := ""}} - {{if eq .Review.OriginalAuthor ""}} - {{$reviewerName = .Review.Reviewer.Name}} - {{else}} - {{$reviewerName = .Review.OriginalAuthor}} - {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} - -
- {{if .Content}} -
-
-
- {{if gt .Poster.ID 0}} - - {{ctx.AvatarUtils.Avatar .Poster 24}} - - {{end}} - - {{ctx.Locale.Tr "action.review_dismissed_reason"}} - -
-
-
- {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - {{ctx.Locale.Tr "repo.issues.no_content"}} - {{end}} -
-
-
-
- {{end}} -
-{{else if eq .Type 33}} -
- {{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} - {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} - {{end}} - -
-{{else if or (eq .Type 34) (eq .Type 35)}} -
- {{svg "octicon-git-merge" 16}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} - -
-{{else if or (eq .Type 36) (eq .Type 37)}} -
- {{svg "octicon-pin" 16}} - {{template "shared/user/avatarlink" dict "user" .Poster}} - - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} - -
-{{end}} From ea06a25d7252c156e810fb5ebb70083d6efd5b53 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:19:50 +0100 Subject: [PATCH 15/44] cleanup --- routers/web/websocket/websocket.go | 24 +++++------------ services/websocket/issue_comment_notifier.go | 20 ++++++++------- services/websocket/notifier.go | 2 +- services/websocket/session.go | 7 ++--- services/websocket/websocket.go | 27 +++++++++++++------- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index 89776bf684a09..d03d615a44a7f 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -6,26 +6,16 @@ package websocket import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web" - notify_service "code.gitea.io/gitea/services/notify" "code.gitea.io/gitea/services/websocket" - - "github.com/olahol/melody" ) -var m *melody.Melody - func Init(r *web.Route) { - m = melody.New() - r.Any("/-/ws", webSocket) - m.HandleConnect(websocket.HandleConnect) - m.HandleMessage(websocket.HandleMessage) - m.HandleDisconnect(websocket.HandleDisconnect) - notify_service.RegisterNotifier(websocket.NewNotifier(m)) -} + m := websocket.Init() -func webSocket(ctx *context.Context) { - err := m.HandleRequest(ctx.Resp, ctx.Req) - if err != nil { - ctx.ServerError("HandleRequest", err) - } + r.Any("/-/ws", func(ctx *context.Context) { + err := m.HandleRequest(ctx.Resp, ctx.Req) + if err != nil { + ctx.ServerError("HandleRequest", err) + } + }) } diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go index a7205b4c7e4ed..ae0439c8435e0 100644 --- a/services/websocket/issue_comment_notifier.go +++ b/services/websocket/issue_comment_notifier.go @@ -8,33 +8,35 @@ import ( "fmt" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "github.com/olahol/melody" ) -func (n *websocketNotifier) filterIssueSessions(repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { +func (n *websocketNotifier) filterIssueSessions(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { return n.filterSessions(func(s *melody.Session, data *sessionData) bool { // if the user is watching the issue, they will get notifications if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { return false } - // if the repo is public, the user will get notifications - if !repo.IsPrivate { - return true - } - // if the repo is private, the user will get notifications if they have access to the repo + hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeNone) + if err != nil { + log.Error("Failed to check access: %v", err) + return false + } - // TODO: check if the user has access to the repo - return data.userID == issue.PosterID + return hasAccess }) } func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { - sessions := n.filterIssueSessions(c.Issue.Repo, c.Issue) + sessions := n.filterIssueSessions(ctx, c.Issue.Repo, c.Issue) for _, s := range sessions { msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 10e3c82f9ab42..6952d91056e6d 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -17,7 +17,7 @@ type websocketNotifier struct { } // NewNotifier create a new webhooksNotifier notifier -func NewNotifier(m *melody.Melody) notify_service.Notifier { +func newNotifier(m *melody.Melody) notify_service.Notifier { return &websocketNotifier{ m: m, rnd: templates.HTMLRenderer(), diff --git a/services/websocket/session.go b/services/websocket/session.go index 6214c449586ed..c13fa73142a87 100644 --- a/services/websocket/session.go +++ b/services/websocket/session.go @@ -8,12 +8,13 @@ import ( "net/url" "github.com/olahol/melody" + + user_model "code.gitea.io/gitea/models/user" ) type sessionData struct { - userID int64 - isSigned bool - onURL string + user *user_model.User + onURL string } func (d *sessionData) isOnURL(_u1 string) bool { diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 384f180633b55..d6a7fd07ee98c 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -6,12 +6,15 @@ package websocket import ( "fmt" + "github.com/olahol/melody" + "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" - - "github.com/olahol/melody" + notify_service "code.gitea.io/gitea/services/notify" ) +var m *melody.Melody + type websocketMessage struct { Action string `json:"action"` Data string `json:"data"` @@ -21,14 +24,21 @@ type subscribeMessageData struct { URL string `json:"url"` } -func HandleConnect(s *melody.Session) { +func Init() *melody.Melody { + m = melody.New() + m.HandleConnect(handleConnect) + m.HandleMessage(handleMessage) + m.HandleDisconnect(handleDisconnect) + notify_service.RegisterNotifier(newNotifier(m)) + return m +} + +func handleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) data := &sessionData{} - if ctx.IsSigned { - data.isSigned = true - data.userID = ctx.Doer.ID + data.user = ctx.Doer } s.Set("data", data) @@ -36,7 +46,7 @@ func HandleConnect(s *melody.Session) { // TODO: handle logouts } -func HandleMessage(s *melody.Session, _msg []byte) { +func handleMessage(s *melody.Session, _msg []byte) { data, err := getSessionData(s) if err != nil { return @@ -67,6 +77,5 @@ func handleSubscribeMessage(data *sessionData, _data any) error { return nil } -func HandleDisconnect(s *melody.Session) { - // TODO: Handle disconnect +func handleDisconnect(s *melody.Session) { } From 78b94ff85b678e1eb78fc0c3bf812ad2709334b2 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:50:49 +0100 Subject: [PATCH 16/44] finish --- services/websocket/websocket.go | 12 ++++++------ templates/base/head.tmpl | 2 +- web_src/js/htmx.js | 5 +++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index d6a7fd07ee98c..c77ff3d9469f6 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,8 +4,7 @@ package websocket import ( - "fmt" - + "github.com/mitchellh/mapstructure" "github.com/olahol/melody" "code.gitea.io/gitea/modules/context" @@ -17,7 +16,7 @@ var m *melody.Melody type websocketMessage struct { Action string `json:"action"` - Data string `json:"data"` + Data any `json:"data"` } type subscribeMessageData struct { @@ -68,9 +67,10 @@ func handleMessage(s *melody.Session, _msg []byte) { } func handleSubscribeMessage(data *sessionData, _data any) error { - msgData, ok := _data.(*subscribeMessageData) - if !ok { - return fmt.Errorf("invalid message data") + msgData := &subscribeMessageData{} + err := mapstructure.Decode(_data, &msgData) + if err != nil { + return err } data.onURL = msgData.URL diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 77eab677b2e7d..f9fb32a70d575 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -29,7 +29,7 @@ {{template "base/head_style" .}} {{template "custom/header" .}} - + {{ctx.DataRaceCheck $.Context}} {{template "custom/body_outer_pre" .}} diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index ceecb11b47a49..fc245abc06a6b 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -20,3 +20,8 @@ document.body.addEventListener('htmx:responseError', (event) => { // TODO: add translations showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); }); + +document.body.addEventListener('htmx:wsOpen', (evt) => { + const socket = evt.detail.socketWrapper; + socket.send(JSON.stringify({action: 'subscribe', data: {url: window.location.href}})); +}); From c5959169dd52cecb969a9b9b3265b95473899abb Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:55:15 +0100 Subject: [PATCH 17/44] fix permission --- services/websocket/issue_comment_notifier.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go index ae0439c8435e0..062dcc1f5739d 100644 --- a/services/websocket/issue_comment_notifier.go +++ b/services/websocket/issue_comment_notifier.go @@ -24,8 +24,8 @@ func (n *websocketNotifier) filterIssueSessions(ctx context.Context, repo *repo_ return false } - // if the repo is private, the user will get notifications if they have access to the repo - hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeNone) + // the user will get notifications if they have access to the repos issues + hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeRead) if err != nil { log.Error("Failed to check access: %v", err) return false From b805631ea2415fa447d5914b0a5782b6279e179f Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:09:00 +0100 Subject: [PATCH 18/44] sort imports --- services/websocket/session.go | 4 ++-- services/websocket/websocket.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/websocket/session.go b/services/websocket/session.go index c13fa73142a87..154a78ef21ed5 100644 --- a/services/websocket/session.go +++ b/services/websocket/session.go @@ -7,9 +7,9 @@ import ( "fmt" "net/url" - "github.com/olahol/melody" - user_model "code.gitea.io/gitea/models/user" + + "github.com/olahol/melody" ) type sessionData struct { diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index c77ff3d9469f6..e0036d774d612 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,12 +4,12 @@ package websocket import ( - "github.com/mitchellh/mapstructure" - "github.com/olahol/melody" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" notify_service "code.gitea.io/gitea/services/notify" + + "github.com/mitchellh/mapstructure" + "github.com/olahol/melody" ) var m *melody.Melody From 7de741c8383b0e09f248323abc06bc8c3a8a7a2e Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:14:43 +0100 Subject: [PATCH 19/44] use custom create-websocket --- web_src/js/htmx.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index fc245abc06a6b..c2e4f453d32b8 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -21,6 +21,13 @@ document.body.addEventListener('htmx:responseError', (event) => { showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); }); +// eslint-disable-next-line no-import-assign +htmx.createWebSocket = (url) => { + // TODO: reuse websocket from shared webworker + const sock = new WebSocket(url, []); + sock.binaryType = htmx.config.wsBinaryType; + return sock; +}; document.body.addEventListener('htmx:wsOpen', (evt) => { const socket = evt.detail.socketWrapper; socket.send(JSON.stringify({action: 'subscribe', data: {url: window.location.href}})); From c1512d3d0301a0235a2dc001a473cc82740eda7f Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:06:02 +0100 Subject: [PATCH 20/44] rename event to e --- web_src/js/htmx.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index c2e4f453d32b8..0f27321045a42 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -1,24 +1,28 @@ -import * as htmx from 'htmx.org'; -import {showErrorToast} from './modules/toast.js'; -import 'htmx.org/dist/ext/ws.js'; +import * as htmx from "htmx.org"; +import { showErrorToast } from "./modules/toast.js"; +import "htmx.org/dist/ext/ws.js"; // https://github.com/bigskysoftware/idiomorph#htmx -import 'idiomorph/dist/idiomorph-ext.js'; +import "idiomorph/dist/idiomorph-ext.js"; // https://htmx.org/reference/#config -htmx.config.requestClass = 'is-loading'; +htmx.config.requestClass = "is-loading"; htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError -document.body.addEventListener('htmx:sendError', (event) => { +document.body.addEventListener("htmx:sendError", (event) => { // TODO: add translations - showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); + showErrorToast( + `Network error when calling ${event.detail.requestConfig.path}` + ); }); // https://htmx.org/events/#htmx:responseError -document.body.addEventListener('htmx:responseError', (event) => { +document.body.addEventListener("htmx:responseError", (event) => { // TODO: add translations - showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); + showErrorToast( + `Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}` + ); }); // eslint-disable-next-line no-import-assign @@ -26,9 +30,12 @@ htmx.createWebSocket = (url) => { // TODO: reuse websocket from shared webworker const sock = new WebSocket(url, []); sock.binaryType = htmx.config.wsBinaryType; + g; return sock; }; -document.body.addEventListener('htmx:wsOpen', (evt) => { - const socket = evt.detail.socketWrapper; - socket.send(JSON.stringify({action: 'subscribe', data: {url: window.location.href}})); +document.body.addEventListener("htmx:wsOpen", (e) => { + const socket = e.detail.socketWrapper; + socket.send( + JSON.stringify({ action: "subscribe", data: { url: window.location.href } }) + ); }); From 201fd83233489329c77f5e22bdb59bd8055f2801 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:07:05 +0100 Subject: [PATCH 21/44] rename event to e --- web_src/js/htmx.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 0f27321045a42..7b61084bfb30b 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -10,18 +10,16 @@ htmx.config.requestClass = "is-loading"; htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError -document.body.addEventListener("htmx:sendError", (event) => { +document.body.addEventListener("htmx:sendError", (e) => { // TODO: add translations - showErrorToast( - `Network error when calling ${event.detail.requestConfig.path}` - ); + showErrorToast(`Network error when calling ${e.detail.requestConfig.path}`); }); // https://htmx.org/events/#htmx:responseError -document.body.addEventListener("htmx:responseError", (event) => { +document.body.addEventListener("htmx:responseError", (e) => { // TODO: add translations showErrorToast( - `Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}` + `Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}` ); }); From 39421d6684223167446928334b49d581d6f59067 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:35:52 +0100 Subject: [PATCH 22/44] undo prettier --- web_src/js/htmx.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 7b61084bfb30b..9e72890876453 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -1,39 +1,42 @@ -import * as htmx from "htmx.org"; -import { showErrorToast } from "./modules/toast.js"; -import "htmx.org/dist/ext/ws.js"; +import * as htmx from 'htmx.org'; +import { showErrorToast } from './modules/toast.js'; +import 'htmx.org/dist/ext/ws.js'; // https://github.com/bigskysoftware/idiomorph#htmx -import "idiomorph/dist/idiomorph-ext.js"; +import 'idiomorph/dist/idiomorph-ext.js'; // https://htmx.org/reference/#config -htmx.config.requestClass = "is-loading"; +htmx.config.requestClass = 'is-loading'; htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError -document.body.addEventListener("htmx:sendError", (e) => { +document.body.addEventListener('htmx:sendError', (e) => { // TODO: add translations showErrorToast(`Network error when calling ${e.detail.requestConfig.path}`); }); // https://htmx.org/events/#htmx:responseError -document.body.addEventListener("htmx:responseError", (e) => { +document.body.addEventListener('htmx:responseError', (e) => { // TODO: add translations showErrorToast( `Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}` ); }); -// eslint-disable-next-line no-import-assign +let webSocket; + +// TODO: move websocket creation to shared webworker htmx.createWebSocket = (url) => { - // TODO: reuse websocket from shared webworker - const sock = new WebSocket(url, []); - sock.binaryType = htmx.config.wsBinaryType; - g; - return sock; + if (![0, 1].includes(webSocket?.readyState)) return webSocket; + const ws = new WebSocket(url, []); + ws.binaryType = htmx.config.wsBinaryType; + webSocket = ws; + return ws; }; -document.body.addEventListener("htmx:wsOpen", (e) => { + +document.body.addEventListener('htmx:wsOpen', (e) => { const socket = e.detail.socketWrapper; socket.send( - JSON.stringify({ action: "subscribe", data: { url: window.location.href } }) + JSON.stringify({ action: 'subscribe', data: { url: window.location.href } }) ); }); From 634834b34efd253115a4277debd20292f98a15ba Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:37:21 +0100 Subject: [PATCH 23/44] undo more prettier changes --- web_src/js/htmx.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 9e72890876453..7cd6c0f3219e6 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -1,5 +1,5 @@ import * as htmx from 'htmx.org'; -import { showErrorToast } from './modules/toast.js'; +import {showErrorToast} from './modules/toast.js'; import 'htmx.org/dist/ext/ws.js'; // https://github.com/bigskysoftware/idiomorph#htmx @@ -18,9 +18,7 @@ document.body.addEventListener('htmx:sendError', (e) => { // https://htmx.org/events/#htmx:responseError document.body.addEventListener('htmx:responseError', (e) => { // TODO: add translations - showErrorToast( - `Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}` - ); + showErrorToast(`Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}`); }); let webSocket; From 94bcdba386dfd880fded43114dd898eec991e2ec Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:34:44 +0100 Subject: [PATCH 24/44] fix imports --- routers/web/websocket/websocket.go | 2 +- services/websocket/websocket.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index d03d615a44a7f..cc484b7024ee9 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -4,8 +4,8 @@ package websocket import ( - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/websocket" ) diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index e0036d774d612..ecf64b427486a 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,8 +4,8 @@ package websocket import ( - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/services/context" notify_service "code.gitea.io/gitea/services/notify" "github.com/mitchellh/mapstructure" From 1602afdd3e28b2af675dd2d05203b456e4416535 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 4 Mar 2024 22:22:07 +0100 Subject: [PATCH 25/44] remove websocket init for now, fix lint --- web_src/js/htmx.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 7cd6c0f3219e6..54f20fb47fd66 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -21,20 +21,11 @@ document.body.addEventListener('htmx:responseError', (e) => { showErrorToast(`Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}`); }); -let webSocket; - -// TODO: move websocket creation to shared webworker -htmx.createWebSocket = (url) => { - if (![0, 1].includes(webSocket?.readyState)) return webSocket; - const ws = new WebSocket(url, []); - ws.binaryType = htmx.config.wsBinaryType; - webSocket = ws; - return ws; -}; +// TODO: move websocket creation to shared webworker by overriding htmx.createWebSocket document.body.addEventListener('htmx:wsOpen', (e) => { const socket = e.detail.socketWrapper; socket.send( - JSON.stringify({ action: 'subscribe', data: { url: window.location.href } }) + JSON.stringify({action: 'subscribe', data: {url: window.location.href}}) ); }); From f2f97a2510435059c0d4cce986f2f95066673820 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 7 Mar 2024 19:17:47 +0100 Subject: [PATCH 26/44] Tweak comment --- web_src/js/htmx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 54f20fb47fd66..3589dad6baabf 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -21,7 +21,7 @@ document.body.addEventListener('htmx:responseError', (e) => { showErrorToast(`Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}`); }); -// TODO: move websocket creation to shared webworker by overriding htmx.createWebSocket +// TODO: move websocket creation to SharedWorker by overriding htmx.createWebSocket document.body.addEventListener('htmx:wsOpen', (e) => { const socket = e.detail.socketWrapper; From 4d75e286d9fa5272d1f323269daccb55f73447e6 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:06:39 +0100 Subject: [PATCH 27/44] add return --- routers/web/websocket/websocket.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index cc484b7024ee9..513b84a58ad02 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -16,6 +16,7 @@ func Init(r *web.Route) { err := m.HandleRequest(ctx.Resp, ctx.Req) if err != nil { ctx.ServerError("HandleRequest", err) + return } }) } From cf23cd6467fd82cb5d02be1b7299e62d03a97372 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:13:29 +0100 Subject: [PATCH 28/44] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ae99f1d8dccd2..150c7f99591c4 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 github.com/minio/minio-go/v7 v7.0.66 + github.com/mitchellh/mapstructure v1.5.0 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 @@ -233,7 +234,6 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect From 372faaa587188d8752f8aa78f9cd621e42005be2 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:42:21 +0100 Subject: [PATCH 29/44] add melody pubsub --- .air.toml | 4 +-- services/pubsub/memory.go | 59 +++++++++++++++++++++++++++++++++ services/pubsub/memory_test.go | 47 ++++++++++++++++++++++++++ services/pubsub/types.go | 20 +++++++++++ services/websocket/notifier.go | 3 +- services/websocket/websocket.go | 5 ++- 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 services/pubsub/memory.go create mode 100644 services/pubsub/memory_test.go create mode 100644 services/pubsub/types.go diff --git a/.air.toml b/.air.toml index d13f8c4f99577..b9aeef3199846 100644 --- a/.air.toml +++ b/.air.toml @@ -5,9 +5,9 @@ tmp_dir = ".air" cmd = "make --no-print-directory backend" bin = "gitea" delay = 1000 -include_ext = ["go", "tmpl"] +include_ext = ["go", "tmpl", "css", "js"] include_file = ["main.go"] -include_dir = ["cmd", "models", "modules", "options", "routers", "services"] +include_dir = ["cmd", "models", "modules", "options", "public", "routers", "services", "templates"] exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"] exclude_regex = ["_test.go$", "_gen.go$"] stop_on_error = true diff --git a/services/pubsub/memory.go b/services/pubsub/memory.go new file mode 100644 index 0000000000000..f105376c8887b --- /dev/null +++ b/services/pubsub/memory.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import ( + "context" + "sync" +) + +type Memory struct { + sync.Mutex + + topics map[string]map[*Subscriber]struct{} +} + +// New creates an in-memory publisher. +func NewMemory() Broker { + return &Memory{ + topics: make(map[string]map[*Subscriber]struct{}), + } +} + +func (p *Memory) Publish(_ context.Context, message Message) { + p.Lock() + + topic, ok := p.topics[message.Topic] + if !ok { + p.Unlock() + return + } + + for s := range topic { + go (*s)(message) + } + p.Unlock() +} + +func (p *Memory) Subscribe(c context.Context, topic string, subscriber Subscriber) { + // Subscribe + p.Lock() + _, ok := p.topics[topic] + if !ok { + p.topics[topic] = make(map[*Subscriber]struct{}) + } + p.topics[topic][&subscriber] = struct{}{} + p.Unlock() + + // Wait for context to be done + <-c.Done() + + // Unsubscribe + p.Lock() + delete(p.topics[topic], &subscriber) + if len(p.topics[topic]) == 0 { + delete(p.topics, topic) + } + p.Unlock() +} diff --git a/services/pubsub/memory_test.go b/services/pubsub/memory_test.go new file mode 100644 index 0000000000000..1d9719077689c --- /dev/null +++ b/services/pubsub/memory_test.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPubsub(t *testing.T) { + var ( + wg sync.WaitGroup + + testMessage = Message{ + Data: []byte("test"), + Topic: "hello-world", + } + ) + + ctx, cancel := context.WithCancelCause( + context.Background(), + ) + + broker := NewMemory() + go func() { + broker.Subscribe(ctx, "hello-world", func(message Message) { assert.Equal(t, testMessage, message); wg.Done() }) + }() + go func() { + broker.Subscribe(ctx, "hello-world", func(_ Message) { wg.Done() }) + }() + + // Wait a bit for the subscriptions to be registered + <-time.After(100 * time.Millisecond) + + wg.Add(2) + go func() { + broker.Publish(ctx, testMessage) + }() + + wg.Wait() + cancel(nil) +} diff --git a/services/pubsub/types.go b/services/pubsub/types.go new file mode 100644 index 0000000000000..afc185654fc65 --- /dev/null +++ b/services/pubsub/types.go @@ -0,0 +1,20 @@ +package pubsub + +import "context" + +// Message defines a published message. +type Message struct { + // Data is the actual data in the entry. + Data []byte `json:"data"` + + // Topic is the topic of the message. + Topic string `json:"topic"` +} + +// Subscriber receives published messages. +type Subscriber func(Message) + +type Broker interface { + Publish(c context.Context, message Message) + Subscribe(c context.Context, topic string, subscriber Subscriber) +} diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 6952d91056e6d..c83fba7eb6c5d 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -7,6 +7,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" + "code.gitea.io/gitea/services/pubsub" "github.com/olahol/melody" ) @@ -17,7 +18,7 @@ type websocketNotifier struct { } // NewNotifier create a new webhooksNotifier notifier -func newNotifier(m *melody.Melody) notify_service.Notifier { +func newNotifier(m *melody.Melody, pubsub pubsub.Broker) notify_service.Notifier { return &websocketNotifier{ m: m, rnd: templates.HTMLRenderer(), diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index ecf64b427486a..37e3d66640e51 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -7,6 +7,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/services/context" notify_service "code.gitea.io/gitea/services/notify" + "code.gitea.io/gitea/services/pubsub" "github.com/mitchellh/mapstructure" "github.com/olahol/melody" @@ -28,7 +29,9 @@ func Init() *melody.Melody { m.HandleConnect(handleConnect) m.HandleMessage(handleMessage) m.HandleDisconnect(handleDisconnect) - notify_service.RegisterNotifier(newNotifier(m)) + + broker := pubsub.NewMemory() // TODO: allow for other pubsub implementations + notify_service.RegisterNotifier(newNotifier(m, broker)) return m } From d86c1dc23a4e7f094cc51d8e10af66348333353d Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 11 Mar 2024 11:09:49 -0400 Subject: [PATCH 30/44] Update services/pubsub/types.go --- services/pubsub/types.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/pubsub/types.go b/services/pubsub/types.go index afc185654fc65..143ffceb0338d 100644 --- a/services/pubsub/types.go +++ b/services/pubsub/types.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package pubsub import "context" From 1328b2b9fed9078bfb926175ae9399d2efb24529 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:53:01 +0100 Subject: [PATCH 31/44] undo --- .air.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.air.toml b/.air.toml index 096752aef5079..de97bd8b298be 100644 --- a/.air.toml +++ b/.air.toml @@ -5,7 +5,7 @@ tmp_dir = ".air" cmd = "make --no-print-directory backend" bin = "gitea" delay = 1000 -include_ext = ["go", "tmpl", "css", "js"] +include_ext = ["go", "tmpl"] include_file = ["main.go"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"] exclude_dir = [ From 40fc078515662d25b695f7c9514e9cae2875a5e9 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:30:02 +0100 Subject: [PATCH 32/44] tidy --- assets/go-licenses.json | 10 ++++++++++ go.mod | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index be9022b69465c..f90828b5b249f 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -599,6 +599,11 @@ "path": "github.com/gorilla/sessions/LICENSE", "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/gorilla/websocket", + "path": "github.com/gorilla/websocket/LICENSE", + "licenseText": "Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/hashicorp/go-cleanhttp", "path": "github.com/hashicorp/go-cleanhttp/LICENSE", @@ -809,6 +814,11 @@ "path": "github.com/nwaples/rardecode/LICENSE", "licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/olahol/melody", + "path": "github.com/olahol/melody/LICENSE", + "licenseText": "Copyright (c) 2015 Ola Holmström. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/olekukonko/tablewriter", "path": "github.com/olekukonko/tablewriter/LICENSE.md", diff --git a/go.mod b/go.mod index 168f9d75afd52..371b63b761f7b 100644 --- a/go.mod +++ b/go.mod @@ -77,8 +77,8 @@ require ( github.com/meilisearch/meilisearch-go v0.26.2 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 - github.com/mitchellh/mapstructure v1.5.0 github.com/minio/minio-go/v7 v7.0.69 + github.com/mitchellh/mapstructure v1.5.0 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 From c6abd32e0d14063c5b3cf48c36fa2f8ceda57b76 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:37:29 +0100 Subject: [PATCH 33/44] fmt --- services/websocket/issue_comment_notifier.go | 1 + services/websocket/notifier.go | 1 + 2 files changed, 2 insertions(+) diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go index 062dcc1f5739d..9e0b060b87c70 100644 --- a/services/websocket/issue_comment_notifier.go +++ b/services/websocket/issue_comment_notifier.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "github.com/olahol/melody" ) diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 6952d91056e6d..3fef637436e20 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -7,6 +7,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" + "github.com/olahol/melody" ) From 43ad9f5173bf61fa2f9cfda782ced62d90085ba2 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 3 Apr 2024 08:35:33 +0200 Subject: [PATCH 34/44] add comma --- web_src/js/htmx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 3589dad6baabf..ae4bb5780441e 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -26,6 +26,6 @@ document.body.addEventListener('htmx:responseError', (e) => { document.body.addEventListener('htmx:wsOpen', (e) => { const socket = e.detail.socketWrapper; socket.send( - JSON.stringify({action: 'subscribe', data: {url: window.location.href}}) + JSON.stringify({action: 'subscribe', data: {url: window.location.href}}), ); }); From 602a42a70e3a69b576d57ba3f31639f75beed76c Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:22:35 +0200 Subject: [PATCH 35/44] adjust websocket --- services/pubsub/memory.go | 59 ++++++++++++++++++++ services/pubsub/memory_test.go | 47 ++++++++++++++++ services/pubsub/types.go | 23 ++++++++ services/websocket/issue_comment_notifier.go | 43 ++++---------- services/websocket/notifier.go | 29 ++-------- services/websocket/websocket.go | 52 ++++++++++++++--- 6 files changed, 187 insertions(+), 66 deletions(-) create mode 100644 services/pubsub/memory.go create mode 100644 services/pubsub/memory_test.go create mode 100644 services/pubsub/types.go diff --git a/services/pubsub/memory.go b/services/pubsub/memory.go new file mode 100644 index 0000000000000..f105376c8887b --- /dev/null +++ b/services/pubsub/memory.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import ( + "context" + "sync" +) + +type Memory struct { + sync.Mutex + + topics map[string]map[*Subscriber]struct{} +} + +// New creates an in-memory publisher. +func NewMemory() Broker { + return &Memory{ + topics: make(map[string]map[*Subscriber]struct{}), + } +} + +func (p *Memory) Publish(_ context.Context, message Message) { + p.Lock() + + topic, ok := p.topics[message.Topic] + if !ok { + p.Unlock() + return + } + + for s := range topic { + go (*s)(message) + } + p.Unlock() +} + +func (p *Memory) Subscribe(c context.Context, topic string, subscriber Subscriber) { + // Subscribe + p.Lock() + _, ok := p.topics[topic] + if !ok { + p.topics[topic] = make(map[*Subscriber]struct{}) + } + p.topics[topic][&subscriber] = struct{}{} + p.Unlock() + + // Wait for context to be done + <-c.Done() + + // Unsubscribe + p.Lock() + delete(p.topics[topic], &subscriber) + if len(p.topics[topic]) == 0 { + delete(p.topics, topic) + } + p.Unlock() +} diff --git a/services/pubsub/memory_test.go b/services/pubsub/memory_test.go new file mode 100644 index 0000000000000..1d9719077689c --- /dev/null +++ b/services/pubsub/memory_test.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPubsub(t *testing.T) { + var ( + wg sync.WaitGroup + + testMessage = Message{ + Data: []byte("test"), + Topic: "hello-world", + } + ) + + ctx, cancel := context.WithCancelCause( + context.Background(), + ) + + broker := NewMemory() + go func() { + broker.Subscribe(ctx, "hello-world", func(message Message) { assert.Equal(t, testMessage, message); wg.Done() }) + }() + go func() { + broker.Subscribe(ctx, "hello-world", func(_ Message) { wg.Done() }) + }() + + // Wait a bit for the subscriptions to be registered + <-time.After(100 * time.Millisecond) + + wg.Add(2) + go func() { + broker.Publish(ctx, testMessage) + }() + + wg.Wait() + cancel(nil) +} diff --git a/services/pubsub/types.go b/services/pubsub/types.go new file mode 100644 index 0000000000000..143ffceb0338d --- /dev/null +++ b/services/pubsub/types.go @@ -0,0 +1,23 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import "context" + +// Message defines a published message. +type Message struct { + // Data is the actual data in the entry. + Data []byte `json:"data"` + + // Topic is the topic of the message. + Topic string `json:"topic"` +} + +// Subscriber receives published messages. +type Subscriber func(Message) + +type Broker interface { + Publish(c context.Context, message Message) + Subscribe(c context.Context, topic string, subscriber Subscriber) +} diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go index 9e0b060b87c70..31052fa025f12 100644 --- a/services/websocket/issue_comment_notifier.go +++ b/services/websocket/issue_comment_notifier.go @@ -5,45 +5,22 @@ package websocket import ( "context" + "encoding/json" "fmt" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" - - "github.com/olahol/melody" + "code.gitea.io/gitea/services/pubsub" ) -func (n *websocketNotifier) filterIssueSessions(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { - return n.filterSessions(func(s *melody.Session, data *sessionData) bool { - // if the user is watching the issue, they will get notifications - if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { - return false - } - - // the user will get notifications if they have access to the repos issues - hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeRead) - if err != nil { - log.Error("Failed to check access: %v", err) - return false - } - - return hasAccess - }) -} - func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { - sessions := n.filterIssueSessions(ctx, c.Issue.Repo, c.Issue) - - for _, s := range sessions { - msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) - err := s.Write([]byte(msg)) - if err != nil { - log.Error("Failed to write to session: %v", err) - } + d, err := json.Marshal(c) + if err != nil { + return } + + n.pubsub.Publish(ctx, pubsub.Message{ + Data: d, + Topic: fmt.Sprintf("repo:%s/%s", c.RefRepo.OwnerName, c.RefRepo.Name), + }) } diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 3fef637436e20..84535c8bb7e49 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -4,17 +4,18 @@ package websocket import ( - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" + "code.gitea.io/gitea/services/pubsub" "github.com/olahol/melody" ) type websocketNotifier struct { notify_service.NullNotifier - m *melody.Melody - rnd *templates.HTMLRender + m *melody.Melody + rnd *templates.HTMLRender + pubsub pubsub.Broker } // NewNotifier create a new webhooksNotifier notifier @@ -29,25 +30,3 @@ func newNotifier(m *melody.Melody) notify_service.Notifier { // htmxUpdateElement = "
%s
" var htmxRemoveElement = "
" - -func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData) bool) []*melody.Session { - sessions, err := n.m.Sessions() - if err != nil { - log.Error("Failed to get sessions: %v", err) - return nil - } - - _sessions := make([]*melody.Session, 0, len(sessions)) - for _, s := range sessions { - data, err := getSessionData(s) - if err != nil { - continue - } - - if fn(s, data) { - _sessions = append(_sessions, s) - } - } - - return _sessions -} diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index ecf64b427486a..41e280c6a11d4 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,9 +4,17 @@ package websocket import ( + goContext "context" + "fmt" + + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/services/context" notify_service "code.gitea.io/gitea/services/notify" + "code.gitea.io/gitea/services/pubsub" "github.com/mitchellh/mapstructure" "github.com/olahol/melody" @@ -20,19 +28,26 @@ type websocketMessage struct { } type subscribeMessageData struct { - URL string `json:"url"` + Repo string `json:"repo"` } func Init() *melody.Melody { m = melody.New() - m.HandleConnect(handleConnect) - m.HandleMessage(handleMessage) + hub := &hub{ + pubsub: pubsub.NewMemory(), + } + m.HandleConnect(hub.handleConnect) + m.HandleMessage(hub.handleMessage) m.HandleDisconnect(handleDisconnect) notify_service.RegisterNotifier(newNotifier(m)) return m } -func handleConnect(s *melody.Session) { +type hub struct { + pubsub pubsub.Broker +} + +func (h *hub) handleConnect(s *melody.Session) { ctx := context.GetWebContext(s.Request) data := &sessionData{} @@ -45,7 +60,7 @@ func handleConnect(s *melody.Session) { // TODO: handle logouts } -func handleMessage(s *melody.Session, _msg []byte) { +func (h *hub) handleMessage(s *melody.Session, _msg []byte) { data, err := getSessionData(s) if err != nil { return @@ -59,21 +74,42 @@ func handleMessage(s *melody.Session, _msg []byte) { switch msg.Action { case "subscribe": - err := handleSubscribeMessage(data, msg.Data) + err := h.handleSubscribeMessage(s, data, msg.Data) if err != nil { return } } } -func handleSubscribeMessage(data *sessionData, _data any) error { +func (h *hub) handleSubscribeMessage(s *melody.Session, data *sessionData, _data any) error { msgData := &subscribeMessageData{} err := mapstructure.Decode(_data, &msgData) if err != nil { return err } - data.onURL = msgData.URL + ctx := goContext.Background() // TODO: use proper context + h.pubsub.Subscribe(ctx, msgData.Repo, func(msg pubsub.Message) { + if data.user != nil { + return + } + + // TODO: check permissions + hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeRead) + if err != nil { + log.Error("Failed to check access: %v", err) + return + } + + if !hasAccess { + return + } + + // TODO: check the actual data received from pubsub and send it correctly to the client + d := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) + _ = s.Write([]byte(d)) + }) + return nil } From 026284644693004171b61b237f5f73e88c0f25b3 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:37:35 +0200 Subject: [PATCH 36/44] adjust --- services/pubsub/memory.go | 4 ++-- services/pubsub/memory_test.go | 5 ++--- services/pubsub/types.go | 5 +---- services/websocket/issue_comment_notifier.go | 6 +++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/services/pubsub/memory.go b/services/pubsub/memory.go index f105376c8887b..0c9e6e9d1e74c 100644 --- a/services/pubsub/memory.go +++ b/services/pubsub/memory.go @@ -21,10 +21,10 @@ func NewMemory() Broker { } } -func (p *Memory) Publish(_ context.Context, message Message) { +func (p *Memory) Publish(_ context.Context, _topic string, message Message) { p.Lock() - topic, ok := p.topics[message.Topic] + topic, ok := p.topics[_topic] if !ok { p.Unlock() return diff --git a/services/pubsub/memory_test.go b/services/pubsub/memory_test.go index 1d9719077689c..3dc153e2746b4 100644 --- a/services/pubsub/memory_test.go +++ b/services/pubsub/memory_test.go @@ -17,8 +17,7 @@ func TestPubsub(t *testing.T) { wg sync.WaitGroup testMessage = Message{ - Data: []byte("test"), - Topic: "hello-world", + Data: []byte("test"), } ) @@ -39,7 +38,7 @@ func TestPubsub(t *testing.T) { wg.Add(2) go func() { - broker.Publish(ctx, testMessage) + broker.Publish(ctx, "hello-world", testMessage) }() wg.Wait() diff --git a/services/pubsub/types.go b/services/pubsub/types.go index 143ffceb0338d..18a35e0963e06 100644 --- a/services/pubsub/types.go +++ b/services/pubsub/types.go @@ -9,15 +9,12 @@ import "context" type Message struct { // Data is the actual data in the entry. Data []byte `json:"data"` - - // Topic is the topic of the message. - Topic string `json:"topic"` } // Subscriber receives published messages. type Subscriber func(Message) type Broker interface { - Publish(c context.Context, message Message) + Publish(c context.Context, topic string, message Message) Subscribe(c context.Context, topic string, subscriber Subscriber) } diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/issue_comment_notifier.go index 31052fa025f12..e8bafaa4b2bba 100644 --- a/services/websocket/issue_comment_notifier.go +++ b/services/websocket/issue_comment_notifier.go @@ -19,8 +19,8 @@ func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model. return } - n.pubsub.Publish(ctx, pubsub.Message{ - Data: d, - Topic: fmt.Sprintf("repo:%s/%s", c.RefRepo.OwnerName, c.RefRepo.Name), + topic := fmt.Sprintf("repo:%s/%s", c.RefRepo.OwnerName, c.RefRepo.Name) + n.pubsub.Publish(ctx, topic, pubsub.Message{ + Data: d, }) } From 85cf27e9cff9764134b0f28cd74ff74e0ad95d83 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Thu, 19 Sep 2024 18:54:22 +0200 Subject: [PATCH 37/44] adjsut --- package-lock.json | 6 ++++++ package.json | 1 + web_src/js/htmx.ts | 10 +++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0e83b60ec24f..4f5f89d79ad0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", + "htmx-ext-ws": "2.0.1", "htmx.org": "2.0.2", "idiomorph": "0.3.0", "jquery": "3.7.1", @@ -10547,6 +10548,11 @@ "entities": "^4.4.0" } }, + "node_modules/htmx-ext-ws": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/htmx-ext-ws/-/htmx-ext-ws-2.0.1.tgz", + "integrity": "sha512-gddFXSzzHH9I7RWm93pGfGIKGPUo2lDtLiK8uoPn8mp/ivC0KQx4LAuQdXdg13S78lBD40Fs/mLEM/xoJCeJxQ==" + }, "node_modules/htmx.org": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.2.tgz", diff --git a/package.json b/package.json index d188e99a30d8b..a899aa10b7ad6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", + "htmx-ext-ws": "2.0.1", "htmx.org": "2.0.2", "idiomorph": "0.3.0", "jquery": "3.7.1", diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts index a95142d901fe6..1ab103a71be58 100644 --- a/web_src/js/htmx.ts +++ b/web_src/js/htmx.ts @@ -1,6 +1,6 @@ import {showErrorToast} from './modules/toast.ts'; import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx -import 'htmx.org/dist/ext/ws.js'; +import 'htmx-ext-ws'; import type {HtmxResponseInfo} from 'htmx.org'; type HtmxEvent = Event & {detail: HtmxResponseInfo}; @@ -12,19 +12,19 @@ window.htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => { // TODO: add translations - showErrorToast(`Network error when calling ${e.detail.requestConfig.path}`); + showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); }); // https://htmx.org/events/#htmx:responseError document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => { // TODO: add translations - showErrorToast(`Error ${e.detail.xhr.status} when calling ${e.detail.requestConfig.path}`); + showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); }); // TODO: move websocket creation to SharedWorker by overriding htmx.createWebSocket -document.body.addEventListener('htmx:wsOpen', (e) => { - const socket = e.detail.socketWrapper; +document.body.addEventListener('htmx:wsOpen', (event: HtmxEvent) => { + const socket = event.detail.socketWrapper; socket.send( JSON.stringify({action: 'subscribe', data: {url: window.location.href}}) ); From abcc6094c55e221d31c3d7da94095b45fdfc7652 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Thu, 19 Sep 2024 19:20:41 +0200 Subject: [PATCH 38/44] fix --- web_src/js/htmx.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts index 1ab103a71be58..1d2c032b8b196 100644 --- a/web_src/js/htmx.ts +++ b/web_src/js/htmx.ts @@ -3,7 +3,10 @@ import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/i import 'htmx-ext-ws'; import type {HtmxResponseInfo} from 'htmx.org'; -type HtmxEvent = Event & {detail: HtmxResponseInfo}; +type SocketWrapper = { + send: (msg: string) => void; +}; +type HtmxEvent = Event & {detail: HtmxResponseInfo & {socketWrapper: SocketWrapper}}; // https://htmx.org/reference/#config window.htmx.config.requestClass = 'is-loading'; @@ -26,6 +29,6 @@ document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => { document.body.addEventListener('htmx:wsOpen', (event: HtmxEvent) => { const socket = event.detail.socketWrapper; socket.send( - JSON.stringify({action: 'subscribe', data: {url: window.location.href}}) + JSON.stringify({action: 'subscribe', data: {url: window.location.href}}), ); }); From 597fac4b0c321a427561a37139b4d34515c2402e Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 Sep 2024 09:03:42 +0200 Subject: [PATCH 39/44] add pubsub --- routers/web/websocket/websocket.go | 2 +- services/pubsub/memory.go | 11 +++--- services/pubsub/memory_test.go | 15 ++++---- services/pubsub/notifier.go | 56 ++++++++++++++++++++++++++++++ services/pubsub/types.go | 13 ++----- services/websocket/notifier.go | 15 ++++---- services/websocket/websocket.go | 47 ++++++++++++++++++++++--- 7 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 services/pubsub/notifier.go diff --git a/routers/web/websocket/websocket.go b/routers/web/websocket/websocket.go index 513b84a58ad02..ba4569fb6bb40 100644 --- a/routers/web/websocket/websocket.go +++ b/routers/web/websocket/websocket.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/services/websocket" ) -func Init(r *web.Route) { +func Init(r *web.Router) { m := websocket.Init() r.Any("/-/ws", func(ctx *context.Context) { diff --git a/services/pubsub/memory.go b/services/pubsub/memory.go index f105376c8887b..3a7745443fcc6 100644 --- a/services/pubsub/memory.go +++ b/services/pubsub/memory.go @@ -5,6 +5,7 @@ package pubsub import ( "context" + "errors" "sync" ) @@ -21,19 +22,21 @@ func NewMemory() Broker { } } -func (p *Memory) Publish(_ context.Context, message Message) { +func (p *Memory) Publish(_ context.Context, _topic string, data []byte) error { p.Lock() - topic, ok := p.topics[message.Topic] + topic, ok := p.topics[_topic] if !ok { p.Unlock() - return + return errors.New("topic not found") } for s := range topic { - go (*s)(message) + go (*s)(data) } p.Unlock() + + return nil } func (p *Memory) Subscribe(c context.Context, topic string, subscriber Subscriber) { diff --git a/services/pubsub/memory_test.go b/services/pubsub/memory_test.go index 1d9719077689c..fb7a638fefb42 100644 --- a/services/pubsub/memory_test.go +++ b/services/pubsub/memory_test.go @@ -14,12 +14,9 @@ import ( func TestPubsub(t *testing.T) { var ( - wg sync.WaitGroup - - testMessage = Message{ - Data: []byte("test"), - Topic: "hello-world", - } + wg sync.WaitGroup + testTopic = "hello-world" + testMessage = []byte("test") ) ctx, cancel := context.WithCancelCause( @@ -28,10 +25,10 @@ func TestPubsub(t *testing.T) { broker := NewMemory() go func() { - broker.Subscribe(ctx, "hello-world", func(message Message) { assert.Equal(t, testMessage, message); wg.Done() }) + broker.Subscribe(ctx, testTopic, func(message []byte) { assert.Equal(t, testMessage, message); wg.Done() }) }() go func() { - broker.Subscribe(ctx, "hello-world", func(_ Message) { wg.Done() }) + broker.Subscribe(ctx, testTopic, func(_ []byte) { wg.Done() }) }() // Wait a bit for the subscriptions to be registered @@ -39,7 +36,7 @@ func TestPubsub(t *testing.T) { wg.Add(2) go func() { - broker.Publish(ctx, testMessage) + broker.Publish(ctx, testTopic, testMessage) }() wg.Wait() diff --git a/services/pubsub/notifier.go b/services/pubsub/notifier.go new file mode 100644 index 0000000000000..36e5ecc1adf17 --- /dev/null +++ b/services/pubsub/notifier.go @@ -0,0 +1,56 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pubsub + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + notify_service "code.gitea.io/gitea/services/notify" +) + +func Init() Broker { + broker := NewMemory() // TODO: allow for other pubsub implementations + notify_service.RegisterNotifier(newNotifier(broker)) + return broker +} + +type pubsubNotifier struct { + notify_service.NullNotifier + broker Broker +} + +// NewNotifier create a new pubsubNotifier notifier +func newNotifier(broker Broker) notify_service.Notifier { + return &pubsubNotifier{ + broker: broker, + } +} + +func (p *pubsubNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { + data := struct { + Function string + Comment *issues_model.Comment + Doer *user_model.User + }{ + Function: "DeleteComment", + Comment: c, + Doer: doer, + } + + msg, err := json.Marshal(data) + if err != nil { + log.Error("Failed to marshal message: %v", err) + return + } + + err = p.broker.Publish(ctx, "notify", msg) + if err != nil { + log.Error("Failed to publish message: %v", err) + return + } +} diff --git a/services/pubsub/types.go b/services/pubsub/types.go index 143ffceb0338d..4312538042c75 100644 --- a/services/pubsub/types.go +++ b/services/pubsub/types.go @@ -5,19 +5,10 @@ package pubsub import "context" -// Message defines a published message. -type Message struct { - // Data is the actual data in the entry. - Data []byte `json:"data"` - - // Topic is the topic of the message. - Topic string `json:"topic"` -} - // Subscriber receives published messages. -type Subscriber func(Message) +type Subscriber func(data []byte) type Broker interface { - Publish(c context.Context, message Message) + Publish(c context.Context, topic string, data []byte) error Subscribe(c context.Context, topic string, subscriber Subscriber) } diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index c83fba7eb6c5d..6a4b3b8c3aa0d 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -7,21 +7,22 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/templates" notify_service "code.gitea.io/gitea/services/notify" - "code.gitea.io/gitea/services/pubsub" "github.com/olahol/melody" ) +var _ notify_service.Notifier = &websocketNotifier{} + type websocketNotifier struct { notify_service.NullNotifier - m *melody.Melody - rnd *templates.HTMLRender + melody *melody.Melody + htmlRenderer *templates.HTMLRender } // NewNotifier create a new webhooksNotifier notifier -func newNotifier(m *melody.Melody, pubsub pubsub.Broker) notify_service.Notifier { +func newNotifier(m *melody.Melody) notify_service.Notifier { return &websocketNotifier{ - m: m, - rnd: templates.HTMLRenderer(), + melody: m, + htmlRenderer: templates.HTMLRenderer(), } } @@ -31,7 +32,7 @@ func newNotifier(m *melody.Melody, pubsub pubsub.Broker) notify_service.Notifier var htmxRemoveElement = "
" func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData) bool) []*melody.Session { - sessions, err := n.m.Sessions() + sessions, err := n.melody.Sessions() if err != nil { log.Error("Failed to get sessions: %v", err) return nil diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 37e3d66640e51..3fcaf90aba4f6 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -4,9 +4,14 @@ package websocket import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/services/context" - notify_service "code.gitea.io/gitea/services/notify" + "code.gitea.io/gitea/modules/log" + gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/pubsub" "github.com/mitchellh/mapstructure" @@ -31,12 +36,46 @@ func Init() *melody.Melody { m.HandleDisconnect(handleDisconnect) broker := pubsub.NewMemory() // TODO: allow for other pubsub implementations - notify_service.RegisterNotifier(newNotifier(m, broker)) + notifier := newNotifier(m) + + ctx, unsubscribe := context.WithCancel(context.Background()) + graceful.GetManager().RunAtShutdown(ctx, func() { + unsubscribe() + }) + + broker.Subscribe(ctx, "notify", func(msg []byte) { + data := struct { + Function string + }{} + + err := json.Unmarshal(msg, &data) + if err != nil { + log.Error("Failed to unmarshal message: %v", err) + return + } + + switch data.Function { + case "DeleteComment": + var data struct { + Comment *issues_model.Comment + Doer *user_model.User + } + + err := json.Unmarshal(msg, &data) + if err != nil { + log.Error("Failed to unmarshal message: %v", err) + return + } + + notifier.DeleteComment(context.Background(), data.Doer, data.Comment) + } + }) + return m } func handleConnect(s *melody.Session) { - ctx := context.GetWebContext(s.Request) + ctx := gitea_context.GetWebContext(s.Request) data := &sessionData{} if ctx.IsSigned { From 63f4e6c61c0bf9947d40c13a8f1de0e80eacf4ec Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 Sep 2024 10:10:00 +0200 Subject: [PATCH 40/44] cleanup --- .../websocket/{issue_comment_notifier.go => comment_notifier.go} | 0 services/websocket/notifier.go | 1 - 2 files changed, 1 deletion(-) rename services/websocket/{issue_comment_notifier.go => comment_notifier.go} (100%) diff --git a/services/websocket/issue_comment_notifier.go b/services/websocket/comment_notifier.go similarity index 100% rename from services/websocket/issue_comment_notifier.go rename to services/websocket/comment_notifier.go diff --git a/services/websocket/notifier.go b/services/websocket/notifier.go index 6a4b3b8c3aa0d..934bbc364f842 100644 --- a/services/websocket/notifier.go +++ b/services/websocket/notifier.go @@ -28,7 +28,6 @@ func newNotifier(m *melody.Melody) notify_service.Notifier { // htmxAddElementEnd = "
%s
" // htmxUpdateElement = "
%s
" - var htmxRemoveElement = "
" func (n *websocketNotifier) filterSessions(fn func(*melody.Session, *sessionData) bool) []*melody.Session { From edb0f9c188d33d79866022fae39d7b683cca46c4 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 Sep 2024 10:27:55 +0200 Subject: [PATCH 41/44] merge --- go.mod | 3 +++ go.sum | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a821d8544f414..5493117b7f792 100644 --- a/go.mod +++ b/go.mod @@ -235,7 +235,10 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect diff --git a/go.sum b/go.sum index 3d9a8ed6ed392..eaa12ed0c8532 100644 --- a/go.sum +++ b/go.sum @@ -475,10 +475,13 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= From f2001a2e7a8a0c227f9236a4264fdab2afe13da7 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 Sep 2024 10:29:36 +0200 Subject: [PATCH 42/44] fix --- services/pubsub/notifier.go | 2 +- services/websocket/comment_notifier.go | 43 ++++++++++++++++++++------ services/websocket/websocket.go | 10 ++---- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/services/pubsub/notifier.go b/services/pubsub/notifier.go index 36e5ecc1adf17..387821cae9c99 100644 --- a/services/pubsub/notifier.go +++ b/services/pubsub/notifier.go @@ -13,7 +13,7 @@ import ( notify_service "code.gitea.io/gitea/services/notify" ) -func Init() Broker { +func InitWithNotifier() Broker { broker := NewMemory() // TODO: allow for other pubsub implementations notify_service.RegisterNotifier(newNotifier(broker)) return broker diff --git a/services/websocket/comment_notifier.go b/services/websocket/comment_notifier.go index e8bafaa4b2bba..9e0b060b87c70 100644 --- a/services/websocket/comment_notifier.go +++ b/services/websocket/comment_notifier.go @@ -5,22 +5,45 @@ package websocket import ( "context" - "encoding/json" "fmt" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/services/pubsub" + "code.gitea.io/gitea/modules/log" + + "github.com/olahol/melody" ) -func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { - d, err := json.Marshal(c) - if err != nil { - return - } +func (n *websocketNotifier) filterIssueSessions(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue) []*melody.Session { + return n.filterSessions(func(s *melody.Session, data *sessionData) bool { + // if the user is watching the issue, they will get notifications + if !data.isOnURL(fmt.Sprintf("/%s/%s/issues/%d", repo.Owner.Name, repo.Name, issue.Index)) { + return false + } - topic := fmt.Sprintf("repo:%s/%s", c.RefRepo.OwnerName, c.RefRepo.Name) - n.pubsub.Publish(ctx, topic, pubsub.Message{ - Data: d, + // the user will get notifications if they have access to the repos issues + hasAccess, err := access.HasAccessUnit(ctx, data.user, repo, unit.TypeIssues, perm.AccessModeRead) + if err != nil { + log.Error("Failed to check access: %v", err) + return false + } + + return hasAccess }) } + +func (n *websocketNotifier) DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { + sessions := n.filterIssueSessions(ctx, c.Issue.Repo, c.Issue) + + for _, s := range sessions { + msg := fmt.Sprintf(htmxRemoveElement, fmt.Sprintf("#%s", c.HashTag())) + err := s.Write([]byte(msg)) + if err != nil { + log.Error("Failed to write to session: %v", err) + } + } +} diff --git a/services/websocket/websocket.go b/services/websocket/websocket.go index 83013ebe94634..ed45aa9543ba4 100644 --- a/services/websocket/websocket.go +++ b/services/websocket/websocket.go @@ -31,14 +31,12 @@ type subscribeMessageData struct { func Init() *melody.Melody { m = melody.New() - hub := &hub{ - pubsub: pubsub.NewMemory(), - } + hub := &hub{} m.HandleConnect(hub.handleConnect) m.HandleMessage(hub.handleMessage) m.HandleDisconnect(hub.handleDisconnect) - broker := pubsub.NewMemory() // TODO: allow for other pubsub implementations + broker := pubsub.InitWithNotifier() notifier := newNotifier(m) ctx, unsubscribe := context.WithCancel(context.Background()) @@ -77,9 +75,7 @@ func Init() *melody.Melody { return m } -type hub struct { - pubsub pubsub.Broker -} +type hub struct{} func (h *hub) handleConnect(s *melody.Session) { ctx := gitea_context.GetWebContext(s.Request) From 56d11fca203d3865929003a5db8c8c8777d13f68 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 Sep 2024 10:39:33 +0200 Subject: [PATCH 43/44] update --- assets/go-licenses.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 66ce8474ccf80..0dc475d3ae9c1 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -684,16 +684,16 @@ "path": "github.com/gorilla/sessions/LICENSE", "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/hashicorp/errwrap", - "path": "github.com/hashicorp/errwrap/LICENSE", - "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party’s\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n" - }, { "name": "github.com/gorilla/websocket", "path": "github.com/gorilla/websocket/LICENSE", "licenseText": "Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/hashicorp/errwrap", + "path": "github.com/hashicorp/errwrap/LICENSE", + "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party’s\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n" + }, { "name": "github.com/hashicorp/go-cleanhttp", "path": "github.com/hashicorp/go-cleanhttp/LICENSE", @@ -707,7 +707,7 @@ { "name": "github.com/hashicorp/go-retryablehttp", "path": "github.com/hashicorp/go-retryablehttp/LICENSE", - "licenseText": "Copyright (c) 2015 HashiCorp, Inc.\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n\n" + "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n\n" }, { "name": "github.com/hashicorp/go-version", From 7efef85448cdb4f128c5467a03772fec2183611e Mon Sep 17 00:00:00 2001 From: Anbraten Date: Mon, 7 Oct 2024 11:38:01 +0200 Subject: [PATCH 44/44] fix memory pubsub --- services/pubsub/memory.go | 21 ++++++++++++--------- services/pubsub/memory_test.go | 16 +++------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/services/pubsub/memory.go b/services/pubsub/memory.go index 3a7745443fcc6..6df7d5428a0fa 100644 --- a/services/pubsub/memory.go +++ b/services/pubsub/memory.go @@ -49,14 +49,17 @@ func (p *Memory) Subscribe(c context.Context, topic string, subscriber Subscribe p.topics[topic][&subscriber] = struct{}{} p.Unlock() - // Wait for context to be done - <-c.Done() + // Unsubscribe when context is done + go func() { + // Wait for context to be done + <-c.Done() - // Unsubscribe - p.Lock() - delete(p.topics[topic], &subscriber) - if len(p.topics[topic]) == 0 { - delete(p.topics, topic) - } - p.Unlock() + // Unsubscribe + p.Lock() + delete(p.topics[topic], &subscriber) + if len(p.topics[topic]) == 0 { + delete(p.topics, topic) + } + p.Unlock() + }() } diff --git a/services/pubsub/memory_test.go b/services/pubsub/memory_test.go index fb7a638fefb42..43e28cbf49ed5 100644 --- a/services/pubsub/memory_test.go +++ b/services/pubsub/memory_test.go @@ -7,7 +7,6 @@ import ( "context" "sync" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -24,20 +23,11 @@ func TestPubsub(t *testing.T) { ) broker := NewMemory() - go func() { - broker.Subscribe(ctx, testTopic, func(message []byte) { assert.Equal(t, testMessage, message); wg.Done() }) - }() - go func() { - broker.Subscribe(ctx, testTopic, func(_ []byte) { wg.Done() }) - }() - - // Wait a bit for the subscriptions to be registered - <-time.After(100 * time.Millisecond) + broker.Subscribe(ctx, testTopic, func(message []byte) { assert.Equal(t, testMessage, message); wg.Done() }) + broker.Subscribe(ctx, testTopic, func(_ []byte) { wg.Done() }) wg.Add(2) - go func() { - broker.Publish(ctx, testTopic, testMessage) - }() + broker.Publish(ctx, testTopic, testMessage) wg.Wait() cancel(nil)