Browse Source

feat: pwa

imwald
codytseng 1 year ago
parent
commit
c5b0c0543a
  1. 1
      .gitignore
  2. 29
      index.html
  3. 10338
      package-lock.json
  4. 5
      package.json
  5. BIN
      public/apple-touch-icon.png
  6. 1
      public/favicon-dark.svg
  7. 1
      public/favicon-light.svg
  8. BIN
      public/favicon.ico
  9. 1
      public/favicon.svg
  10. BIN
      public/pwa-192x192.png
  11. BIN
      public/pwa-512x512.png
  12. BIN
      public/pwa-maskable-192x192.png
  13. BIN
      public/pwa-maskable-512x512.png
  14. 2
      public/robots.txt
  15. 2
      src/components/AccountButton/ProfileButton.tsx
  16. 2
      src/components/ScrollToTopButton/index.tsx
  17. 2
      src/components/Sidebar/index.tsx
  18. 2
      src/index.css
  19. 4
      src/layouts/PrimaryPageLayout/index.tsx
  20. 2
      src/layouts/SecondaryPageLayout/index.tsx
  21. 32
      src/providers/RelaySettingsProvider.tsx
  22. 6
      src/providers/ScreenSizeProvider.tsx
  23. 54
      vite.config.ts

1
.gitignore vendored

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

29
index.html

@ -2,20 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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="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:url" content="https://jumble.social" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Jumble" /> <meta property="og:title" content="Jumble" />
@ -28,6 +23,16 @@
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true" content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true"
/> />
<title>Jumble</title> <title>Jumble</title>
<style>
body {
background-color: #FFFFFF;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000000;
}
}
</style>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

10338
package-lock.json generated

File diff suppressed because it is too large Load Diff

5
package.json

@ -1,7 +1,7 @@
{ {
"name": "jumble", "name": "jumble",
"version": "0.1.0", "version": "0.1.0",
"description": "Yet another Nostr desktop client", "description": "A beautiful nostr client focused on browsing relay feeds",
"private": true, "private": true,
"type": "module", "type": "module",
"author": "codytseng", "author": "codytseng",
@ -70,6 +70,7 @@
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.1", "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

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

1
public/favicon-dark.svg

@ -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

1
public/favicon-light.svg

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

1
public/favicon.svg

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/pwa-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/pwa-maskable-192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
public/pwa-maskable-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

2
public/robots.txt

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

2
src/components/AccountButton/ProfileButton.tsx

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

2
src/components/ScrollToTopButton/index.tsx

@ -19,7 +19,7 @@ export default function ScrollToTopButton({
<Button <Button
variant="secondary-2" variant="secondary-2"
className={cn( 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 className
)} )}
onClick={handleScrollToTop} onClick={handleScrollToTop}

2
src/components/Sidebar/index.tsx

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

2
src/index.css

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

4
src/layouts/PrimaryPageLayout/index.tsx

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

2
src/layouts/SecondaryPageLayout/index.tsx

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

32
src/providers/RelaySettingsProvider.tsx

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

6
src/providers/ScreenSizeProvider.tsx

@ -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' type TScreenSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
@ -18,8 +18,8 @@ export const useScreenSize = () => {
} }
export function ScreenSizeProvider({ children }: { children: React.ReactNode }) { export function ScreenSizeProvider({ children }: { children: React.ReactNode }) {
const [screenSize, setScreenSize] = useState<TScreenSize>('xl') const [screenSize, setScreenSize] = useState<TScreenSize>('sm')
const isSmallScreen = screenSize === 'sm' const isSmallScreen = useMemo(() => screenSize === 'sm', [screenSize])
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {

54
vite.config.ts

@ -1,13 +1,63 @@
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src') '@': 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'
}
})
]
}) })

Loading…
Cancel
Save