mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-29 12:09:05 +01:00
feat: apply disable animations before the first paint
This commit is contained in:
@@ -3,9 +3,11 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterFrontend(router *gin.Engine) error {
|
||||
func RegisterFrontend(router *gin.Engine, animationsDisabled func(ctx context.Context) bool) error {
|
||||
return ErrFrontendNotIncluded
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package frontend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -38,10 +39,11 @@ var precompressedExtensions = map[string]struct{}{
|
||||
}
|
||||
|
||||
// This function, created by the init() method, writes to "w" the index.html page, populating the nonce
|
||||
var writeIndexFn func(w io.Writer, nonce string) error
|
||||
var writeIndexFn func(w io.Writer, nonce string, noMotion bool) error
|
||||
|
||||
func init() {
|
||||
const scriptTag = "<script>"
|
||||
const htmlTag = `<html lang="en">`
|
||||
|
||||
// Read the index.html from the bundle
|
||||
index, iErr := fs.ReadFile(frontendFS, "dist/index.html")
|
||||
@@ -49,27 +51,27 @@ func init() {
|
||||
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
||||
}
|
||||
|
||||
writeIndexFn = func(w io.Writer, nonce string) (err error) {
|
||||
// If there's no nonce, write the index as-is
|
||||
if nonce == "" {
|
||||
_, err = w.Write(index)
|
||||
return err
|
||||
}
|
||||
writeIndexFn = func(w io.Writer, nonce string, noMotion bool) (err error) {
|
||||
page := index
|
||||
|
||||
// Add nonce to all <script> tags
|
||||
// We replace "<script" with `<script nonce="..."` everywhere it appears
|
||||
modified := bytes.ReplaceAll(
|
||||
index,
|
||||
[]byte(scriptTag),
|
||||
[]byte(`<script nonce="`+nonce+`">`),
|
||||
)
|
||||
if nonce != "" {
|
||||
page = bytes.ReplaceAll(page, []byte(scriptTag), []byte(`<script nonce="`+nonce+`">`))
|
||||
}
|
||||
|
||||
_, err = w.Write(modified)
|
||||
// Disable animations must hold from the first paint, so the served page carries it instead of waiting for the config request
|
||||
if noMotion {
|
||||
page = bytes.Replace(page, []byte(htmlTag), []byte(`<html lang="en" class="no-motion">`), 1)
|
||||
}
|
||||
|
||||
_, err = w.Write(page)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterFrontend(router *gin.Engine) error {
|
||||
// RegisterFrontend serves the SPA; animationsDisabled reports the Disable animations setting for each page load
|
||||
func RegisterFrontend(router *gin.Engine, animationsDisabled func(ctx context.Context) bool) error {
|
||||
distFS, err := fs.Sub(frontendFS, "dist")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sub FS: %w", err)
|
||||
@@ -99,7 +101,7 @@ func RegisterFrontend(router *gin.Engine) error {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Status(http.StatusOK)
|
||||
err = writeIndexFn(c.Writer, nonce)
|
||||
err = writeIndexFn(c.Writer, nonce, animationsDisabled(c.Request.Context()))
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsSPARequest(t *testing.T) {
|
||||
@@ -110,3 +112,14 @@ func TestFileServerWithCachingDoesNotCompressUnsupportedExtensions(t *testing.T)
|
||||
assert.Empty(t, res.Header().Get("Content-Encoding"))
|
||||
assert.Empty(t, res.Header().Get("Vary"))
|
||||
}
|
||||
|
||||
func TestWriteIndexMarksDisabledAnimations(t *testing.T) {
|
||||
var page bytes.Buffer
|
||||
require.NoError(t, writeIndexFn(&page, "abc", true))
|
||||
assert.Contains(t, page.String(), `<html lang="en" class="no-motion">`)
|
||||
assert.Contains(t, page.String(), `<script nonce="abc">`)
|
||||
|
||||
page.Reset()
|
||||
require.NoError(t, writeIndexFn(&page, "", false))
|
||||
assert.NotContains(t, page.String(), "no-motion")
|
||||
}
|
||||
|
||||
@@ -139,7 +139,10 @@ func registerGlobalMiddleware(r *gin.Engine) {
|
||||
|
||||
func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, auth *authz.Middleware, rateLimitServices map[string]*ratelimit.RateLimitService) error {
|
||||
|
||||
err := frontend.RegisterFrontend(r)
|
||||
err := frontend.RegisterFrontend(r, func(ctx context.Context) bool {
|
||||
cfg, err := svc.appConfigService.GetConfig(ctx)
|
||||
return err == nil && cfg.DisableAnimations.IsTrue()
|
||||
})
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||
} else if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user