Compare commits
3 commits
bea1d6abfa
...
7eb55e2a14
Author | SHA1 | Date | |
---|---|---|---|
|
7eb55e2a14 | ||
|
29b8b5307e | ||
|
cacca17dd8 |
11 changed files with 242 additions and 21 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
26
docs/optimizing.md
Normal file
26
docs/optimizing.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Optimizing Tutu
|
||||||
|
|
||||||
|
Topic Index:
|
||||||
|
|
||||||
|
- Time to first byte
|
||||||
|
- Time to first draw: [Load size](#load-size)
|
||||||
|
- CLS
|
||||||
|
- Framerate: [Algorithm](#algorithm)
|
||||||
|
|
||||||
|
## Load size
|
||||||
|
|
||||||
|
The baseline for the load size is lowest 3G download bandwidth in 2s, typically 1.1Mbps (or ~137 kilobytes/s) * 2s = 274 kilobytes.
|
||||||
|
|
||||||
|
In another words there is 274 kilobytes budget for an interaction without further notice. Notice and progress are needed if the interaction needs than that.
|
||||||
|
|
||||||
|
The service worker can use 1 chunk of size.
|
||||||
|
|
||||||
|
## Algorithm
|
||||||
|
|
||||||
|
Don't choose algorithm solely on the time complexity. GUI app needs smooth, not fast. The priority:
|
||||||
|
|
||||||
|
- On the main thread: batching. Batching is usually required to spread the work to multiple frames.
|
||||||
|
- Think in Map-Reduce framework if you don't have any idea.
|
||||||
|
- On the worker thread: balance the speed and the memory usage.
|
||||||
|
- Arrays are usually faster and use less memory.
|
||||||
|
- Worker is always available on our target platforms, but workers introduce latency in the starting and the communication.
|
10
manifest.config.ts
Normal file
10
manifest.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ManifestOptions } from "vite-plugin-pwa";
|
||||||
|
|
||||||
|
const manifest: Partial<ManifestOptions> = {
|
||||||
|
name: "Tutu for Mastodon",
|
||||||
|
short_name: "Tutu",
|
||||||
|
description: "Tutu is an app to read, post, dog and cat on the mastodon.",
|
||||||
|
theme_color: "#673ab7"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default manifest;
|
29
package.json
29
package.json
|
@ -14,40 +14,41 @@
|
||||||
"author": "Rubicon",
|
"author": "Rubicon",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@suid/vite-plugin": "^0.3.0",
|
"@suid/vite-plugin": "^0.3.1",
|
||||||
"@types/hammerjs": "^2.0.45",
|
"@types/hammerjs": "^2.0.46",
|
||||||
"postcss": "^8.4.45",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.5",
|
"vite": "^5.4.9",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vite-plugin-solid": "^2.10.2",
|
"vite-plugin-solid": "^2.10.2",
|
||||||
"vite-plugin-solid-styled": "^0.11.1",
|
"vite-plugin-solid-styled": "^0.11.1",
|
||||||
"workbox-build": "^7.1.1",
|
"workbox-build": "^7.1.1",
|
||||||
"wrangler": "^3.78.2"
|
"wrangler": "^3.81.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.5.4",
|
"@formatjs/intl-localematcher": "^0.5.5",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/solid": "^0.4.2",
|
"@nanostores/solid": "^0.5.0",
|
||||||
"@solid-primitives/event-listener": "^2.3.3",
|
"@solid-primitives/event-listener": "^2.3.3",
|
||||||
"@solid-primitives/i18n": "^2.1.1",
|
"@solid-primitives/i18n": "^2.1.1",
|
||||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||||
"@solid-primitives/map": "^0.4.13",
|
"@solid-primitives/map": "^0.4.13",
|
||||||
"@solid-primitives/resize-observer": "^2.0.26",
|
"@solid-primitives/resize-observer": "^2.0.26",
|
||||||
"@solidjs/router": "^0.14.5",
|
"@solidjs/router": "^0.14.10",
|
||||||
"@suid/icons-material": "^0.8.0",
|
"@suid/icons-material": "^0.8.1",
|
||||||
"@suid/material": "^0.17.0",
|
"@suid/material": "^0.18.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"colorjs.io": "^0.5.2",
|
"colorjs.io": "^0.5.2",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"fast-average-color": "^9.4.0",
|
"fast-average-color": "^9.4.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"iso-639-1": "^3.1.3",
|
"iso-639-1": "^3.1.3",
|
||||||
"masto": "^6.8.0",
|
"masto": "^6.10.0",
|
||||||
"nanostores": "^0.11.3",
|
"nanostores": "^0.11.3",
|
||||||
"solid-js": "^1.8.22",
|
"solid-js": "^1.9.2",
|
||||||
"solid-styled": "^0.11.1",
|
"solid-styled": "^0.11.1",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
|
|
133
public/logo.svg
Normal file
133
public/logo.svg
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<title
|
||||||
|
id="title6">Tutu's Icon</title>
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#fac8a3;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#fac8a3;stop-opacity:1;"
|
||||||
|
offset="0.72011906"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f48d8a;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#linearGradient4"
|
||||||
|
id="linearGradient5"
|
||||||
|
x1="244.74585"
|
||||||
|
y1="430.05423"
|
||||||
|
x2="281.31232"
|
||||||
|
y2="82.147469"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#linearGradient4"
|
||||||
|
id="linearGradient9"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="244.74585"
|
||||||
|
y1="430.05423"
|
||||||
|
x2="281.31232"
|
||||||
|
y2="82.147469" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
transform="matrix(0,1.4786237,-1.4786237,0,642.78291,-98.6117)"
|
||||||
|
style="display:none">
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient5);stroke:#000000;stroke-width:2;stroke-dasharray:none"
|
||||||
|
d="m 142.83262,404.32618 c 82.40343,-32.96137 141.81198,28.9112 141.81198,28.9112 0,0 21.1442,-23.1068 65.90935,-81.91599 51.82799,-68.08783 41.673,-112.8227 41.673,-112.8227 0,0 2.09565,-72.92272 -51.48333,-125.1428 C 299.2268,72.892041 192.11851,96.043081 192.11851,96.043081 c 0,0 -84.44469,24.815289 -100.925375,105.021299 -16.480686,80.20601 51.639485,203.2618 51.639485,203.2618 z"
|
||||||
|
id="path2" />
|
||||||
|
<g
|
||||||
|
id="g6"
|
||||||
|
style="stroke-width:2;stroke-dasharray:none">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:2.3;stroke-dasharray:none"
|
||||||
|
d="m 210.50439,396.31338 c 0,0 -29.4511,-105.3688 -28.96641,-141.50635 0.34942,-26.05202 -0.47344,-34.47943 3.80644,-54.3634 5.96795,-27.72652 12.70659,-35.21402 12.70659,-35.21402"
|
||||||
|
id="path3" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.8;stroke-dasharray:none"
|
||||||
|
d="m 185.10879,292.14608 c 2.88624,-0.57725 6.83376,-30.94178 44.70393,-42.39467"
|
||||||
|
id="path4" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g9"
|
||||||
|
transform="matrix(0,1.5553086,-1.5553086,0,668.42415,-94.877994)">
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient9);stroke:#000000;stroke-width:2;stroke-dasharray:none"
|
||||||
|
d="m 117.92992,367.86152 c 25.48298,38.39529 93.78536,33.94838 93.78536,33.94838 0,0 58.98001,-7.82594 105.93153,-57.60357 51.75,-54.86494 41.67301,-98.59259 41.67301,-98.59259 0,0 0.85581,-49.23349 -57.70901,-95.79319 -44.18497,-35.12756 -115.71798,-13.75528 -115.71798,-13.75528 0,0 -87.11283,22.14714 -94.699695,109.46821 -7.087568,81.5744 26.736785,122.32804 26.736785,122.32804 z"
|
||||||
|
id="path6" />
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
style="stroke-width:2;stroke-dasharray:none"
|
||||||
|
transform="translate(-1.7787639,5.3362916)">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:2.3;stroke-dasharray:none"
|
||||||
|
d="m 210.50439,396.31338 c 0,0 -29.4511,-105.3688 -28.96641,-141.50635 0.34942,-26.05202 -0.47344,-34.47943 3.80644,-54.3634 5.96795,-27.72652 12.70659,-35.21402 12.70659,-35.21402"
|
||||||
|
id="path7" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.8;stroke-dasharray:none"
|
||||||
|
d="m 185.10879,292.14608 c 2.88624,-0.57725 6.83376,-30.94178 44.70393,-42.39467"
|
||||||
|
id="path8" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata6">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>Tutu's Icon</dc:title>
|
||||||
|
<dc:date>2024/10/22</dc:date>
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Rubicon</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Rubicon</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<cc:license
|
||||||
|
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" />
|
||||||
|
</cc:Work>
|
||||||
|
<cc:License
|
||||||
|
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/">
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||||
|
<cc:requires
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||||
|
<cc:requires
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||||
|
<cc:prohibits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||||
|
<cc:requires
|
||||||
|
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
|
||||||
|
</cc:License>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.1 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
12
pwa-assets.config.ts
Normal file
12
pwa-assets.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
minimal2023Preset as preset
|
||||||
|
} from '@vite-pwa/assets-generator/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
headLinkOptions: {
|
||||||
|
preset: '2023'
|
||||||
|
},
|
||||||
|
preset,
|
||||||
|
images: ['public/logo.svg']
|
||||||
|
})
|
|
@ -78,7 +78,7 @@ async function importDateFnLocale(tag: string): Promise<Locale> {
|
||||||
case "en_us":
|
case "en_us":
|
||||||
return (await import("date-fns/locale/en-US")).enUS;
|
return (await import("date-fns/locale/en-US")).enUS;
|
||||||
case "en_gb":
|
case "en_gb":
|
||||||
return (await import("date-fns/locale/en-GB")).enGB;
|
return enGB;
|
||||||
case "zh_cn":
|
case "zh_cn":
|
||||||
return (await import("date-fns/locale/zh-CN")).zhCN;
|
return (await import("date-fns/locale/zh-CN")).zhCN;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -126,7 +126,7 @@ const Profile: Component = () => {
|
||||||
variant="dense"
|
variant="dense"
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: bannerSampledColors()?.text,
|
color: scrolledPastBanner() ? undefined : bannerSampledColors()?.text,
|
||||||
paddingTop: "var(--safe-area-inset-top)",
|
paddingTop: "var(--safe-area-inset-top)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,6 +18,10 @@ import PullDownToRefresh from "./PullDownToRefresh";
|
||||||
import TootComposer from "./TootComposer";
|
import TootComposer from "./TootComposer";
|
||||||
import Thread from "./Thread.jsx";
|
import Thread from "./Thread.jsx";
|
||||||
import { useDefaultSession } from "../masto/clients";
|
import { useDefaultSession } from "../masto/clients";
|
||||||
|
import { useHeroSignal } from "../platform/anim";
|
||||||
|
import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet";
|
||||||
|
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
|
||||||
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
const TootList: Component<{
|
const TootList: Component<{
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
@ -26,7 +30,9 @@ const TootList: Component<{
|
||||||
onChangeToot: (id: string, value: mastodon.v1.Status) => void;
|
onChangeToot: (id: string, value: mastodon.v1.Status) => void;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const session = useDefaultSession();
|
const session = useDefaultSession();
|
||||||
|
const [, setHeroSrc] = useHeroSignal(BOTTOM_SHEET_HERO);
|
||||||
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onBookmark = async (
|
const onBookmark = async (
|
||||||
client: mastodon.rest.Client,
|
client: mastodon.rest.Client,
|
||||||
|
@ -65,6 +71,30 @@ const TootList: Component<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openFullScreenToot = (
|
||||||
|
toot: mastodon.v1.Status,
|
||||||
|
srcElement?: HTMLElement,
|
||||||
|
reply?: boolean,
|
||||||
|
) => {
|
||||||
|
const p = session()?.account;
|
||||||
|
if (!p) return;
|
||||||
|
const inf = p.inf;
|
||||||
|
if (!inf) {
|
||||||
|
console.warn("no account info?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHeroSrc(srcElement);
|
||||||
|
const acct = `${inf.username}@${p.site}`;
|
||||||
|
setTootBottomSheetCache(acct, toot);
|
||||||
|
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
||||||
|
state: reply
|
||||||
|
? {
|
||||||
|
tootReply: true,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={(err, reset) => {
|
fallback={(err, reset) => {
|
||||||
|
@ -82,14 +112,19 @@ const TootList: Component<{
|
||||||
toots={toots}
|
toots={toots}
|
||||||
onBoost={onBoost}
|
onBoost={onBoost}
|
||||||
onBookmark={onBookmark}
|
onBookmark={onBookmark}
|
||||||
onReply={({ status }, element) => {}}
|
onReply={({ status }, element) =>
|
||||||
|
openFullScreenToot(status, element, true)
|
||||||
|
}
|
||||||
client={session()?.client!}
|
client={session()?.client!}
|
||||||
isExpended={(status) => status.id === expandedThreadId()}
|
isExpended={(status) => status.id === expandedThreadId()}
|
||||||
onItemClick={(status, event) => {
|
onItemClick={(status, event) => {
|
||||||
if (status.id !== expandedThreadId()) {
|
if (status.id !== expandedThreadId()) {
|
||||||
setExpandedThreadId((x) => (x ? undefined : status.id));
|
setExpandedThreadId((x) => (x ? undefined : status.id));
|
||||||
} else {
|
} else {
|
||||||
// TODO: open full-screen toot
|
openFullScreenToot(
|
||||||
|
status,
|
||||||
|
event.currentTarget as HTMLElement,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import solidStyled from "vite-plugin-solid-styled";
|
||||||
import suid from "@suid/vite-plugin";
|
import suid from "@suid/vite-plugin";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import version from "vite-plugin-package-version";
|
import version from "vite-plugin-package-version";
|
||||||
|
import manifest from "./manifest.config";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -23,9 +24,10 @@ export default defineConfig(({ mode }) => ({
|
||||||
},
|
},
|
||||||
srcDir: "src/serviceworker",
|
srcDir: "src/serviceworker",
|
||||||
filename: "main.ts",
|
filename: "main.ts",
|
||||||
manifest: {
|
manifest: manifest,
|
||||||
theme_color: "#673ab7"
|
pwaAssets: {
|
||||||
}
|
config: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
version(),
|
version(),
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue