Add Firefox for Android PWA support #634 #635

open
opened by vielle.dev targeting master

the images could be generated from the dolly svg but that would probably require bringing in a large dependency like inkscape and wouldn't bring huge benifits imo (&& just sticking it in tree was mentioned as best in the discord)

Changed files
+112 -15
appview
nix
+1
appview/state/router.go
··· 35 35 router.Get("/favicon.svg", s.Favicon) 36 36 router.Get("/favicon.ico", s.Favicon) 37 37 router.Get("/pwa-manifest.json", s.PWAManifest) 38 + router.Get("/pwa-icon.png", s.PWAIcon) 38 39 router.Get("/robots.txt", s.RobotsTxt) 39 40 40 41 userRouter := s.UserRouter(&middleware)
+110 -15
appview/state/state.go
··· 1 1 package state 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "database/sql" 6 7 "errors" 7 8 "fmt" 9 + "image" 10 + "image/color" 11 + "image/draw" 12 + "image/png" 8 13 "log/slog" 9 14 "net/http" 15 + "strconv" 10 16 "strings" 11 17 "time" 12 18 ··· 20 26 dbnotify "tangled.org/core/appview/notify/db" 21 27 phnotify "tangled.org/core/appview/notify/posthog" 22 28 "tangled.org/core/appview/oauth" 29 + "tangled.org/core/appview/ogcard" 23 30 "tangled.org/core/appview/pages" 24 31 "tangled.org/core/appview/reporesolver" 25 32 "tangled.org/core/appview/validator" ··· 39 46 securejoin "github.com/cyphar/filepath-securejoin" 40 47 "github.com/go-chi/chi/v5" 41 48 "github.com/posthog/posthog-go" 49 + "github.com/srwiley/rasterx" 42 50 ) 43 51 44 52 type State struct { ··· 221 229 222 230 // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 223 231 const manifestJson = `{ 224 - "name": "tangled", 225 - "description": "tightly-knit social coding.", 226 - "icons": [ 227 - { 228 - "src": "/favicon.svg", 229 - "sizes": "144x144" 230 - } 231 - ], 232 - "start_url": "/", 233 - "id": "org.tangled", 234 - 235 - "display": "standalone", 236 - "background_color": "#111827", 237 - "theme_color": "#111827" 232 + "name": "tangled", 233 + "description": "tightly-knit social coding.", 234 + "start_url": "/", 235 + "id": "org.tangled", 236 + "display": "standalone", 237 + "background_color": "#111827", 238 + "theme_color": "#111827", 239 + "icons": [ 240 + { 241 + "src": "/pwa-icon.png?res=512&transparent=true", 242 + "type": "image/png", 243 + "sizes": "512x512", 244 + "purpose": "any monchrome" 245 + }, 246 + { 247 + "src": "/pwa-icon.png?res=512", 248 + "type": "image/png", 249 + "sizes": "512x512", 250 + "purpose": "maskable" 251 + }, 252 + { 253 + "src": "/pwa-icon.png?res=192", 254 + "type": "image/png", 255 + "sizes": "192x192", 256 + "purpose": "maskable" 257 + }, 258 + { 259 + "src": "/pwa-icon.png?res=144", 260 + "type": "image/png", 261 + "sizes": "144x144", 262 + "purpose": "maskable" 263 + }, 264 + { 265 + "src": "/pwa-icon.png?res=96", 266 + "type": "image/png", 267 + "sizes": "96x96", 268 + "purpose": "maskable" 269 + }, 270 + { 271 + "src": "/pwa-icon.png?res=72", 272 + "type": "image/png", 273 + "sizes": "72x72", 274 + "purpose": "maskable" 275 + }, 276 + { 277 + "src": "/pwa-icon.png?res=48", 278 + "type": "image/png", 279 + "sizes": "48x48", 280 + "purpose": "maskable" 281 + } 282 + ] 238 283 }` 239 284 240 - func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) { 285 + func (s *State) PWAManifest(w http.ResponseWriter, r *http.Request) { 241 286 w.Header().Set("Content-Type", "application/json") 242 287 w.Write([]byte(manifestJson)) 243 288 } 244 289 290 + 291 + func (s *State) PWAIcon(w http.ResponseWriter, r *http.Request) { 292 + tangledBgColour := color.RGBA{0x11, 0x18, 0x27, 0xff} 293 + etag := "W/\"dolly-logo-v1 " + r.URL.Query().Get("res") + r.URL.Query().Get("transparent") + "\"" 294 + 295 + w.Header().Set("Content-Type", "image/png") 296 + w.Header().Set("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400, stale-if-error=86400") 297 + w.Header().Set("Etag", etag) 298 + // if the client already has the same logo dont bother recreating it 299 + if len(r.Header["If-None-Match"]) == 1 && r.Header["If-None-Match"][0] == etag { 300 + w.WriteHeader(304) 301 + return 302 + } 303 + 304 + icon, err := ogcard.BuildSVGIconFromPath("templates/fragments/dolly/logo.svg", tangledBgColour) 305 + // ignore error as icon is default icon on error 306 + // also this should not fail 307 + 308 + transparent := r.URL.Query().Get("transparent") == "true"; 309 + size, err := strconv.Atoi(r.URL.Query().Get("res")) 310 + if err != nil { 311 + size = 512 312 + } 313 + 314 + // maskable safe area is centered circle with radius 40% 315 + // area outside may be cropped so make sure the icon fills it 316 + // bc trig, safe square is just over 70% of screen 317 + icon.SetTarget(float64(size) * 0.15, float64(size) * 0.15, float64(size) * 0.7, float64(size) * 0.7) 318 + rgba := image.NewRGBA(image.Rect(0, 0, size, size)) 319 + 320 + // set image bg 321 + var bgColour color.Color = tangledBgColour 322 + if transparent { 323 + bgColour = color.Transparent 324 + } 325 + draw.Draw(rgba, rgba.Bounds(), &image.Uniform{ bgColour }, image.Point{}, draw.Src) 326 + 327 + // Create a scanner and rasterize the SVG 328 + scanner := rasterx.NewScannerGV(size, size, rgba, rgba.Bounds()) 329 + raster := rasterx.NewDasher(size, size, scanner) 330 + 331 + icon.Draw(raster, 1.0) 332 + 333 + var img_buff bytes.Buffer 334 + err = png.Encode(&img_buff, rgba) 335 + // ignore error as encoding shouldnt fail 336 + // if it fails itll return no bytes which is fine 337 + w.Write(img_buff.Bytes()) 338 + } 339 + 245 340 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) { 246 341 user := s.oauth.GetUser(r) 247 342 s.pages.TermsOfService(w, pages.TermsOfServiceParams{
+1
nix/pkgs/appview-static-files.nix
··· 17 17 (allow file-read* (subpath "/System/Library/OpenSSL")) 18 18 ''; 19 19 } '' 20 + #!/bin/bash 20 21 mkdir -p $out/{fonts,icons} && cd $out 21 22 cp -f ${htmx-src} htmx.min.js 22 23 cp -f ${htmx-ws-src} htmx-ext-ws.min.js