Compare commits

...

3 commits

Author SHA1 Message Date
thislight
7eb55e2a14
added icon
All checks were successful
/ depoly (push) Successful in 1m20s
2024-10-22 17:48:17 +08:00
thislight
29b8b5307e
Profile: fix toolbar color as scrolled by banner 2024-10-18 22:37:19 +08:00
thislight
cacca17dd8
TootList: support openFullScreenToot 2024-10-18 22:35:04 +08:00
11 changed files with 242 additions and 21 deletions

BIN
bun.lockb

Binary file not shown.

26
docs/optimizing.md Normal file
View 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
View 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;

View file

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

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

12
pwa-assets.config.ts Normal file
View 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']
})

View file

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

View file

@ -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)",
}} }}
> >

View file

@ -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,
);
} }
}} }}
/> />

View file

@ -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(),
], ],