mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-29 20:19:04 +01:00
feat: apply disable animations before the first paint
This commit is contained in:
@@ -3,9 +3,11 @@
|
|||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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
|
return ErrFrontendNotIncluded
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package frontend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"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
|
// 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() {
|
func init() {
|
||||||
const scriptTag = "<script>"
|
const scriptTag = "<script>"
|
||||||
|
const htmlTag = `<html lang="en">`
|
||||||
|
|
||||||
// Read the index.html from the bundle
|
// Read the index.html from the bundle
|
||||||
index, iErr := fs.ReadFile(frontendFS, "dist/index.html")
|
index, iErr := fs.ReadFile(frontendFS, "dist/index.html")
|
||||||
@@ -49,27 +51,27 @@ func init() {
|
|||||||
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
panic(fmt.Errorf("failed to read index.html: %w", iErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
writeIndexFn = func(w io.Writer, nonce string) (err error) {
|
writeIndexFn = func(w io.Writer, nonce string, noMotion bool) (err error) {
|
||||||
// If there's no nonce, write the index as-is
|
page := index
|
||||||
if nonce == "" {
|
|
||||||
_, err = w.Write(index)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add nonce to all <script> tags
|
// Add nonce to all <script> tags
|
||||||
// We replace "<script" with `<script nonce="..."` everywhere it appears
|
// We replace "<script" with `<script nonce="..."` everywhere it appears
|
||||||
modified := bytes.ReplaceAll(
|
if nonce != "" {
|
||||||
index,
|
page = bytes.ReplaceAll(page, []byte(scriptTag), []byte(`<script nonce="`+nonce+`">`))
|
||||||
[]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
|
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")
|
distFS, err := fs.Sub(frontendFS, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create sub FS: %w", err)
|
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("Content-Type", "text/html; charset=utf-8")
|
||||||
c.Header("Cache-Control", "no-store")
|
c.Header("Cache-Control", "no-store")
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
err = writeIndexFn(c.Writer, nonce)
|
err = writeIndexFn(c.Writer, nonce, animationsDisabled(c.Request.Context()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
_ = c.Error(fmt.Errorf("failed to write index.html file: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsSPARequest(t *testing.T) {
|
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("Content-Encoding"))
|
||||||
assert.Empty(t, res.Header().Get("Vary"))
|
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 {
|
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) {
|
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||||
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
slog.Warn("Frontend is not included in the build. Skipping frontend registration.")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user