diff --git a/CHANGELOG.md b/CHANGELOG.md index 664aae5..bd56254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,5 @@ [Unreleased] -## v1.1.0 - 2025-09-07 - -### Added - -- feat(chat): use websocket to update messages -- feat(chat): add css animation - ## v1.0.1 - 2025-08-31 ### Fixed diff --git a/README.md b/README.md index 2b834c6..64dae49 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ Add a webhook (Integration > Webhooks): You can embed the chat using this URL: `{server.base_url}/chat/messages` -If you use a reverse proxy, configure `{server.base_url}/ws.*` as weboscket. - ## Usage ``` diff --git a/assets/src/js/lib/chat.js b/assets/src/js/lib/chat.js index 38a6666..60217f3 100644 --- a/assets/src/js/lib/chat.js +++ b/assets/src/js/lib/chat.js @@ -1,51 +1,40 @@ const isChat = () => { - return getChat() !== null + return document.querySelector('#chat') !== null } const getChat = () => { return document.querySelector('#chat') } -const createWebSocketConnection = () => { - const protocol = location.protocol === 'https:' ? 'wss' : 'ws' +const updateChat = () => { + fetch(document.location.href) + .then((response) => response.text()) + .then((html) => { + const parser = new DOMParser() + const doc = parser.parseFromString(html, 'text/html') + const nextMessages = doc.querySelectorAll('.message') - return new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws/chat/messages`) -} + const currentMessages = getChat().querySelectorAll('.message') + const messagesContainer = getChat().querySelector('.messages') -const isInViewport = (element) => { - const rect = element.getBoundingClientRect() + nextMessages.forEach((nextMessage) => { + let add = true - return (rect.top - 90) > 0 -} + currentMessages.forEach((currentMessage) => { + if (currentMessage.id === nextMessage.id) { + add = false + } + }) -const runChat = () => { - const ws = createWebSocketConnection() + if (add) { + messagesContainer.appendChild(nextMessage) + } + }) - ws.addEventListener('close', runChat) + messagesContainer.scrollTo(0, messagesContainer.scrollHeight) - ws.addEventListener('message', (event) => { - const items = JSON.parse(event.data) - - if (items == null || !items.length) { - return - } - - const container = getChat().querySelector('.messages') - - items.forEach((item) => { - const message = document.createElement('div') - message.innerHTML = item - - container.appendChild(message) + window.setTimeout(updateChat, 500) }) - - getChat().querySelectorAll('.message').forEach((message) => { - if (!isInViewport(message)) { - message.classList.toggle('animate__fadeInUp', false) - message.classList.toggle('animate__fadeOutUp', true) - } - }) - }) } -export {isChat, runChat} +export {isChat, updateChat} diff --git a/assets/src/js/main.js b/assets/src/js/main.js index 393d4ec..8d75a5c 100644 --- a/assets/src/js/main.js +++ b/assets/src/js/main.js @@ -1,6 +1,6 @@ import '../scss/main.scss' -import {isChat, runChat} from './lib/chat.js' +import {isChat, updateChat} from './lib/chat.js' if (isChat()) { - runChat() + updateChat() } diff --git a/assets/src/scss/main.scss b/assets/src/scss/main.scss index 7110344..37ce62e 100644 --- a/assets/src/scss/main.scss +++ b/assets/src/scss/main.scss @@ -1,5 +1,3 @@ -@import "~animate.css/animate.css"; - :root { --color-owncast-user-0: #ff717b; --color-owncast-user-1: #f4e413; @@ -18,24 +16,19 @@ --theme-color-users-5: var(--color-owncast-user-5); --theme-color-users-6: var(--color-owncast-user-6); --theme-color-users-7: var(--color-owncast-user-7); - - --animate-duration: 500ms; } #chat { background: transparent; margin: 0; font-family: monospace; + height: calc(100vh - 10px); + /* border: 5px solid #254779; */ .messages { + max-height: calc(100vh - 20px); overflow: hidden; font-size: 13px; - display: flex; - align-self: end; - flex-direction: column; - position: fixed; - bottom: 10px; - width: 100%; .message { background: #000000cc; diff --git a/bin/watch.sh b/bin/watch.sh index 1aa23bc..f4ddf7a 100755 --- a/bin/watch.sh +++ b/bin/watch.sh @@ -6,7 +6,6 @@ sn=owncastwh st="Owncast Webhook" while true; do - screen -X -S "$sn" quit rm -f cmd/server/rice-box.go ./node_modules/.bin/webpack screen -S "$sn" -d -m go run ./cmd/server/ diff --git a/cmd/server/server.go b/cmd/server/server.go index 9e16d5c..9fcb48f 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -14,7 +14,6 @@ import ( "github.com/labstack/echo/v4/middleware" "gitnet.fr/deblan/owncast-webhook/internal/client/twitch" "gitnet.fr/deblan/owncast-webhook/internal/config" - "gitnet.fr/deblan/owncast-webhook/internal/test" "gitnet.fr/deblan/owncast-webhook/internal/web/router" ) @@ -44,10 +43,6 @@ func main() { e.Use(middleware.Logger()) router.RegisterControllers(e) - if conf.Test.Enable { - test.GenerateFakeMessages() - } - if conf.Twitch.Enable { twitch.IrcClient() } diff --git a/config.ini.example b/config.ini.example index 7885e57..9be2b6d 100644 --- a/config.ini.example +++ b/config.ini.example @@ -10,7 +10,3 @@ base_url = "https://live.example.com" [twitch] enable = false channel = "username" - -; Add fake messages to test -[test] -enable = false diff --git a/go.mod b/go.mod index 12784d9..a7fcd5b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/GeertJohan/go.rice v1.0.3 github.com/gempir/go-twitch-irc/v4 v4.2.0 github.com/go-playground/validator v9.31.0+incompatible - github.com/gorilla/websocket v1.5.3 github.com/labstack/echo/v4 v4.12.0 gopkg.in/ini.v1 v1.67.0 maragu.dev/gomponents v1.2.0 @@ -14,7 +13,6 @@ require ( require ( github.com/daaku/go.zipexe v1.0.2 // indirect - github.com/go-faker/faker/v4 v4.6.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect @@ -28,7 +26,7 @@ require ( golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index bdbad59..51b1541 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gempir/go-twitch-irc/v4 v4.2.0 h1:OCeff+1aH4CZIOxgKOJ8dQjh+1ppC6sLWrXOcpGZyq4= github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg= -github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k= -github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -18,8 +16,6 @@ github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= @@ -50,8 +46,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= diff --git a/internal/config/config.go b/internal/config/config.go index e3d07c3..9b020a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,9 +16,6 @@ type Config struct { Owncast struct { BaseUrl string } - Test struct { - Enable bool - } Twitch struct { Enable bool Channel string @@ -48,7 +45,6 @@ func (c *Config) Load(file string) { config.Server.WebhookSecret = cfg.Section("server").Key("webhook_secret").String() config.Owncast.BaseUrl = cfg.Section("owncast").Key("base_url").String() - config.Test.Enable = cfg.Section("test").Key("enable").MustBool(false) config.Twitch.Enable = cfg.Section("twitch").Key("enable").MustBool(false) config.Twitch.Channel = cfg.Section("twitch").Key("channel").String() diff --git a/internal/store/message.go b/internal/store/message.go index a8b106d..385a8c1 100644 --- a/internal/store/message.go +++ b/internal/store/message.go @@ -7,7 +7,6 @@ var messageStore *MessageStore const ( MessageOriginOwncast MessageOrigin = iota MessageOriginTwitch - MessageOriginTest ) type MessageStore struct { @@ -22,10 +21,6 @@ func (s *MessageStore) All() []MessageInterface { return s.messages } -func (s *MessageStore) Clear() { - s.messages = []MessageInterface{} -} - func GetMessageStore() *MessageStore { if messageStore == nil { messageStore = new(MessageStore) diff --git a/internal/store/test_message.go b/internal/store/test_message.go deleted file mode 100644 index 1cdfe12..0000000 --- a/internal/store/test_message.go +++ /dev/null @@ -1,30 +0,0 @@ -package store - -import ( - "github.com/gempir/go-twitch-irc/v4" - "github.com/go-faker/faker/v4" -) - -type TestMessage struct { - Message twitch.PrivateMessage -} - -func (o TestMessage) ID() string { - return faker.Sentence() -} - -func (o TestMessage) Visible() bool { - return true -} - -func (o TestMessage) Origin() MessageOrigin { - return MessageOriginTest -} - -func (o TestMessage) Author() string { - return faker.Username() -} - -func (o TestMessage) Content() string { - return faker.Sentence() -} diff --git a/internal/test/fake.go b/internal/test/fake.go deleted file mode 100644 index 90fbd12..0000000 --- a/internal/test/fake.go +++ /dev/null @@ -1,17 +0,0 @@ -package test - -import ( - "math/rand" - "time" - - "gitnet.fr/deblan/owncast-webhook/internal/store" -) - -func GenerateFakeMessages() { - go func() { - for { - store.GetMessageStore().Add(store.TestMessage{}) - time.Sleep(time.Duration(rand.Intn(3000-100)+100) * time.Millisecond) - } - }() -} diff --git a/internal/web/controller/chat/controller.go b/internal/web/controller/chat/controller.go index 7bc963f..ab42367 100644 --- a/internal/web/controller/chat/controller.go +++ b/internal/web/controller/chat/controller.go @@ -1,10 +1,8 @@ package chat import ( - "bytes" - "encoding/json" + "fmt" - "github.com/gorilla/websocket" "github.com/labstack/echo/v4" "gitnet.fr/deblan/owncast-webhook/assets" "gitnet.fr/deblan/owncast-webhook/internal/store" @@ -13,10 +11,6 @@ import ( . "maragu.dev/gomponents/html" ) -var ( - upgrader = websocket.Upgrader{} -) - type Controller struct { } @@ -24,50 +18,51 @@ func New(e *echo.Echo) *Controller { c := Controller{} e.GET("/chat/messages", c.Messages) - e.GET("/ws/chat/messages", c.WebsocketMessages) return &c } -func (ctrl *Controller) WebsocketMessages(c echo.Context) error { - ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) - - if err != nil { - return err - } - - defer ws.Close() - - storage := store.GetMessageStore() - - for { - messages := storage.All() - - if len(messages) == 0 { - continue - } - - storage.Clear() - - var items []string - - for _, message := range messages { - var buff bytes.Buffer - msg := CreateMessageView(message) - msg.Render(&buff) - - items = append(items, buff.String()) - } - - j, _ := json.Marshal(items) - - ws.WriteMessage(websocket.TextMessage, j) - } - - return nil -} - func (ctrl *Controller) Messages(c echo.Context) error { + createMessage := func(message store.MessageInterface) Node { + var containerStyle Node + var userStyle Node + var originIcon Node + + switch message.Origin() { + case store.MessageOriginOwncast: + msg := message.(store.OwncastMessage) + + containerStyle = StyleAttr(fmt.Sprintf( + "border-color: var(--theme-color-users-%d)", + msg.WebhookMessage.User.DisplayColor, + )) + + userStyle = StyleAttr(fmt.Sprintf( + "color: var(--theme-color-users-%d)", + msg.WebhookMessage.User.DisplayColor, + )) + + originIcon = Img(Src(assets.Asset("dist/img/owncast.png")), Class("message-origin")) + case store.MessageOriginTwitch: + originIcon = Img(Src(assets.Asset("dist/img/twitch.png")), Class("message-origin")) + } + + return Div( + Class("message"), + ID(message.ID()), + containerStyle, + Div( + Class("message-user"), + userStyle, + Group([]Node{originIcon, Text(message.Author())}), + ), + Div( + Class("message-body"), + Raw(message.Content()), + ), + ) + } + page := HTML5(HTML5Props{ Title: "Chat", Language: "fr", @@ -77,7 +72,10 @@ func (ctrl *Controller) Messages(c echo.Context) error { }, Body: []Node{ ID("chat"), - Div(Class("messages")), + Div( + Class("messages"), + Map(store.GetMessageStore().All(), createMessage), + ), Group(assets.EntrypointJs("main")), }, }) diff --git a/internal/web/controller/chat/utils.go b/internal/web/controller/chat/utils.go deleted file mode 100644 index 7b6ef4a..0000000 --- a/internal/web/controller/chat/utils.go +++ /dev/null @@ -1,50 +0,0 @@ -package chat - -import ( - "fmt" - - "gitnet.fr/deblan/owncast-webhook/assets" - "gitnet.fr/deblan/owncast-webhook/internal/store" - . "maragu.dev/gomponents" - . "maragu.dev/gomponents/html" -) - -func CreateMessageView(message store.MessageInterface) Node { - var containerStyle Node - var userStyle Node - var originIcon Node - - switch message.Origin() { - case store.MessageOriginOwncast: - msg := message.(store.OwncastMessage) - - containerStyle = StyleAttr(fmt.Sprintf( - "border-color: var(--theme-color-users-%d)", - msg.WebhookMessage.User.DisplayColor, - )) - - userStyle = StyleAttr(fmt.Sprintf( - "color: var(--theme-color-users-%d)", - msg.WebhookMessage.User.DisplayColor, - )) - - originIcon = Img(Src(assets.Asset("dist/img/owncast.png")), Class("message-origin")) - case store.MessageOriginTwitch: - originIcon = Img(Src(assets.Asset("dist/img/twitch.png")), Class("message-origin")) - } - - return Div( - Class("message animate__animated animate__fadeInUp"), - ID(message.ID()), - containerStyle, - Div( - Class("message-user"), - userStyle, - Group([]Node{originIcon, Text(message.Author())}), - ), - Div( - Class("message-body"), - Raw(message.Content()), - ), - ) -} diff --git a/package-lock.json b/package-lock.json index 9037682..21e542c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "": { "dependencies": { "@symfony/webpack-encore": "github:symfony/webpack-encore", - "animate.css": "^4.1.1", "raw-loader": "^4.0.2" }, "devDependencies": { @@ -2735,12 +2734,6 @@ "ajv": "^6.9.1" } }, - "node_modules/animate.css": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", - "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", - "license": "MIT" - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index ed07c0d..5a26d4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "dependencies": { "@symfony/webpack-encore": "github:symfony/webpack-encore", - "animate.css": "^4.1.1", "raw-loader": "^4.0.2" }, "devDependencies": {