Compare commits

..

No commits in common. "46e7f1aaea91f742dd816369fbed0e8dc85944cb" and "21afb718f7aa429e16cb61aa2107c1035143aa02" have entirely different histories.

14 changed files with 25 additions and 360 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
*.lockb binary diff=lockb

BIN
bun.lockb

Binary file not shown.

View file

@ -24,7 +24,6 @@
"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",
"wrangler": "^3.78.2" "wrangler": "^3.78.2"
}, },
"dependencies": { "dependencies": {
@ -50,9 +49,7 @@
"solid-js": "^1.8.22", "solid-js": "^1.8.22",
"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"
"workbox-core": "^7.1.0",
"workbox-precaching": "^7.1.0"
}, },
"packageManager": "bun@1.1.21" "packageManager": "bun@1.1.21"
} }

View file

@ -5,7 +5,6 @@ import {
createEffect, createEffect,
createMemo, createMemo,
createRenderEffect, createRenderEffect,
createSignal,
ErrorBoundary, ErrorBoundary,
lazy, lazy,
onCleanup, onCleanup,
@ -18,14 +17,6 @@ import {
import { $accounts, updateAcctInf } from "./accounts/stores.js"; import { $accounts, updateAcctInf } from "./accounts/stores.js";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { DateFnScope, useLanguage } from "./platform/i18n.jsx"; import { DateFnScope, useLanguage } from "./platform/i18n.jsx";
import { useRegisterSW } from "virtual:pwa-register/solid";
import {
isJSONRPCResult,
ResultDispatcher,
type JSONRPC,
} from "./serviceworker/services.js";
import { makeEventListener } from "@solid-primitives/event-listener";
import { ServiceWorkerProvider } from "./platform/host.js";
const AccountSignIn = lazy(() => import("./accounts/SignIn.js")); const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
const AccountMastodonOAuth2Callback = lazy( const AccountMastodonOAuth2Callback = lazy(
@ -67,43 +58,6 @@ const App: Component = () => {
const theme = useRootTheme(); const theme = useRootTheme();
const accts = useStore($accounts); const accts = useStore($accounts);
const lang = useLanguage(); const lang = useLanguage();
const [serviceWorker, setServiceWorker] = createSignal<ServiceWorker>();
const dispatcher = new ResultDispatcher();
let checkAge = 0;
const untilServiceWorkerAlive = async (
worker: ServiceWorker,
expectedAge: number,
) => {
const [call, ret] = dispatcher.createTypedCall("ping", undefined);
worker.postMessage(await call);
const result = await ret;
console.assert(!result.error, result);
if (expectedAge === checkAge) {
setServiceWorker(worker);
}
};
makeEventListener(window, "message", (event: MessageEvent<JSONRPC>) => {
if (isJSONRPCResult(event.data)) {
dispatcher.dispatch(event.data.id, event.data);
}
});
const {
needRefresh: [needRefresh],
offlineReady: [offlineReady],
} = useRegisterSW({
onRegisteredSW(scriptUrl, reg) {
console.info("service worker is registered from %s", scriptUrl);
const active = reg?.active;
if (!active) {
console.warn("No service is in activating or activated");
return;
}
untilServiceWorkerAlive(active, checkAge++);
},
});
const clients = createMemo(() => { const clients = createMemo(() => {
return accts().map((x) => ({ return accts().map((x) => ({
@ -149,15 +103,7 @@ const App: Component = () => {
<ThemeProvider theme={theme()}> <ThemeProvider theme={theme()}>
<DateFnScope> <DateFnScope>
<ClientProvider value={clients}> <ClientProvider value={clients}>
<ServiceWorkerProvider
value={{
needRefresh,
offlineReady,
serviceWorker,
}}
>
<Routing /> <Routing />
</ServiceWorkerProvider>
</ClientProvider> </ClientProvider>
</DateFnScope> </DateFnScope>
</ThemeProvider> </ThemeProvider>

View file

@ -1,37 +1,12 @@
import { createContext, useContext, type Accessor } from "solid-js";
import { useRegisterSW } from "virtual:pwa-register/solid";
export function isiOS() { export function isiOS() {
return ( return [
[ 'iPad Simulator',
"iPad Simulator", 'iPhone Simulator',
"iPhone Simulator", 'iPod Simulator',
"iPod Simulator", 'iPad',
"iPad", 'iPhone',
"iPhone", 'iPod'
"iPod", ].includes(navigator.platform)
].includes(navigator.platform) ||
// iPad on iOS 13 detection // iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document) || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}
export type ServiceWorkerService = {
needRefresh: Accessor<boolean>;
offlineReady: Accessor<boolean>;
serviceWorker: Accessor<ServiceWorker | undefined>;
};
const ServiceWorkerContext = /* @__PURE__ */ createContext<
ServiceWorkerService
>(({
needRefresh: () => false,
offlineReady: () => false,
serviceWorker: () => undefined
}));
export const ServiceWorkerProvider = ServiceWorkerContext.Provider;
export function useServiceWorker(): ServiceWorkerService {
return useContext(ServiceWorkerContext);
} }

View file

@ -1,35 +0,0 @@
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { clientsClaim } from "workbox-core";
import type { AnyCall } from "./services";
function checkServiceWorker(
self: WorkerGlobalScope,
): self is ServiceWorkerGlobalScope {
return !!(self as unknown as ServiceWorkerGlobalScope).registration;
}
if (checkServiceWorker(self)) {
cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST, {
cleanURLs: false
});
// auto update
self.skipWaiting();
clientsClaim();
} else {
throw new TypeError("This entry point must be run in a service worker");
}
self.addEventListener("message", (event: MessageEvent<AnyCall>) => {
if (event.data.method === "ping") {
event.source.postMessage({id: event.data.id, jsonrpc: "2.0", result: undefined})
} else {
event.source.postMessage({
error: {
code: -32601,
message: "Method not found"
}
})
}
})

View file

@ -1,177 +0,0 @@
export type JSONRPC = {
jsonrpc: "2.0";
id?: string | number;
};
export type Call<T = undefined> = JSONRPC & {
method: string;
params: T;
};
export type RemoteError<E = undefined> = { data: E } & (
| {
code: number;
message: string;
}
| {
code: -32700;
message: "Parse Error";
}
| {
code: -32600;
message: "Invalid Request";
}
| {
code: -32601;
message: "Method not found";
}
| {
code: -32602;
message: "Invalid params";
}
| {
code: -32603;
message: "Internal error";
}
);
export type Result<T, E> = JSONRPC & { id: string | number } & (
| {
result: T;
error: undefined;
}
| { error: RemoteError<E>; result: undefined }
);
export function isJSONRPCResult(
object: Record<string, unknown>,
): object is Result<unknown, unknown> {
return object["jsonrpc"] === "2.0" && object["id"] && !object["method"];
}
export function isJSONRPCCall(
object: Record<string, unknown>,
): object is Call<unknown> {
return object["jsonrpc"] === "2.0" && !!object["method"];
}
export class ResultDispatcher {
private map: Map<
number | string,
((value: Result<unknown, unknown>) => void) | true // `true` = a call is generated, but the promise not created
>;
private nextId: number = Number.MIN_SAFE_INTEGER;
constructor() {
this.map = new Map();
}
private rollId() {
let id = 0;
while (this.map.get((id = this.nextId++))) {
if (this.nextId >= Number.MAX_SAFE_INTEGER) {
this.nextId = Number.MIN_SAFE_INTEGER;
}
}
return id;
}
createCall<T>(
method: string,
params: T,
): [Promise<Call<T>>, Promise<Result<unknown, unknown>>] {
const id = this.rollId();
const p = new Promise<Result<unknown, unknown>>((resolve) =>
this.map.set(id, resolve),
);
this.map.set(id, true);
const call: Call<T> = {
jsonrpc: "2.0",
id,
method,
params,
};
return [
new Promise((resolve) => {
const waitUntilTheIdSet = () => {
// We must do this check to make sure the id is set before the call made.
// or the dispatching may lost the callback
if (this.map.get(id)) {
resolve(call);
} else {
setTimeout(waitUntilTheIdSet, 0);
}
};
waitUntilTheIdSet();
}),
p,
];
}
dispatch(id: string | number, message: Result<unknown, unknown>) {
{
const callback = this.map.get(id);
if (!callback) return;
if (typeof callback !== "boolean") {
callback(message);
this.map.delete(id);
return Promise.resolve();
}
}
return new Promise<void>((resolve) => {
let retried = 0;
const checkAndDispatch = () => {
const callback = this.map.get(id);
if (typeof callback !== "boolean") {
callback(message);
this.map.delete(id);
resolve();
return;
}
setTimeout(checkAndDispatch, 0);
if (++retried > 3) {
console.warn(
`retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`,
);
}
};
// start the loop
checkAndDispatch();
});
}
createTypedCall<
K extends keyof RequestParams,
P extends RequestParams[K],
R extends ResponseResults[K],
E extends ResponseErrorDatas[K],
>(method: K, params: P): [Promise<Call<P>>, Promise<Result<R, E>>] {
return this.createCall(method, params) as [
Promise<Call<P>>,
Promise<Result<R, E>>,
];
}
}
interface RequestParams {
ping: void;
}
interface ResponseResults {
ping: void;
}
interface ResponseErrorDatas {
[key: string]: void;
}
export type AnyCall<K extends keyof RequestParams = keyof RequestParams> =
JSONRPC & {
method: K;
params: RequestParams[K];
};

View file

@ -1,5 +0,0 @@
{
"compilerOptions": {
"lib": ["ESNext", "WebWorker"],
},
}

View file

@ -45,7 +45,6 @@ import {
} from "../platform/i18n.jsx"; } from "../platform/i18n.jsx";
import { type Template } from "@solid-primitives/i18n"; import { type Template } from "@solid-primitives/i18n";
import BottomSheet from "../material/BottomSheet.jsx"; import BottomSheet from "../material/BottomSheet.jsx";
import { useServiceWorker } from "../platform/host.js";
type Strings = { type Strings = {
["lang.auto"]: Template<{ detected: string }>; ["lang.auto"]: Template<{ detected: string }>;
@ -61,7 +60,9 @@ const Settings: ParentComponent = (props) => {
); );
const navigate = useNavigate(); const navigate = useNavigate();
const settings$ = useStore($settings); const settings$ = useStore($settings);
const { needRefresh, offlineReady } = useServiceWorker(); const {
needRefresh: [needRefresh],
} = useRegisterSW();
const dateFnLocale = useDateFnLocale(); const dateFnLocale = useDateFnLocale();
const [profiles] = useSignedInProfiles(); const [profiles] = useSignedInProfiles();
@ -235,16 +236,6 @@ const Settings: ParentComponent = (props) => {
</Show> </Show>
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem>
<ListItemText secondary={
offlineReady()
? t("availability.offline")
: t("availability.online")
}>
{t("availability")}
</ListItemText>
</ListItem>
<Divider />
</li> </li>
</List> </List>
</Scaffold> </Scaffold>

View file

@ -32,9 +32,5 @@
"motions.gifs": "GIFs", "motions.gifs": "GIFs",
"motions.gifs.autoplay": "Auto-play GIFs", "motions.gifs.autoplay": "Auto-play GIFs",
"motions.vids": "Videos", "motions.vids": "Videos",
"motions.vids.autoplay": "Auto-play Videos", "motions.vids.autoplay": "Auto-play Videos"
"availability": "Availability",
"availability.offline": "Offline ready",
"availability.online": "Online only"
} }

View file

@ -32,9 +32,5 @@
"motions.gifs": "动图", "motions.gifs": "动图",
"motions.gifs.autoplay": "自动播放动图", "motions.gifs.autoplay": "自动播放动图",
"motions.vids": "视频", "motions.vids": "视频",
"motions.vids.autoplay": "自动播放视频", "motions.vids.autoplay": "自动播放视频"
"availability": "离线可用程度",
"availability.offline": "可以离线使用",
"availability.online": "需要联网使用"
} }

View file

@ -24,7 +24,6 @@ import { css } from "solid-styled";
import { vibrate } from "../platform/hardware"; import { vibrate } from "../platform/hardware";
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
import TootComposer from "./TootComposer"; import TootComposer from "./TootComposer";
import { useDocumentTitle } from "../utils";
let cachedEntry: [string, mastodon.v1.Status] | undefined; let cachedEntry: [string, mastodon.v1.Status] | undefined;
@ -107,14 +106,8 @@ const TootBottomSheet: Component = (props) => {
} }
}); });
useDocumentTitle(() => {
const t = toot()?.reblog ?? toot()
const name = t?.account.displayName ?? "Someone"
return `${name}'s toot`
})
const tootDisplayName = () => { const tootDisplayName = () => {
const t = toot()?.reblog ?? toot(); const t = toot();
if (t) { if (t) {
return resolveCustomEmoji(t.account.displayName, t.account.emojis); return resolveCustomEmoji(t.account.displayName, t.account.emojis);
} }

View file

@ -1,23 +1,18 @@
import { import { createRenderEffect, createSignal, onCleanup } from "solid-js";
createRenderEffect,
onCleanup,
type Accessor,
} from "solid-js";
export function useDocumentTitle(newTitle?: string | Accessor<string>) { export function useDocumentTitle(newTitle?: string) {
const capturedTitle = document.title; const capturedTitle = document.title;
const [title, setTitle] = createSignal(newTitle ?? capturedTitle);
createRenderEffect(() => { createRenderEffect(() => {
if (newTitle) document.title = title();
document.title = typeof newTitle === "string" ? newTitle : newTitle();
}); });
onCleanup(() => { onCleanup(() => {
document.title = capturedTitle; document.title = capturedTitle;
}); });
return (x: ((x: string) => string) | string) => return setTitle;
(document.title = typeof x === "string" ? x : x(document.title));
} }
export function mergeClass(c1: string | undefined, c2: string | undefined) { export function mergeClass(c1: string | undefined, c2: string | undefined) {

View file

@ -16,16 +16,10 @@ export default defineConfig(({ mode }) => ({
}, },
}), }),
VitePWA({ VitePWA({
strategies: "injectManifest",
registerType: "autoUpdate", registerType: "autoUpdate",
devOptions: { devOptions: {
enabled: mode === "staging" || mode === "dev", enabled: mode === "staging",
}, },
srcDir: "src/serviceworker",
filename: "main.ts",
manifest: {
theme_color: "#673ab7"
}
}), }),
version(), version(),
], ],