commit a236b8d07abf80abca8a4ff61d4aba09def5d7f2
parent 9c0c3cc3a0c714b1812d086fe2829d238af45fe0
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Fri, 21 Feb 2025 20:25:57 -0800
top-bar stream for quban
Diffstat:
9 files changed, 463 insertions(+), 0 deletions(-)
diff --git a/cmd/dkf/migrations/164.sql b/cmd/dkf/migrations/164.sql
@@ -0,0 +1,4 @@
+-- +migrate Up
+ALTER TABLE users ADD COLUMN use_stream_top_bar TINYINT(1) NOT NULL DEFAULT 0;
+
+-- +migrate Down
diff --git a/pkg/database/tableUsers.go b/pkg/database/tableUsers.go
@@ -159,6 +159,7 @@ type User struct {
AFK bool
UseStream bool
UseStreamMenu bool
+ UseStreamTopBar bool
SyntaxHighlightCode string
ConfirmExternalLinks bool
ChessSoundsEnabled bool
@@ -826,6 +827,7 @@ func (d *DkfDB) createUser(usernameStr, password, repassword, gpgPublicKey strin
newUser.RegistrationDuration = registrationDuration
newUser.UseStream = true
newUser.UseStreamMenu = true
+ newUser.UseStreamTopBar = true
newUser.DisplayAliveIndicator = true
newUser.CodeBlockHeight = 300
newUser.HellbanOpacity = 30
diff --git a/pkg/web/handlers/api/v1/stream-top-bar.qtpl b/pkg/web/handlers/api/v1/stream-top-bar.qtpl
@@ -0,0 +1,120 @@
+{% import "dkforest/pkg/database" %}
+
+{% func RenderTopBar(CSRF string, Data chatTopBarData, AuthUser *database.User) -%}
+<html lang="en">
+ <head>
+ <title></title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ body { font-family: Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; }
+ a { color: #00bc8c; text-decoration: none; font-size: 14px; }
+ a:hover { color: #007053; text-decoration: underline; }
+ .wrapper {
+ position: relative;
+ width: 500px;
+ }
+ .wrapper input, .wrapper textarea {
+ box-sizing: border-box;
+ border-radius: 10px 0px 0px 10px;
+ border: 1px solid rgba(10,10,10,255);
+ background: rgba(76, 76, 76, 255);
+ margin: 0;
+ float: left;
+ padding-right: 10px;
+ padding-left: 10px;
+ width: 421px;
+ height: 22px;
+ color: #fff;
+ box-shadow:0 0 0 black;
+ outline: none;
+ }
+ .wrapper input:focus {
+ outline: none;
+ }
+ .wrapper button {
+ border-radius: 0 10px 10px 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 80px;
+ height: 22px;
+ color: white;
+ border: 1px solid rgba(10,10,10,255);
+ background: rgba(65, 65, 65, 255);
+ }
+ .wrapper button:hover {
+ background: rgba(57, 57, 57, 255);
+ }
+ {%- if Data.Multiline -%}
+ .wrapper textarea, .wrapper button {
+ height: 122px;
+ }
+ {%- endif -%}
+ * {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ [list]::-webkit-calendar-picker-indicator {
+ display: none !important;
+ }
+
+ #top-bar-form { margin: 0; padding: 5px 0 0 0; }
+ #refresh-btn { line-height: 20px; }
+ #file-upload-btn { height: 16px; font-size: 9px; margin-top: 2px; margin-left: 5px; position: absolute; }
+ #err-lbl { color: #e74c3c; font-size: 80%; }
+ #success-lbl { color: #198754; font-size: 80%; }
+ #username-td { padding-right: 5px; }
+ #links-td { color: #aaa; }
+ </style>
+ </head>
+ <body>
+ <form method="post" id="top-bar-form" enctype="multipart/form-data">
+ <input type="hidden" name="csrf" value="{%s CSRF %}" />
+ <table>
+ <tr>
+ <td id="username-td"><span {%= unesc(AuthUser.GenerateChatStyle()) -%}>{%s string(AuthUser.Username) -%}</span></td>
+ <td>
+ <div class="wrapper">
+ {%- if Data.Multiline -%}
+ <textarea name="message" maxlength="10000" autocomplete="off"{%- if AuthUser.SpellcheckEnabled -%} spellcheck="true"{%- endif -%} autofocus>{%s Data.Message %}</textarea>
+ {%- else -%}
+ <input value="{%s Data.Message -%}" type="text" name="message" maxlength="10000" autocomplete="off"{%- if AuthUser.SpellcheckEnabled -%} spellcheck="true"{%- endif -%} autofocus{%- if AuthUser.AutocompleteCommandsEnabled -%} list="commands"{%- endif -%} />
+ {%- if AuthUser.AutocompleteCommandsEnabled -%}
+ <datalist id="commands">
+ {%- for _, el := range Data.CommandsList -%}<option value="{%s el %}">{%- endfor -%}
+ </datalist>
+ {%- endif -%}
+ {%- endif -%}
+ <button type="submit" value="send_message" name="btn_submit">send</button>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ {%- if Data.Error != "" || Data.Success != "" -%}
+ <a href="/api/v1/chat/top-bar/{%s Data.RoomName -%}?{%- if Data.Multiline -%}{%s Data.QueryParamsMl -%}{%- else -%}{%s Data.QueryParamsNml -%}{%- endif -%}" title="Refresh" id="refresh-btn">↻</a>
+ {%- endif -%}
+ </td>
+ <td id="links-td">
+ {%- if Data.Error != "" -%}
+ <span id="err-lbl">{%s Data.Error -%}</span>
+ {%- elseif Data.Success != "" -%}
+ <span id="success-lbl">{%s Data.Success -%}</span>
+ {%- else -%}
+ <a href="/settings/chat" target="_top">Settings</a> |
+ <a href="/chat/help" target="_top">"Available commands</a> |
+ <a href="/chat/create-room" target="_top">Create room</a>
+ {%- if AuthUser.CanUseMultiline -%} | <a href="/chat/{%s Data.RoomName -%}?{%- if Data.Multiline -%}{%s Data.QueryParamsNml -%}{%- else -%}{%s Data.QueryParamsMl -%}{%- endif -%}" target="_top">ml</a>{%- endif -%}
+ {%- if AuthUser.CanUpload() -%} | <input name="file" type="file" id="file-upload-btn" />{%- endif -%}
+ {%- endif -%}
+ </td>
+ <td></td>
+ </tr>
+ </table>
+ </form>
+ </body>
+</html>
+{%- endfunc -%}
diff --git a/pkg/web/handlers/api/v1/stream-top-bar.qtpl.go b/pkg/web/handlers/api/v1/stream-top-bar.qtpl.go
@@ -0,0 +1,312 @@
+// Code generated by qtc from "stream-top-bar.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line stream-top-bar.qtpl:1
+package v1
+
+//line stream-top-bar.qtpl:1
+import "dkforest/pkg/database"
+
+//line stream-top-bar.qtpl:3
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line stream-top-bar.qtpl:3
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+//line stream-top-bar.qtpl:3
+func StreamRenderTopBar(qw422016 *qt422016.Writer, CSRF string, Data chatTopBarData, AuthUser *database.User) {
+//line stream-top-bar.qtpl:3
+ qw422016.N().S(`<html lang="en">
+ <head>
+ <title></title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ body { font-family: Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; }
+ a { color: #00bc8c; text-decoration: none; font-size: 14px; }
+ a:hover { color: #007053; text-decoration: underline; }
+ .wrapper {
+ position: relative;
+ width: 500px;
+ }
+ .wrapper input, .wrapper textarea {
+ box-sizing: border-box;
+ border-radius: 10px 0px 0px 10px;
+ border: 1px solid rgba(10,10,10,255);
+ background: rgba(76, 76, 76, 255);
+ margin: 0;
+ float: left;
+ padding-right: 10px;
+ padding-left: 10px;
+ width: 421px;
+ height: 22px;
+ color: #fff;
+ box-shadow:0 0 0 black;
+ outline: none;
+ }
+ .wrapper input:focus {
+ outline: none;
+ }
+ .wrapper button {
+ border-radius: 0 10px 10px 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 80px;
+ height: 22px;
+ color: white;
+ border: 1px solid rgba(10,10,10,255);
+ background: rgba(65, 65, 65, 255);
+ }
+ .wrapper button:hover {
+ background: rgba(57, 57, 57, 255);
+ }
+`)
+//line stream-top-bar.qtpl:51
+ if Data.Multiline {
+//line stream-top-bar.qtpl:51
+ qw422016.N().S(` .wrapper textarea, .wrapper button {
+ height: 122px;
+ }
+`)
+//line stream-top-bar.qtpl:55
+ }
+//line stream-top-bar.qtpl:55
+ qw422016.N().S(` * {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ [list]::-webkit-calendar-picker-indicator {
+ display: none !important;
+ }
+
+ #top-bar-form { margin: 0; padding: 5px 0 0 0; }
+ #refresh-btn { line-height: 20px; }
+ #file-upload-btn { height: 16px; font-size: 9px; margin-top: 2px; margin-left: 5px; position: absolute; }
+ #err-lbl { color: #e74c3c; font-size: 80%; }
+ #success-lbl { color: #198754; font-size: 80%; }
+ #username-td { padding-right: 5px; }
+ #links-td { color: #aaa; }
+ </style>
+ </head>
+ <body>
+ <form method="post" id="top-bar-form" enctype="multipart/form-data">
+ <input type="hidden" name="csrf" value="`)
+//line stream-top-bar.qtpl:75
+ qw422016.E().S(CSRF)
+//line stream-top-bar.qtpl:75
+ qw422016.N().S(`" />
+ <table>
+ <tr>
+ <td id="username-td"><span `)
+//line stream-top-bar.qtpl:78
+ streamunesc(qw422016, AuthUser.GenerateChatStyle())
+//line stream-top-bar.qtpl:78
+ qw422016.N().S(`>`)
+//line stream-top-bar.qtpl:78
+ qw422016.E().S(string(AuthUser.Username))
+//line stream-top-bar.qtpl:78
+ qw422016.N().S(`</span></td>
+ <td>
+ <div class="wrapper">
+`)
+//line stream-top-bar.qtpl:81
+ if Data.Multiline {
+//line stream-top-bar.qtpl:81
+ qw422016.N().S(` <textarea name="message" maxlength="10000" autocomplete="off"`)
+//line stream-top-bar.qtpl:82
+ if AuthUser.SpellcheckEnabled {
+//line stream-top-bar.qtpl:82
+ qw422016.N().S(`spellcheck="true"`)
+//line stream-top-bar.qtpl:82
+ }
+//line stream-top-bar.qtpl:82
+ qw422016.N().S(`autofocus>`)
+//line stream-top-bar.qtpl:82
+ qw422016.E().S(Data.Message)
+//line stream-top-bar.qtpl:82
+ qw422016.N().S(`</textarea>
+`)
+//line stream-top-bar.qtpl:83
+ } else {
+//line stream-top-bar.qtpl:83
+ qw422016.N().S(` <input value="`)
+//line stream-top-bar.qtpl:84
+ qw422016.E().S(Data.Message)
+//line stream-top-bar.qtpl:84
+ qw422016.N().S(`" type="text" name="message" maxlength="10000" autocomplete="off"`)
+//line stream-top-bar.qtpl:84
+ if AuthUser.SpellcheckEnabled {
+//line stream-top-bar.qtpl:84
+ qw422016.N().S(`spellcheck="true"`)
+//line stream-top-bar.qtpl:84
+ }
+//line stream-top-bar.qtpl:84
+ qw422016.N().S(`autofocus`)
+//line stream-top-bar.qtpl:84
+ if AuthUser.AutocompleteCommandsEnabled {
+//line stream-top-bar.qtpl:84
+ qw422016.N().S(`list="commands"`)
+//line stream-top-bar.qtpl:84
+ }
+//line stream-top-bar.qtpl:84
+ qw422016.N().S(`/>
+`)
+//line stream-top-bar.qtpl:85
+ if AuthUser.AutocompleteCommandsEnabled {
+//line stream-top-bar.qtpl:85
+ qw422016.N().S(` <datalist id="commands">
+`)
+//line stream-top-bar.qtpl:87
+ for _, el := range Data.CommandsList {
+//line stream-top-bar.qtpl:87
+ qw422016.N().S(`<option value="`)
+//line stream-top-bar.qtpl:87
+ qw422016.E().S(el)
+//line stream-top-bar.qtpl:87
+ qw422016.N().S(`">`)
+//line stream-top-bar.qtpl:87
+ }
+//line stream-top-bar.qtpl:87
+ qw422016.N().S(` </datalist>
+`)
+//line stream-top-bar.qtpl:89
+ }
+//line stream-top-bar.qtpl:90
+ }
+//line stream-top-bar.qtpl:90
+ qw422016.N().S(` <button type="submit" value="send_message" name="btn_submit">send</button>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+`)
+//line stream-top-bar.qtpl:97
+ if Data.Error != "" || Data.Success != "" {
+//line stream-top-bar.qtpl:97
+ qw422016.N().S(` <a href="/api/v1/chat/top-bar/`)
+//line stream-top-bar.qtpl:98
+ qw422016.E().S(Data.RoomName)
+//line stream-top-bar.qtpl:98
+ qw422016.N().S(`?`)
+//line stream-top-bar.qtpl:98
+ if Data.Multiline {
+//line stream-top-bar.qtpl:98
+ qw422016.E().S(Data.QueryParamsMl)
+//line stream-top-bar.qtpl:98
+ } else {
+//line stream-top-bar.qtpl:98
+ qw422016.E().S(Data.QueryParamsNml)
+//line stream-top-bar.qtpl:98
+ }
+//line stream-top-bar.qtpl:98
+ qw422016.N().S(`" title="Refresh" id="refresh-btn">↻</a>
+`)
+//line stream-top-bar.qtpl:99
+ }
+//line stream-top-bar.qtpl:99
+ qw422016.N().S(` </td>
+ <td id="links-td">
+`)
+//line stream-top-bar.qtpl:102
+ if Data.Error != "" {
+//line stream-top-bar.qtpl:102
+ qw422016.N().S(` <span id="err-lbl">`)
+//line stream-top-bar.qtpl:103
+ qw422016.E().S(Data.Error)
+//line stream-top-bar.qtpl:103
+ qw422016.N().S(`</span>
+`)
+//line stream-top-bar.qtpl:104
+ } else if Data.Success != "" {
+//line stream-top-bar.qtpl:104
+ qw422016.N().S(` <span id="success-lbl">`)
+//line stream-top-bar.qtpl:105
+ qw422016.E().S(Data.Success)
+//line stream-top-bar.qtpl:105
+ qw422016.N().S(`</span>
+`)
+//line stream-top-bar.qtpl:106
+ } else {
+//line stream-top-bar.qtpl:106
+ qw422016.N().S(` <a href="/settings/chat" target="_top">Settings</a> |
+ <a href="/chat/help" target="_top">"Available commands</a> |
+ <a href="/chat/create-room" target="_top">Create room</a>
+`)
+//line stream-top-bar.qtpl:110
+ if AuthUser.CanUseMultiline {
+//line stream-top-bar.qtpl:110
+ qw422016.N().S(`| <a href="/chat/`)
+//line stream-top-bar.qtpl:110
+ qw422016.E().S(Data.RoomName)
+//line stream-top-bar.qtpl:110
+ qw422016.N().S(`?`)
+//line stream-top-bar.qtpl:110
+ if Data.Multiline {
+//line stream-top-bar.qtpl:110
+ qw422016.E().S(Data.QueryParamsNml)
+//line stream-top-bar.qtpl:110
+ } else {
+//line stream-top-bar.qtpl:110
+ qw422016.E().S(Data.QueryParamsMl)
+//line stream-top-bar.qtpl:110
+ }
+//line stream-top-bar.qtpl:110
+ qw422016.N().S(`" target="_top">ml</a>`)
+//line stream-top-bar.qtpl:110
+ }
+//line stream-top-bar.qtpl:111
+ if AuthUser.CanUpload() {
+//line stream-top-bar.qtpl:111
+ qw422016.N().S(`| <input name="file" type="file" id="file-upload-btn" />`)
+//line stream-top-bar.qtpl:111
+ }
+//line stream-top-bar.qtpl:112
+ }
+//line stream-top-bar.qtpl:112
+ qw422016.N().S(` </td>
+ <td></td>
+ </tr>
+ </table>
+ </form>
+ </body>
+</html>
+`)
+//line stream-top-bar.qtpl:120
+}
+
+//line stream-top-bar.qtpl:120
+func WriteRenderTopBar(qq422016 qtio422016.Writer, CSRF string, Data chatTopBarData, AuthUser *database.User) {
+//line stream-top-bar.qtpl:120
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line stream-top-bar.qtpl:120
+ StreamRenderTopBar(qw422016, CSRF, Data, AuthUser)
+//line stream-top-bar.qtpl:120
+ qt422016.ReleaseWriter(qw422016)
+//line stream-top-bar.qtpl:120
+}
+
+//line stream-top-bar.qtpl:120
+func RenderTopBar(CSRF string, Data chatTopBarData, AuthUser *database.User) string {
+//line stream-top-bar.qtpl:120
+ qb422016 := qt422016.AcquireByteBuffer()
+//line stream-top-bar.qtpl:120
+ WriteRenderTopBar(qb422016, CSRF, Data, AuthUser)
+//line stream-top-bar.qtpl:120
+ qs422016 := string(qb422016.B)
+//line stream-top-bar.qtpl:120
+ qt422016.ReleaseByteBuffer(qb422016)
+//line stream-top-bar.qtpl:120
+ return qs422016
+//line stream-top-bar.qtpl:120
+}
diff --git a/pkg/web/handlers/api/v1/topBarHandler.go b/pkg/web/handlers/api/v1/topBarHandler.go
@@ -9,6 +9,7 @@ import (
"dkforest/pkg/web/handlers/interceptors"
"dkforest/pkg/web/handlers/interceptors/command"
"dkforest/pkg/web/handlers/streamModals"
+ "dkforest/pkg/web/handlers/utils/stream"
"fmt"
"github.com/dustin/go-humanize"
"github.com/labstack/echo"
@@ -130,6 +131,7 @@ func buildCommandsList(authUser *database.User, room database.ChatRoom) (command
func ChatTopBarHandler(c echo.Context) error {
authUser := c.Get("authUser").(*database.User)
db := c.Get("database").(*database.DkfDB)
+ csrf, _ := c.Get("csrf").(string)
var data chatTopBarData
data.RoomName = c.Param("roomName")
@@ -174,6 +176,17 @@ func ChatTopBarHandler(c echo.Context) error {
// GET requests stops here
if c.Request().Method != http.MethodPost {
+ if authUser.UseStreamTopBar {
+ streamItem, err := stream.SetStreaming(c, authUser.ID, "")
+ if err != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ defer streamItem.Cleanup()
+ _, _ = c.Response().Write([]byte(RenderTopBar(csrf, data, authUser)))
+ c.Response().Flush()
+ <-streamItem.Quit
+ return c.NoContent(http.StatusOK)
+ }
return c.Render(http.StatusOK, "chat-top-bar", data)
}
diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go
@@ -678,6 +678,7 @@ type settingsChatData struct {
DisplayHellbanButton bool
UseStream bool
UseStreamMenu bool
+ UseStreamTopBar bool
DisplayAliveIndicator bool
ManualMultiline bool
ConfirmExternalLinks bool
diff --git a/pkg/web/handlers/settings.go b/pkg/web/handlers/settings.go
@@ -66,6 +66,7 @@ func SettingsChatHandler(c echo.Context) error {
data.NotifyChessMove = authUser.NotifyChessMove
data.UseStream = authUser.UseStream
data.UseStreamMenu = authUser.UseStreamMenu
+ data.UseStreamTopBar = authUser.UseStreamTopBar
data.DisplayAliveIndicator = authUser.DisplayAliveIndicator
data.ConfirmExternalLinks = authUser.ConfirmExternalLinks
data.ChessSoundsEnabled = authUser.ChessSoundsEnabled
@@ -117,6 +118,7 @@ func changeSettingsForm(c echo.Context, data settingsChatData) error {
data.NotifyChessMove = utils.DoParseBool(c.Request().PostFormValue("notify_chess_move"))
data.UseStream = utils.DoParseBool(c.Request().PostFormValue("use_stream"))
data.UseStreamMenu = utils.DoParseBool(c.Request().PostFormValue("use_stream_menu"))
+ data.UseStreamTopBar = utils.DoParseBool(c.Request().PostFormValue("use_stream_top_bar"))
data.DisplayAliveIndicator = utils.DoParseBool(c.Request().PostFormValue("display_alive_indicator"))
data.ConfirmExternalLinks = utils.DoParseBool(c.Request().PostFormValue("confirm_external_links"))
data.ChessSoundsEnabled = utils.DoParseBool(c.Request().PostFormValue("chess_sounds_enabled"))
@@ -166,6 +168,7 @@ func changeSettingsForm(c echo.Context, data settingsChatData) error {
authUser.NotifyChessMove = data.NotifyChessMove
authUser.UseStream = data.UseStream
authUser.UseStreamMenu = data.UseStreamMenu
+ authUser.UseStreamTopBar = data.UseStreamTopBar
authUser.DisplayAliveIndicator = data.DisplayAliveIndicator
authUser.ConfirmExternalLinks = data.ConfirmExternalLinks
authUser.ChessSoundsEnabled = data.ChessSoundsEnabled
diff --git a/pkg/web/middlewares/middlewares.go b/pkg/web/middlewares/middlewares.go
@@ -36,6 +36,7 @@ var GzipMiddleware = middleware.GzipWithConfig(
c.Path() == "/poker/:roomID/bet" ||
c.Path() == "/api/v1/chat/messages/:roomName/stream" ||
c.Path() == "/api/v1/chat/messages/:roomName/stream/menu" ||
+ c.Path() == "/api/v1/chat/top-bar/:roomName" ||
c.Path() == "/uploads/:filename" ||
c.Path() == "/" {
return true
diff --git a/pkg/web/public/views/pages/settings/chat.gohtml b/pkg/web/public/views/pages/settings/chat.gohtml
@@ -212,6 +212,13 @@
</div>
<div class="form-check form-check-1">
<div class="checkbox-wrapper form-check-input">
+ <input class="my-cbx" type="checkbox" name="use_stream_top_bar" id="use_stream_top_bar" value="1"{{ if .Data.UseStreamTopBar }} checked{{ end }} />
+ <label for="use_stream_top_bar" class="toggle"><span></span></label>
+ </div>
+ <label class="form-check-label" for="use_stream_top_bar">{{ t "Use chat top-bar stream version" . }}</label>
+ </div>
+ <div class="form-check form-check-1">
+ <div class="checkbox-wrapper form-check-input">
<input class="my-cbx" type="checkbox" name="display_alive_indicator" id="display_alive_indicator" value="1"{{ if .Data.DisplayAliveIndicator }} checked{{ end }} />
<label for="display_alive_indicator" class="toggle"><span></span></label>
</div>