feat: pwa

This commit is contained in:
codytseng
2024-12-22 16:37:38 +08:00
parent 869e164469
commit c5b0c0543a
23 changed files with 3825 additions and 51 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
dev-dist
*.local
# Editor directories and files

View File

@@ -2,20 +2,15 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="/favicon-light.svg"
media="(prefers-color-scheme: light)"
type="image/svg+xml"
/>
<link
rel="icon"
href="/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
type="image/svg+xml"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-title" content="Jumble" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<meta property="og:url" content="https://jumble.social" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Jumble" />
@@ -28,6 +23,16 @@
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true"
/>
<title>Jumble</title>
<style>
body {
background-color: #FFFFFF;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000000;
}
}
</style>
</head>
<body>
<div id="root"></div>

3730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "jumble",
"version": "0.1.0",
"description": "Yet another Nostr desktop client",
"description": "A beautiful nostr client focused on browsing relay feeds",
"private": true,
"type": "module",
"author": "codytseng",
@@ -70,6 +70,7 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.1",
"vite": "^6.0.3"
"vite": "^6.0.3",
"vite-plugin-pwa": "^0.21.1"
}
}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1080 1228" version="1.1" fill="#ffffff" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path id="path1" d="M360.047,1225.75c-31.046,-3.901 -75.11,-14.46 -106.756,-25.58c-101.676,-35.727 -175.164,-93.066 -215.387,-168.055c-12.079,-22.521 -30.071,-71.422 -27.297,-74.195c0.736,-0.736 11.648,5.578 24.249,14.031c135.436,90.86 301.047,169.043 465.056,219.547l32.77,10.091l-20.27,7.416c-43.455,15.896 -105.159,22.678 -152.365,16.745Zm166.293,-59.234c-168.523,-50.004 -331.475,-126.514 -481.755,-226.196c-37.737,-25.031 -41.489,-28.372 -43.419,-38.663c-3.585,-19.109 1.498,-83.894 9.798,-124.886c7.343,-36.266 27.664,-106.034 32.278,-110.818c2.023,-2.099 217.924,48.207 221.274,51.557c0.975,0.975 -1.132,11.339 -4.682,23.032c-24.542,80.842 -27.217,127.586 -9.935,173.593c22.507,59.917 114.521,99.888 177.281,77.012c29.23,-10.654 56.593,-41.085 82.629,-91.894c29.288,-57.155 32.348,-64.988 196.483,-503.076c81.138,-216.562 148.499,-394.821 149.692,-396.131c2.1,-2.304 217.949,76.926 223.076,81.884c2.056,1.988 -262.476,712.505 -307.806,826.747c-18.422,46.426 -56.939,123.045 -77.918,154.993c-10.157,15.469 -30.753,40.901 -45.769,56.515c-27.821,28.93 -66.46,58.952 -75.447,58.621c-2.738,-0.106 -23.339,-5.631 -45.78,-12.29Z" style="fill-rule:nonzero;"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1080 1228" version="1.1" fill="#000000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path id="path1" d="M360.047,1225.75c-31.046,-3.901 -75.11,-14.46 -106.756,-25.58c-101.676,-35.727 -175.164,-93.066 -215.387,-168.055c-12.079,-22.521 -30.071,-71.422 -27.297,-74.195c0.736,-0.736 11.648,5.578 24.249,14.031c135.436,90.86 301.047,169.043 465.056,219.547l32.77,10.091l-20.27,7.416c-43.455,15.896 -105.159,22.678 -152.365,16.745Zm166.293,-59.234c-168.523,-50.004 -331.475,-126.514 -481.755,-226.196c-37.737,-25.031 -41.489,-28.372 -43.419,-38.663c-3.585,-19.109 1.498,-83.894 9.798,-124.886c7.343,-36.266 27.664,-106.034 32.278,-110.818c2.023,-2.099 217.924,48.207 221.274,51.557c0.975,0.975 -1.132,11.339 -4.682,23.032c-24.542,80.842 -27.217,127.586 -9.935,173.593c22.507,59.917 114.521,99.888 177.281,77.012c29.23,-10.654 56.593,-41.085 82.629,-91.894c29.288,-57.155 32.348,-64.988 196.483,-503.076c81.138,-216.562 148.499,-394.821 149.692,-396.131c2.1,-2.304 217.949,76.926 223.076,81.884c2.056,1.988 -262.476,712.505 -307.806,826.747c-18.422,46.426 -56.939,123.045 -77.918,154.993c-10.157,15.469 -30.753,40.901 -45.769,56.515c-27.821,28.93 -66.46,58.952 -75.447,58.621c-2.738,-0.106 -23.339,-5.631 -45.78,-12.29Z" style="fill-rule:nonzero;"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

