client/packages/mtp/src/tauri.tsx
Alois 0a304e44f2
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 6m14s
/ build-desktop (linux) (push) Successful in 11m36s
/ build-mobile (push) Successful in 24m26s
/ release (push) Successful in 1m39s
feat(mtp): move useful stuff over to mtp directly
2026-08-27 23:30:33 +02:00

258 lines
7.3 KiB
TypeScript

import {
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import {
ConnectionState,
MTPProxyConnection,
type MTPFrame,
type MTPProxyAdapter,
} from "mtp";
import {
mtp as mtpSchemas,
type Calls,
type Communities,
type Contacts,
} from "@tensamin/shared/data";
import { log } from "@tensamin/shared/log";
import {
type BoundSendFn,
MTPContext,
type ProtocolMessage,
removeMissingContacts,
useMessageHandlers,
} from "./mtpContext";
type NativeSnapshot = {
generation: number;
readyState: number;
identified: boolean;
state?: unknown;
error?: string;
};
function createTauriAdapter() {
const subscriptions = new Map<string, Set<(message: MTPFrame) => void>>();
const adapter: MTPProxyAdapter = {
request: (type, data) =>
invoke<MTPFrame>("mtp_request", { typeName: type, data }),
subscribe(type, handler) {
const handlers = subscriptions.get(type) ?? new Set();
handlers.add(handler);
subscriptions.set(type, handlers);
return () => {
handlers.delete(handler);
if (handlers.size === 0) subscriptions.delete(type);
};
},
};
return {
adapter,
dispatch(message: MTPFrame) {
for (const handler of subscriptions.get(message.type) ?? [])
handler(message);
},
};
}
export function TauriProvider(props: {
children: ReactNode;
blockConnection?: boolean;
}) {
const [snapshot, setSnapshot] = useState<NativeSnapshot>({
generation: 0,
readyState: ConnectionState.Disconnected,
identified: false,
});
const [freshContacts, setFreshContacts] = useState<Contacts>([]);
const [freshCommunities, setFreshCommunities] = useState<Communities>([]);
const [freshCalls, setFreshCalls] = useState<Calls>([]);
const generationRef = useRef(0);
const { addInterceptor, attachSubscriptions, interceptorsRef, subscribe } =
useMessageHandlers();
const [{ bridge, connection }] = useState(() => {
const bridge = createTauriAdapter();
return {
bridge,
connection: new MTPProxyConnection(bridge.adapter, {
schemas: mtpSchemas,
throwProtocolErrors: true,
onValidationError: (error) => {
log(1, "mtp", "red", "Failed to validate native MTP message", error);
},
}),
};
});
const applySnapshot = useCallback(async (next: NativeSnapshot) => {
if (next.generation < generationRef.current) return;
generationRef.current = next.generation;
if (next.error)
log(0, "android", "orange", "MTP connection failed", next.error);
if (!next.identified) {
setSnapshot(next);
return;
}
if (next.state === undefined) {
setSnapshot({
...next,
identified: false,
error: "Native MTP connection omitted initial state",
});
return;
}
try {
const state = await mtpSchemas.ClientStateSync.response.parseAsync(
next.state,
);
setFreshContacts(state.Contacts);
setFreshCommunities(state.Communities);
setFreshCalls(state.Calls);
setSnapshot(next);
} catch (error) {
log(0, "mtp", "red", "Invalid native MTP state", error);
setSnapshot({
...next,
identified: false,
error: "Invalid ClientStateSync payload",
});
}
}, []);
const dispatchMessage = useCallback(
(message: MTPFrame) => {
bridge.dispatch(message);
},
[bridge],
);
useEffect(() => {
return attachSubscriptions(connection);
}, [attachSubscriptions, connection]);
useEffect(
() =>
subscribe("GetStates", (message) => {
setFreshContacts((contacts) =>
removeMissingContacts(contacts, message),
);
}),
[subscribe],
);
useEffect(() => {
if (props.blockConnection) return;
let disposed = false;
let unlisten: UnlistenFn | undefined;
void (async () => {
try {
const nextUnlisten = await listen<
| { kind: "state"; snapshot: NativeSnapshot }
| { kind: "message"; generation: number; message: MTPFrame }
| { kind: "log"; level: number; message: string; details?: unknown }
>("mtp://event", ({ payload }) => {
if (disposed) return;
if (payload.kind === "state") {
void applySnapshot(payload.snapshot);
} else if (payload.kind === "message") {
if (payload.generation === generationRef.current) {
dispatchMessage(payload.message);
}
} else {
log(
payload.level,
"android",
"orange",
payload.message,
payload.details,
);
}
});
if (disposed) nextUnlisten();
else unlisten = nextUnlisten;
} catch (error) {
log(0, "mtp", "red", "Failed to subscribe to native MTP events", error);
}
try {
const current = await invoke<NativeSnapshot>("mtp_status");
if (!disposed) await applySnapshot(current);
} catch (error) {
log(0, "mtp", "red", "Failed to load native MTP status", error);
}
})();
return () => {
disposed = true;
unlisten?.();
};
}, [applySnapshot, dispatchMessage, props.blockConnection]);
useEffect(() => {
if (props.blockConnection) return;
const updateVisibility = () => {
void invoke("mtp_set_ui_visible", {
visible: document.visibilityState === "visible" && document.hasFocus(),
});
};
updateVisibility();
document.addEventListener("visibilitychange", updateVisibility);
window.addEventListener("focus", updateVisibility);
window.addEventListener("blur", updateVisibility);
return () => {
document.removeEventListener("visibilitychange", updateVisibility);
window.removeEventListener("focus", updateVisibility);
window.removeEventListener("blur", updateVisibility);
void invoke("mtp_set_ui_visible", { visible: false });
};
}, [props.blockConnection]);
const send = useCallback<BoundSendFn>(
async (type, data, options) => {
const response = await connection.request(type, data, options);
if (response.type === "GetStates") {
setFreshContacts((contacts) =>
removeMissingContacts(
contacts,
response as ProtocolMessage<"GetStates">,
),
);
}
for (const interceptor of interceptorsRef.current) {
void Promise.resolve(interceptor({ type, data, response })).catch(
(error) => {
log(1, "mtp", "yellow", "MTP interceptor failed", error, { type });
},
);
}
return response;
},
[connection, interceptorsRef],
);
const connected = snapshot.readyState === ConnectionState.Connected;
return (
<MTPContext.Provider
value={{
send,
subscribe,
addInterceptor,
readyState: snapshot.readyState,
identified: snapshot.identified,
freshContacts,
freshCommunities,
freshCalls,
contextReady: connected && snapshot.identified,
loadingDescription: connected
? "Waiting for authenticated session"
: "Establishing native transport channel",
}}
>
{props.children}
</MTPContext.Provider>
);
}