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",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@suid/vite-plugin": "^0.3.0",
|
||||
"@types/hammerjs": "^2.0.45",
|
||||
"postcss": "^8.4.45",
|
||||
"@suid/vite-plugin": "^0.3.1",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.5",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"vite-plugin-solid-styled": "^0.11.1",
|
||||
"workbox-build": "^7.1.1",
|
||||
"wrangler": "^3.78.2"
|
||||
"wrangler": "^3.81.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"@formatjs/intl-localematcher": "^0.5.5",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@nanostores/solid": "^0.4.2",
|
||||
"@nanostores/solid": "^0.5.0",
|
||||
"@solid-primitives/event-listener": "^2.3.3",
|
||||
"@solid-primitives/i18n": "^2.1.1",
|
||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||
"@solid-primitives/map": "^0.4.13",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"@solidjs/router": "^0.14.5",
|
||||
"@suid/icons-material": "^0.8.0",
|
||||
"@suid/material": "^0.17.0",
|
||||
"@solidjs/router": "^0.14.10",
|
||||
"@suid/icons-material": "^0.8.1",
|
||||
"@suid/material": "^0.18.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"colorjs.io": "^0.5.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"iso-639-1": "^3.1.3",
|
||||
"masto": "^6.8.0",
|
||||
"masto": "^6.10.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"solid-js": "^1.8.22",
|
||||
"solid-js": "^1.9.2",
|
||||
"solid-styled": "^0.11.1",
|
||||
"stacktrace-js": "^2.0.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":
|
||||
return (await import("date-fns/locale/en-US")).enUS;
|
||||
case "en_gb":
|
||||
return (await import("date-fns/locale/en-GB")).enGB;
|
||||
return enGB;
|
||||
case "zh_cn":
|
||||
return (await import("date-fns/locale/zh-CN")).zhCN;
|
||||
default:
|
||||
|
|
|
@ -126,7 +126,7 @@ const Profile: Component = () => {
|
|||
variant="dense"
|
||||
sx={{
|
||||
display: "flex",
|
||||
color: bannerSampledColors()?.text,
|
||||
color: scrolledPastBanner() ? undefined : bannerSampledColors()?.text,
|
||||
paddingTop: "var(--safe-area-inset-top)",
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -18,6 +18,10 @@ import PullDownToRefresh from "./PullDownToRefresh";
|
|||
import TootComposer from "./TootComposer";
|
||||
import Thread from "./Thread.jsx";
|
||||
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<{
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
|
@ -26,7 +30,9 @@ const TootList: Component<{
|
|||
onChangeToot: (id: string, value: mastodon.v1.Status) => void;
|
||||
}> = (props) => {
|
||||
const session = useDefaultSession();
|
||||
const [, setHeroSrc] = useHeroSignal(BOTTOM_SHEET_HERO);
|
||||
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onBookmark = async (
|
||||
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 (
|
||||
<ErrorBoundary
|
||||
fallback={(err, reset) => {
|
||||
|
@ -82,14 +112,19 @@ const TootList: Component<{
|
|||
toots={toots}
|
||||
onBoost={onBoost}
|
||||
onBookmark={onBookmark}
|
||||
onReply={({ status }, element) => {}}
|
||||
onReply={({ status }, element) =>
|
||||
openFullScreenToot(status, element, true)
|
||||
}
|
||||
client={session()?.client!}
|
||||
isExpended={(status) => status.id === expandedThreadId()}
|
||||
onItemClick={(status, event) => {
|
||||
if (status.id !== expandedThreadId()) {
|
||||
setExpandedThreadId((x) => (x ? undefined : status.id));
|
||||
} 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 { VitePWA } from "vite-plugin-pwa";
|
||||
import version from "vite-plugin-package-version";
|
||||
import manifest from "./manifest.config";
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [
|
||||
|
@ -23,9 +24,10 @@ export default defineConfig(({ mode }) => ({
|
|||
},
|
||||
srcDir: "src/serviceworker",
|
||||
filename: "main.ts",
|
||||
manifest: {
|
||||
theme_color: "#673ab7"
|
||||
}
|
||||
manifest: manifest,
|
||||
pwaAssets: {
|
||||
config: true,
|
||||
},
|
||||
}),
|
||||
version(),
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue