Browse Source

add a pwa

master
Silberengel 1 month ago
parent
commit
2e7c1a7583
  1. 25
      httpd.conf.template
  2. 11844
      package-lock.json
  3. 1
      package.json
  4. 4
      public/healthz.json
  5. 3
      src/app.html
  6. 2
      src/lib/components/content/MetadataCard.svelte
  7. 2
      src/lib/modules/comments/Comment.svelte
  8. 2
      src/lib/modules/discussions/DiscussionCard.svelte
  9. 4
      src/lib/modules/feed/FeedPost.svelte
  10. 2
      src/lib/modules/feed/HighlightCard.svelte
  11. 2
      src/lib/modules/feed/Reply.svelte
  12. 2
      src/lib/modules/feed/ZapReceiptReply.svelte
  13. 56
      src/routes/manifest.webmanifest/+server.ts
  14. 97
      src/routes/settings/+page.svelte
  15. 2
      svelte.config.js
  16. 67
      vite.config.ts

25
httpd.conf.template

@ -5,6 +5,7 @@ LoadModule authz_core_module modules/mod_authz_core.so @@ -5,6 +5,7 @@ LoadModule authz_core_module modules/mod_authz_core.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule dir_module modules/mod_dir.so
LoadModule deflate_module modules/mod_deflate.so
PidFile "/usr/local/apache2/logs/httpd.pid"
ErrorLog "/proc/self/fd/2"
@ -47,4 +48,28 @@ RewriteRule ^/healthz$ /healthz.json [L] @@ -47,4 +48,28 @@ RewriteRule ^/healthz$ /healthz.json [L]
<IfModule mod_headers.c>
Header set Service-Worker-Allowed "/"
# Cache static assets aggressively (they have hashed filenames)
<LocationMatch "^/_app/immutable/">
Header set Cache-Control "public, max-age=31536000, immutable"
</LocationMatch>
# Cache other static assets
<LocationMatch "\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|webp|avif)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</LocationMatch>
# Don't cache HTML files (they need to be fresh for updates)
<LocationMatch "\.html$">
Header set Cache-Control "no-cache, must-revalidate"
</LocationMatch>
</IfModule>
# Enable compression for text-based files
<IfModule mod_deflate.c>
<Location />
SetOutputFilter DEFLATE
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png|zip|gz|bz2|sit|rar|webp|avif)$ no-gzip dont-vary
</Location>
</IfModule>

11844
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
package.json

@ -54,6 +54,7 @@ @@ -54,6 +54,7 @@
"@types/marked": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vite-pwa/sveltekit": "^1.1.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.2.0",
"buildTime": "2026-02-07T00:27:17.672Z",
"buildTime": "2026-02-07T06:18:18.476Z",
"gitCommit": "unknown",
"timestamp": 1770424037672
"timestamp": 1770445098476
}

3
src/app.html

@ -39,6 +39,9 @@ @@ -39,6 +39,9 @@
<meta name="theme-color" content="#f1f5f9" />
<meta name="color-scheme" content="light dark" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.webmanifest">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

2
src/lib/components/content/MetadataCard.svelte

@ -92,7 +92,7 @@ @@ -92,7 +92,7 @@
<div class="flex items-center gap-2">
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(event))}
/>

2
src/lib/modules/comments/Comment.svelte

@ -125,7 +125,7 @@ @@ -125,7 +125,7 @@
<div class="ml-auto flex items-center gap-2 comment-header-actions">
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(comment))}
/>

2
src/lib/modules/discussions/DiscussionCard.svelte

@ -230,7 +230,7 @@ @@ -230,7 +230,7 @@
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.875em;">{getRelativeTime()}</span>
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(thread))}
/>

4
src/lib/modules/feed/FeedPost.svelte

@ -823,7 +823,7 @@ @@ -823,7 +823,7 @@
{/if}
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(post))}
/>
@ -1003,7 +1003,7 @@ @@ -1003,7 +1003,7 @@
{/if}
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(post))}
/>

2
src/lib/modules/feed/HighlightCard.svelte

@ -331,7 +331,7 @@ @@ -331,7 +331,7 @@
{/if}
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(highlight))}
/>

2
src/lib/modules/feed/Reply.svelte

@ -98,7 +98,7 @@ @@ -98,7 +98,7 @@
<div class="ml-auto flex items-center gap-2">
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(reply))}
/>

2
src/lib/modules/feed/ZapReceiptReply.svelte

@ -109,7 +109,7 @@ @@ -109,7 +109,7 @@
<div class="ml-auto flex items-center gap-2">
<IconButton
icon="eye"
label="View event"
label="View"
size={16}
onclick={() => goto(getEventLink(zapReceipt))}
/>

56
src/routes/manifest.webmanifest/+server.ts

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
import type { RequestHandler } from '@sveltejs/kit';
export const prerender = true;
export const GET: RequestHandler = async () => {
const manifest = {
name: 'aitherboard - Decentralized Messageboard on Nostr',
short_name: 'aitherboard',
description: 'A decentralized messageboard built on the Nostr protocol. Create threads, comment, react, and zap in a censorship-resistant environment.',
theme_color: '#f1f5f9',
background_color: '#ffffff',
display: 'standalone',
start_url: '/',
scope: '/',
icons: [
{
src: '/favicon.ico',
sizes: '64x64',
type: 'image/x-icon'
},
{
src: '/apple-touch-icon-180x180.png',
sizes: '180x180',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/apple-touch-icon-152x152.png',
sizes: '152x152',
type: 'image/png'
},
{
src: '/apple-touch-icon-144x144.png',
sizes: '144x144',
type: 'image/png'
},
{
src: '/apple-touch-icon-120x120.png',
sizes: '120x120',
type: 'image/png'
},
{
src: '/apple-touch-icon-114x114.png',
sizes: '114x114',
type: 'image/png'
}
]
};
return new Response(JSON.stringify(manifest, null, 2), {
headers: {
'Content-Type': 'application/manifest+json',
'Cache-Control': 'public, max-age=3600'
}
});
};