1
public/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#000" d="M209.757 411.798c-7.912-.995-19.141-3.69-27.206-6.527-25.912-9.117-44.641-23.75-54.891-42.886-3.079-5.747-7.664-18.226-6.957-18.934.188-.188 2.969 1.424 6.18 3.581 34.516 23.186 76.721 43.138 118.519 56.026l8.351 2.575-5.166 1.892c-11.074 4.057-26.799 5.788-38.83 4.273Zm42.38-15.115c-42.948-12.761-84.476-32.285-122.775-57.723-9.617-6.388-10.573-7.24-11.065-9.866-.914-4.877.382-21.409 2.497-31.87 1.871-9.255 7.05-27.059 8.226-28.279.516-.536 55.538 12.301 56.391 13.156.249.249-.288 2.894-1.193 5.878-6.254 20.63-6.936 32.558-2.532 44.299 5.736 15.29 29.186 25.49 45.18 19.652 7.449-2.718 14.423-10.484 21.058-23.45 7.464-14.585 8.244-16.584 50.074-128.379 20.677-55.264 37.844-100.754 38.148-101.088.536-.588 55.544 19.63 56.851 20.895.524.508-66.892 181.824-78.444 210.977-4.695 11.847-14.511 31.4-19.857 39.552-2.589 3.948-7.838 10.438-11.664 14.422-7.091 7.383-16.938 15.044-19.228 14.96-.698-.027-5.948-1.437-11.667-3.136Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/pwa-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/pwa-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@@ -32,7 +32,7 @@ export default function ProfileButton({
if (variant === 'titlebar') {
triggerComponent = (
<button>
<Avatar className="w-7 h-7 hover:opacity-90">
<Avatar className="ml-2 w-6 h-6 hover:opacity-90">
<AvatarImage src={avatar} />
<AvatarFallback>
<img src={defaultAvatar} />

View File

@@ -19,7 +19,7 @@ export default function ScrollToTopButton({
<Button
variant="secondary-2"
className={cn(
`absolute bottom-2 right-2 rounded-full w-11 h-11 p-0 hover:text-background transition-transform ${visible ? '' : 'translate-y-14'}`,
`absolute bottom-6 right-6 rounded-full w-12 h-12 p-0 hover:text-background transition-transform ${visible ? '' : 'translate-y-20'}`,
className
)}
onClick={handleScrollToTop}

View File

@@ -6,7 +6,6 @@ import AboutInfoDialog from '../AboutInfoDialog'
import AccountButton from '../AccountButton'
import NotificationButton from '../NotificationButton'
import PostButton from '../PostButton'
import RefreshButton from '../RefreshButton'
import RelaySettingsButton from '../RelaySettingsButton'
import SearchButton from '../SearchButton'
@@ -23,7 +22,6 @@ export default function PrimaryPageSidebar() {
<RelaySettingsButton variant="sidebar" />
<NotificationButton variant="sidebar" />
<SearchButton variant="sidebar" />
<RefreshButton variant="sidebar" />
<AboutInfoDialog>
<Button variant="sidebar" size="sidebar">
<Info />

View File

@@ -19,8 +19,6 @@
body {
font-family: 'Inter', sans-serif;
color: hsl(var(--foreground));
background-color: hsl(var(--background));
overflow: hidden;
-webkit-overflow-scrolling: touch;
text-size-adjust: 100%;

View File

@@ -2,7 +2,6 @@ import Logo from '@/assets/Logo'
import AccountButton from '@/components/AccountButton'
import NotificationButton from '@/components/NotificationButton'
import PostButton from '@/components/PostButton'
import RefreshButton from '@/components/RefreshButton'
import RelaySettingsButton from '@/components/RelaySettingsButton'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import SearchButton from '@/components/SearchButton'
@@ -62,7 +61,7 @@ const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode
>
<PrimaryPageTitlebar visible={visible} />
<div className="sm:px-4 pb-4 pt-11 xl:pt-4">{children}</div>
<ScrollToTopButton scrollAreaRef={scrollAreaRef} visible={visible} />
<ScrollToTopButton scrollAreaRef={scrollAreaRef} visible={visible && lastScrollTop > 500} />
</ScrollArea>
)
})
@@ -107,7 +106,6 @@ function PrimaryPageTitlebar({ visible = true }: { visible?: boolean }) {
<SearchButton />
</div>
<div className="flex gap-2 items-center">
<RefreshButton />
<RelaySettingsButton />
<NotificationButton />
</div>

View File

@@ -58,7 +58,7 @@ export default function SecondaryPageLayout({
<div className="sm:px-4 pb-4 pt-11 w-full h-full">{children}</div>
<ScrollToTopButton
scrollAreaRef={scrollAreaRef}
visible={!hideScrollToTopButton && visible}
visible={!hideScrollToTopButton && visible && lastScrollTop > 500}
/>
</ScrollArea>
)

View File

@@ -41,20 +41,16 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
useEffect(() => {
const init = async () => {
const searchParams = new URLSearchParams(window.location.search)
const tempRelays = searchParams
.getAll('r')
.filter((url) => isWebsocketUrl(url))
.map((url) => normalizeUrl(url))
if (tempRelays.length) {
setTemporaryRelayUrls(tempRelays)
}
const storedGroups = await storage.getRelayGroups()
setRelayGroups(storedGroups)
const searchParams = new URLSearchParams(window.location.search)
const tempRelays = searchParams
.getAll('r')
.filter((url) => isWebsocketUrl(url))
.map((url) => normalizeUrl(url))
if (tempRelays.length) {
setTemporaryRelayUrls(tempRelays)
}
init()
const storedGroups = storage.getRelayGroups()
setRelayGroups(storedGroups)
}, [])
useEffect(() => {
@@ -63,25 +59,25 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
? temporaryRelayUrls
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
if (JSON.stringify(relayUrls) !== JSON.stringify(newRelayUrls)) {
setRelayUrls(newRelayUrls)
}
const relayInfos = await client.fetchRelayInfos(newRelayUrls)
setSearchableRelayUrls(newRelayUrls.filter((_, index) => checkSearchRelay(relayInfos[index])))
const nonAlgoRelayUrls = newRelayUrls.filter((_, index) => !checkAlgoRelay(relayInfos[index]))
setAreAlgoRelays(newRelayUrls.length > 0 && nonAlgoRelayUrls.length === 0)
client.setCurrentRelayUrls(nonAlgoRelayUrls)
if (JSON.stringify(relayUrls) !== JSON.stringify(newRelayUrls)) {
setRelayUrls(newRelayUrls)
}
}
handler()
}, [relayGroups, temporaryRelayUrls, relayUrls])
const updateGroups = async (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => {
const updateGroups = (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => {
let newGroups = relayGroups
setRelayGroups((pre) => {
newGroups = fn(pre)
return newGroups
})
await storage.setRelayGroups(newGroups)
storage.setRelayGroups(newGroups)
}
const switchRelayGroup = (groupName: string) => {

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from 'react'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
type TScreenSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
@@ -18,8 +18,8 @@ export const useScreenSize = () => {
}
export function ScreenSizeProvider({ children }: { children: React.ReactNode }) {
const [screenSize, setScreenSize] = useState<TScreenSize>('xl')
const isSmallScreen = screenSize === 'sm'
const [screenSize, setScreenSize] = useState<TScreenSize>('sm')
const isSmallScreen = useMemo(() => screenSize === 'sm', [screenSize])
useEffect(() => {
const handleResize = () => {

View File

@@ -1,13 +1,63 @@
import react from '@vitejs/plugin-react'
import path from 'path'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
},
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,png,jpg,svg}'],
globDirectory: 'dist/',
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
cleanupOutdatedCaches: true
},
devOptions: {
enabled: true
},
manifest: {
name: 'Jumble',
short_name: 'Jumble',
icons: [
{
src: '/pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any'
},
{
src: '/pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any'
},
{
src: '/pwa-maskable-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable'
},
{
src: '/pwa-maskable-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable'
}
],
start_url: '/',
display: 'standalone',
background_color: '#FFFFFF',
theme_color: '#FFFFFF',
description: 'A beautiful nostr client focused on browsing relay feeds'
}
})
]
})