97
src/routes/settings/+page.svelte

@ -18,6 +18,17 @@ @@ -18,6 +18,17 @@
let expiringEvents = $state(false);
let includeClientTag = $state(true);
// PWA install state
let deferredPrompt = $state<BeforeInstallPromptEvent | null>(null);
let showInstallButton = $state(false);
let isInstalled = $state(false);
// Type for beforeinstallprompt event
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
onMount(() => {
// Read current preferences from localStorage (preferred) or DOM (fallback)
const storedTextSize = localStorage.getItem('textSize') as TextSize | null;
@ -45,6 +56,38 @@ @@ -45,6 +56,38 @@
// Load client tag preference
includeClientTag = shouldIncludeClientTag();
// Check if PWA is already installed
if (typeof window !== 'undefined') {
// Check if running in standalone mode (installed PWA)
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
const isIOSStandalone = (window.navigator as any).standalone === true;
isInstalled = isStandalone || isIOSStandalone;
// Listen for beforeinstallprompt event
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
deferredPrompt = e as BeforeInstallPromptEvent;
showInstallButton = true;
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
// Listen for appinstalled event
const handleAppInstalled = () => {
isInstalled = true;
showInstallButton = false;
deferredPrompt = null;
};
window.addEventListener('appinstalled', handleAppInstalled);
// Cleanup
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}
});
function applyPreferences() {
@ -107,6 +150,30 @@ @@ -107,6 +150,30 @@
goto('/');
}
}
async function handlePWAInstall() {
if (!deferredPrompt) return;
try {
// Show the install prompt
await deferredPrompt.prompt();
// Wait for user response
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
// Clear the deferred prompt
deferredPrompt = null;
showInstallButton = false;
} catch (error) {
console.error('Error showing install prompt:', error);
}
}
</script>
<Header />
@ -289,6 +356,36 @@ @@ -289,6 +356,36 @@
</p>
</div>
<!-- PWA Install -->
<div class="preference-section bg-fog-post dark:bg-fog-dark-post border border-fog-border dark:border-fog-dark-border p-4 rounded">
<div class="preference-label mb-3">
<span class="font-semibold text-fog-text dark:text-fog-dark-text">Install App</span>
</div>
<div class="preference-controls">
{#if isInstalled}
<div class="flex items-center gap-2 text-fog-text dark:text-fog-dark-text">
<Icon name="check" size={16} />
<span>App is installed</span>
</div>
{:else if showInstallButton}
<button
onclick={handlePWAInstall}
class="toggle-button"
aria-label="Install aitherboard as a Progressive Web App"
>
<span>📱 Install App</span>
</button>
{:else}
<p class="text-fog-text-light dark:text-fog-dark-text-light" style="font-size: 0.875em;">
Install prompt not available. Make sure you're using a supported browser and the app meets PWA requirements.
</p>
{/if}
</div>
<p class="text-fog-text-light dark:text-fog-dark-text-light mt-2" style="font-size: 0.875em;">
Install aitherboard as a Progressive Web App to use it offline and access it from your home screen.
</p>
</div>
<!-- Keyboard Shortcuts -->
<div class="preference-section bg-fog-post dark:bg-fog-dark-post border border-fog-border dark:border-fog-dark-border p-4 rounded">
<div class="preference-label mb-3">

2
svelte.config.js

@ -14,7 +14,7 @@ const config = { @@ -14,7 +14,7 @@ const config = {
pages: 'build',
assets: 'build',
fallback: '200.html',
precompress: false,
precompress: true,
strict: true
}),
prerender: {

67
vite.config.ts

@ -1,10 +1,75 @@ @@ -1,10 +1,75 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { execSync } from 'child_process';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
export default defineConfig({
plugins: [
sveltekit(),
SvelteKitPWA({
strategies: 'generateSW',
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2,webp,avif}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/.*\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days
}
}
}
]
},
manifest: {
name: 'aitherboard - Decentralized Messageboard on Nostr',
short_name: 'aitherboard',
description: 'A decentralized messageboard built on the Nostr protocol. Create threads, comment, react, and zap in a censorship-resistant environment.',
theme_color: '#f1f5f9',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: 'favicon.ico',
sizes: '64x64',
type: 'image/x-icon'
},
{
src: 'apple-touch-icon-180x180.png',
sizes: '180x180',
type: 'image/png',
purpose: 'any maskable'
},
{
src: 'apple-touch-icon-152x152.png',
sizes: '152x152',
type: 'image/png'
},
{
src: 'apple-touch-icon-144x144.png',
sizes: '144x144',
type: 'image/png'
},
{
src: 'apple-touch-icon-120x120.png',
sizes: '120x120',
type: 'image/png'
},
{
src: 'apple-touch-icon-114x114.png',
sizes: '114x114',
type: 'image/png'
}
]
},
devOptions: {
enabled: false
}
}),
{
name: 'generate-healthz',
buildStart() {
@ -22,7 +87,7 @@ export default defineConfig({ @@ -22,7 +87,7 @@ export default defineConfig({
},
build: {
target: 'esnext',
sourcemap: true,
sourcemap: false,
manifest: false
}
});

Loading…
Cancel
Save