From 1a8581d44ff3b8e42d6465009fee8e38055645e1 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 18 Oct 2022 04:45:37 +0800 Subject: [PATCH 1/5] upgrade rspc + fix reactivity + add in disabled Normi code --- Cargo.lock | Bin 179472 -> 179741 bytes Cargo.toml | 6 +- apps/desktop/package.json | 6 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/desktop/src-tauri/tauri.linux.conf.json | 2 +- apps/desktop/src/index.tsx | 15 +- apps/mobile/package.json | 6 +- apps/mobile/src/types/bindings.ts | 13 ++ apps/web/package.json | 4 +- apps/web/src/App.tsx | 24 ++- core/Cargo.toml | 4 +- core/src/api/mod.rs | 9 +- core/src/api/normi.rs | 91 +++++++++ core/src/api/utils/library.rs | 6 +- packages/client/package.json | 6 +- packages/client/src/core.ts | 13 ++ .../client/src/hooks/useCurrentLibrary.tsx | 8 +- packages/client/src/normi/index.ts | 60 ++++++ packages/client/src/normi/types.ts | 27 +++ packages/client/src/normi/utils.ts | 183 ++++++++++++++++++ packages/client/src/rspc.ts | 73 ++++--- packages/interface/package.json | 5 +- packages/interface/src/App.tsx | 2 +- packages/interface/src/screens/Overview.tsx | 142 ++++++++------ pnpm-lock.yaml | Bin 691998 -> 696885 bytes 25 files changed, 582 insertions(+), 125 deletions(-) create mode 100644 core/src/api/normi.rs create mode 100644 packages/client/src/normi/index.ts create mode 100644 packages/client/src/normi/types.ts create mode 100644 packages/client/src/normi/utils.ts diff --git a/Cargo.lock b/Cargo.lock index 5250160d46835c25154b5ac74648dd029cc00160..5f8ea18c5ce0532ba64b303f52ad4d35ffa65621 100644 GIT binary patch delta 680 zcmbR6i)-#5t__F6Hg5{AdMsL$o2kTAmReMtnV+X%tDt0{XP{>|Ju!(C8e!ANU|Om9$SlA1h0LUy~BH6wfI z4gs& yCr;mSicw%Y<0D2_ZJJm-eGM-Y_w;kTOeWg}_?V_J(#8_&WTxQlu>wq+*8l*8L(lX8 delta 536 zcmcJJO)ErE9L77RrkJVmvKXVJ$&`}PIrp5o=YOJEU|~mOY}B~tEeTU&Sjfi8is&xR zO0ranvbbB>$%ZeW7+XGoB=;lC#_p--?RWl@J^jw!KGfph z{e-vC?D#@wQyqV*_;9u0U-LNEuN&KYw%MxdXvdbwL^whGM;^=WmThk7#%81G*Vgfv YU+KleNxcUzqW@u;Gk7W4=)-r{e|K%9tN;K2 diff --git a/Cargo.toml b/Cargo.toml index b87c2743a..c4d7fe9d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,13 @@ prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client- "sqlite", ], default-features = false } rspc = { version = "0.1.2" } +normi = { version = "0.0.1" } +specta = { version = "0.0.4" } [patch.crates-io] # We use this patch so we can compile for the IOS simulator on M1 openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92c3dec225a9e984884d5b30a517e5d44a24d03b" } -rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "b41e4d7868818119d8e3e4e0319d7dce0e675eb0" } # TODO: Move back to crates.io when new jsonrpc executor is released \ No newline at end of file +rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: Move back to crates.io when new jsonrpc executor is released +normi = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io +specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a567d20a5..4ace83600 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -11,12 +11,12 @@ "build": "tauri build" }, "dependencies": { - "@rspc/client": "^0.1.2", - "@rspc/tauri": "^0.1.2", + "@rspc/tauri": "^0.0.0-main-7c0a67c1", + "@rspc/client": "^0.0.0-main-7c0a67c1", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tanstack/react-query": "^4.10.1", + "@tanstack/react-query": "^4.12.0", "@tauri-apps/api": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 582a253a1..7592122aa 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "distDir": "../dist", "devPath": "http://localhost:8001", - "beforeDevCommand": "pnpm exec vite --clearScreen=false", + "beforeDevCommand": "pnpm exec vite --clearScreen=false --mode development", "beforeBuildCommand": "pnpm exec vite build" }, "tauri": { diff --git a/apps/desktop/src-tauri/tauri.linux.conf.json b/apps/desktop/src-tauri/tauri.linux.conf.json index f7ebc35cd..31b8f8f01 100644 --- a/apps/desktop/src-tauri/tauri.linux.conf.json +++ b/apps/desktop/src-tauri/tauri.linux.conf.json @@ -6,7 +6,7 @@ "build": { "distDir": "../dist", "devPath": "http://localhost:8001", - "beforeDevCommand": "pnpm exec vite --clearScreen=false", + "beforeDevCommand": "pnpm exec vite --clearScreen=false --mode development", "beforeBuildCommand": "pnpm exec vite build" }, "tauri": { diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 5bf37ecfb..60599f534 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,6 +1,6 @@ -import { createClient } from '@rspc/client'; -import { TauriTransport } from '@rspc/tauri'; -import { OperatingSystem, PlatformProvider, Procedures, queryClient, rspc } from '@sd/client'; +import { loggerLink } from '@rspc/client'; +import { tauriLink } from '@rspc/tauri'; +import { OperatingSystem, PlatformProvider, hooks, queryClient } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; import { KeybindEvent } from '@sd/interface'; import { dialog, invoke, os, shell } from '@tauri-apps/api'; @@ -10,8 +10,9 @@ import { createRoot } from 'react-dom/client'; import '@sd/ui/style'; -const client = createClient({ - transport: new TauriTransport() +const isDev = import.meta.env.DEV; +const client = hooks.createClient({ + links: [...(isDev ? [loggerLink()] : []), tauriLink()] }); async function getOs(): Promise { @@ -52,11 +53,11 @@ function App() { }, []); return ( - + - + ); } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 49b065fb3..5cf02690e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -17,10 +17,10 @@ "@react-navigation/drawer": "^6.4.4", "@react-navigation/native": "^6.0.12", "@react-navigation/stack": "^6.2.3", - "@rspc/client": "^0.1.2", - "@rspc/react": "^0.1.2", + "@rspc/client": "^0.0.0-main-7c0a67c1", + "@rspc/react": "^0.0.0-main-7c0a67c1", "@sd/assets": "file:../../packages/assets", - "@tanstack/react-query": "^4.2.3", + "@tanstack/react-query": "^4.12.0", "byte-size": "^8.1.0", "class-variance-authority": "^0.2.3", "date-fns": "^2.29.2", diff --git a/apps/mobile/src/types/bindings.ts b/apps/mobile/src/types/bindings.ts index 5ad4ffda2..e63d0ac01 100644 --- a/apps/mobile/src/types/bindings.ts +++ b/apps/mobile/src/types/bindings.ts @@ -14,6 +14,11 @@ export type Procedures = { { key: "locations.indexer_rules.get", input: LibraryArgs, result: IndexerRule } | { key: "locations.indexer_rules.list", input: LibraryArgs, result: Array } | { key: "locations.list", input: LibraryArgs, result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | + { key: "normi.composite", input: never, result: NormalisedCompositeId } | + { key: "normi.org", input: never, result: NormalisedOrganisation } | + { key: "normi.user", input: never, result: NormalisedUser } | + { key: "normi.userSync", input: never, result: NormalisedUser } | + { key: "normi.version", input: never, result: string } | { key: "tags.get", input: LibraryArgs, result: Tag | null } | { key: "tags.getExplorerData", input: LibraryArgs, result: ExplorerData } | { key: "tags.getForObject", input: LibraryArgs, result: Array } | @@ -91,6 +96,14 @@ export interface NodeConfig { version: string | null, id: string, name: string, export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } +export interface NormalisedCompositeId { $type: string, $id: any, org_id: string, user_id: string } + +export interface NormalisedOrganisation { $type: string, $id: any, id: string, name: string, users: NormalizedVec, owner: NormalisedUser, non_normalised_data: Array } + +export interface NormalisedUser { $type: string, $id: any, id: string, name: string } + +export interface NormalizedVec { $type: string, edges: Array } + export interface Object { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string } export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" diff --git a/apps/web/package.json b/apps/web/package.json index 15588492d..13c84b210 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,11 +9,11 @@ }, "dependencies": { "@fontsource/inter": "^4.5.13", - "@rspc/client": "^0.1.2", + "@rspc/client": "^0.0.0-main-7c0a67c1", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tanstack/react-query": "^4.10.1", + "@tanstack/react-query": "^4.12.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f944d8a85..31518f549 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,12 +1,20 @@ -import { WebsocketTransport, createClient } from '@rspc/client'; -import { PlatformProvider, Procedures, queryClient, rspc } from '@sd/client'; +import { createWSClient, loggerLink, wsLink } from '@rspc/client'; +import { PlatformProvider, hooks, queryClient } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; import { useEffect } from 'react'; -const client = createClient({ - transport: new WebsocketTransport( - import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspc/ws' - ) +const wsClient = createWSClient({ + url: import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspc/ws' +}); + +const isDev = import.meta.env.DEV && false; // TODO: Remove false +const client = hooks.createClient({ + links: [ + ...(isDev ? [loggerLink()] : []), + wsLink({ + client: wsClient + }) + ] }); const platform: Platform = { @@ -21,11 +29,11 @@ function App() { return (
- + - +
); } diff --git a/core/Cargo.toml b/core/Cargo.toml index c0889def6..532f36461 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -36,6 +36,8 @@ blake3 = "1.3.1" # Project dependencies rspc = { workspace = true, features = ["uuid", "chrono", "tracing"] } prisma-client-rust = { workspace = true } +normi = { workspace = true } +specta = { workspace = true } uuid = { version = "1.1.2", features = ["v4", "serde"] } sysinfo = "0.26.4" thiserror = "1.0.37" @@ -51,7 +53,7 @@ image = "0.24.4" webp = "0.2.2" ffmpeg-next = { version = "5.1.1", optional = true, features = [] } sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } -sd-file-ext = { path = "../crates/file-ext"} +sd-file-ext = { path = "../crates/file-ext" } fs_extra = "1.2.0" tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index d24b8e011..94a557a33 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -39,6 +39,7 @@ mod files; mod jobs; mod libraries; mod locations; +mod normi; mod tags; pub mod utils; pub mod volumes; @@ -52,12 +53,7 @@ struct NodeState { pub(crate) fn mount() -> Arc { let r = ::new() - .config( - Config::new() - // TODO: This messes with Tauri's hot reload so we can't use it until their is a solution upstream. https://github.com/tauri-apps/tauri/issues/4617 - // .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./index.ts")), - .set_ts_bindings_header("/* eslint-disable */"), - ) + .config(Config::new().set_ts_bindings_header("/* eslint-disable */")) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("getNode", |t| { t(|ctx, _: ()| async move { @@ -68,6 +64,7 @@ pub(crate) fn mount() -> Arc { }) }) }) + .merge("normi.", normi::mount()) .merge("library.", libraries::mount()) .merge("volumes.", volumes::mount()) .merge("tags.", tags::mount()) diff --git a/core/src/api/normi.rs b/core/src/api/normi.rs new file mode 100644 index 000000000..f345ca285 --- /dev/null +++ b/core/src/api/normi.rs @@ -0,0 +1,91 @@ +use normi::{typed, Object}; +use rspc::Type; +use serde::Serialize; + +use super::RouterBuilder; + +#[derive(Serialize, Type, Object)] +#[normi(rename = "org")] +pub struct Organisation { + #[normi(id)] + pub id: String, + pub name: String, + #[normi(refr)] + pub users: Vec, + #[normi(refr)] + pub owner: User, + pub non_normalised_data: Vec<()>, +} + +#[derive(Serialize, Type, Object)] +pub struct User { + #[normi(id)] + pub id: String, + pub name: String, +} + +#[derive(Serialize, Type, Object)] +pub struct CompositeId { + #[normi(id)] + pub org_id: String, + #[normi(id)] + pub user_id: String, +} + +pub fn mount() -> RouterBuilder { + RouterBuilder::new() + .query("version", |t| t(|_, _: ()| "0.1.0")) + .query("userSync", |t| { + t.resolver(|_, _: ()| User { + id: "1".to_string(), + name: "Monty Beaumont".to_string(), + }) + .map(typed) + }) + .query("user", |t| { + t.resolver(|_, _: ()| async move { + Ok(User { + id: "1".to_string(), + name: "Monty Beaumont".to_string(), + }) + }) + .map(typed) + }) + .query("org", |t| { + t.resolver(|_, _: ()| async move { + Ok(Organisation { + id: "org-1".into(), + name: "Org 1".into(), + users: vec![ + User { + id: "user-1".into(), + name: "Monty Beaumont".into(), + }, + User { + id: "user-2".into(), + name: "Millie Beaumont".into(), + }, + User { + id: "user-3".into(), + name: "Oscar Beaumont".into(), + }, + ], + owner: User { + id: "user-1".into(), + name: "Monty Beaumont".into(), + }, + non_normalised_data: vec![(), ()], + }) + }) + .map(typed) + }) + .query("composite", |t| { + t.resolver(|_, _: ()| async move { + Ok(CompositeId { + org_id: "org-1".into(), + user_id: "user-1".into(), + }) + }) + .map(typed) + }) +} diff --git a/core/src/api/utils/library.rs b/core/src/api/utils/library.rs index 7fb2a6c05..8f537d3e5 100644 --- a/core/src/api/utils/library.rs +++ b/core/src/api/utils/library.rs @@ -85,7 +85,7 @@ where TArg: DeserializeOwned + specta::Type + Send + 'static, { self.query(key, move |t| { - let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::from_builder(&t)).resolver); t(move |ctx, arg: LibraryArgs| { let resolver = resolver.clone(); @@ -131,7 +131,7 @@ where TArg: DeserializeOwned + specta::Type + Send + 'static, { self.mutation(key, move |t| { - let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::from_builder(&t)).resolver); t(move |ctx, arg: LibraryArgs| { let resolver = resolver.clone(); @@ -169,7 +169,7 @@ where TResolver: Fn(Ctx, TArg, Uuid) -> TStream + Send + Sync + 'static, { self.subscription(key, |t| { - let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::from_builder(&t)).resolver); t(move |ctx, arg: LibraryArgs| { // TODO(@Oscar): Upstream rspc work to allow this to work diff --git a/packages/client/package.json b/packages/client/package.json index 6e0ca3c10..74d624b5f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -17,10 +17,10 @@ "preset": "scripts/jest/node" }, "dependencies": { - "@rspc/client": "^0.1.2", - "@rspc/react": "^0.1.2", + "@rspc/client": "^0.0.0-main-7c0a67c1", + "@rspc/react": "^0.0.0-main-7c0a67c1", "@sd/config": "workspace:*", - "@tanstack/react-query": "^4.10.1", + "@tanstack/react-query": "^4.12.0", "eventemitter3": "^4.0.7", "immer": "^9.0.15", "lodash": "^4.17.21", diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 5ad4ffda2..e63d0ac01 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -14,6 +14,11 @@ export type Procedures = { { key: "locations.indexer_rules.get", input: LibraryArgs, result: IndexerRule } | { key: "locations.indexer_rules.list", input: LibraryArgs, result: Array } | { key: "locations.list", input: LibraryArgs, result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | + { key: "normi.composite", input: never, result: NormalisedCompositeId } | + { key: "normi.org", input: never, result: NormalisedOrganisation } | + { key: "normi.user", input: never, result: NormalisedUser } | + { key: "normi.userSync", input: never, result: NormalisedUser } | + { key: "normi.version", input: never, result: string } | { key: "tags.get", input: LibraryArgs, result: Tag | null } | { key: "tags.getExplorerData", input: LibraryArgs, result: ExplorerData } | { key: "tags.getForObject", input: LibraryArgs, result: Array } | @@ -91,6 +96,14 @@ export interface NodeConfig { version: string | null, id: string, name: string, export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } +export interface NormalisedCompositeId { $type: string, $id: any, org_id: string, user_id: string } + +export interface NormalisedOrganisation { $type: string, $id: any, id: string, name: string, users: NormalizedVec, owner: NormalisedUser, non_normalised_data: Array } + +export interface NormalisedUser { $type: string, $id: any, id: string, name: string } + +export interface NormalizedVec { $type: string, edges: Array } + export interface Object { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string } export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" diff --git a/packages/client/src/hooks/useCurrentLibrary.tsx b/packages/client/src/hooks/useCurrentLibrary.tsx index d9d02e43f..ab8847296 100644 --- a/packages/client/src/hooks/useCurrentLibrary.tsx +++ b/packages/client/src/hooks/useCurrentLibrary.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from 'react'; -import { proxy, useSnapshot } from 'valtio'; +import { proxy, subscribe, useSnapshot } from 'valtio'; -import { getExplorerStore, useBridgeQuery, useExplorerStore } from '../index'; +import { getExplorerStore, useBridgeQuery } from '../index'; // The name of the localStorage key for caching library data const libraryCacheLocalStorageKey = 'sd-library-list'; @@ -26,6 +26,10 @@ export function getLibraryIdRaw(): string | null { return currentLibraryUuidStore.id; } +export function onLibraryChange(func: (newLibraryId: string | null) => void) { + subscribe(currentLibraryUuidStore, () => func(currentLibraryUuidStore.id)); +} + // this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood. export const useCurrentLibrary = () => { const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id; diff --git a/packages/client/src/normi/index.ts b/packages/client/src/normi/index.ts new file mode 100644 index 000000000..b150ab6fb --- /dev/null +++ b/packages/client/src/normi/index.ts @@ -0,0 +1,60 @@ +// TODO(@Oscar): I wanna move Normi out of this repo and into rspc because it will make the code way more maintainable but right now I am unsure on the public API to make that possible. +import { CustomHooks } from '@rspc/client'; +// @ts-expect-error: // TODO(@Oscar): Fix types +import { __useMutation, __useQuery } from '@rspc/react/internal'; +import { useMemo } from 'react'; + +import { NormiOptions, getNormiCache, loadDataFromCache } from './utils'; + +export function normiCustomHooks( + { contextSharing }: NormiOptions, + nextHooks?: () => CustomHooks +): () => CustomHooks { + let normiCache = getNormiCache(contextSharing ?? false); + const next = nextHooks?.(); + + // TODO: Handle manual modifications to the query cache + // // queryClient.getQueryCache().subscribe(({ type, query }) => { + // // if (type === "added") { + // // console.log("ADDED", query.queryKey, query.state.data); + // // } else if (type === "updated") { + // // console.log("UPDATE", query.queryKey, query.state.data); + + // // const d = query.state.data; + // // if (Array.isArray(d)) { + // // d.forEach((f) => { + // // if (typeof f?.$id == "string") normyCache.set(f.$id, f); + // // }); + // // } + // // } else if (type === "removed") { + // // console.log("REMOVED", query.queryKey, query.state.data); + // // } + // // }); + + // TODO: Subscribe to backend for updates when things change + // - Subscribe for active queries + + return () => ({ + mapQueryKey: next?.mapQueryKey, + doQuery: next?.doQuery, + doMutation: next?.doMutation + // dangerous: { + // useQuery(keyAndInput, handler, opts) { + // const hook = __useQuery(keyAndInput, handler, opts); + // const data = useMemo(() => { + // return loadDataFromCache(hook.data, normiCache); + // }, [hook.data]); + + // return { + // ...hook, + // data + // }; + // }, + // useMutation(handler, opts) { + // const hook = __useMutation(handler, opts); + // // TODO: Normalize data before `onSuccess` or returning from `hook.data` + // return hook; + // } + // } + }); +} diff --git a/packages/client/src/normi/types.ts b/packages/client/src/normi/types.ts new file mode 100644 index 000000000..c87b3432b --- /dev/null +++ b/packages/client/src/normi/types.ts @@ -0,0 +1,27 @@ +import { ProcedureDef } from '@rspc/client'; + +// https://stackoverflow.com/a/54487392 +export type OmitDistributive = T extends any + ? T extends object + ? Id> + : T + : never; +export type Id = {} & { [P in keyof T]: T[P] }; // Cosmetic use only makes the tooltips expand the type can be removed +export type OmitRecursively = Omit< + { [P in keyof T]: OmitDistributive }, + K +>; + +/** + * is responsible for normalizing the Typescript type before the type is exposed back to the user. + * + * @internal + */ +export type Normalized = T extends any + ? { + key: T['key']; + // TODO: Typescript transformation for arrays + result: OmitRecursively; + input: T['input']; + } + : never; diff --git a/packages/client/src/normi/utils.ts b/packages/client/src/normi/utils.ts new file mode 100644 index 000000000..afa8b46e8 --- /dev/null +++ b/packages/client/src/normi/utils.ts @@ -0,0 +1,183 @@ +export type NormiCache = Map>; + +declare global { + interface Window { + normiCache?: NormiCache; + } +} + +export interface NormiOptions { + contextSharing?: boolean; +} + +export function getNormiCache(contextSharing: boolean): NormiCache { + if (contextSharing) { + if (window.normiCache === undefined) { + window.normiCache = new Map(); + } + + return window.normiCache; + } else { + return new Map(); + } +} + +export function getOrCreate(map: Map>, key: K): Map { + let m = map.get(key); + if (m === undefined) { + m = new Map(); + map.set(key, m); + } + return m; +} + +export function normaliseValue(value: any, normiCache: NormiCache): any { + if (value === null || value === undefined) { + return value; + } else if (typeof value === 'object') { + if ('$id' in value && '$type' in value) { + getOrCreate(normiCache, value.$type).set(value.$id, normaliseValueForStorage(value, true)); + delete value.$id; + delete value.$type; + } else if ('$type' in value && 'edges' in value) { + // TODO: Caching all the edges + value = (value.edges as any[]).map((v) => normaliseValue(v, normiCache)); + } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = normaliseValue(v, normiCache); + } + } + + return value; +} + +export function normaliseValueForStorage(value: any, rootElem: boolean): any { + if (value === null || value === undefined) { + return value; + } else if (typeof value === 'object') { + if ('$id' in value && '$type' in value) { + if (rootElem) { + let v = Object.assign({}, value); + delete v.$id; + delete v.$type; + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, vv] of Object.entries(v)) { + v[k] = normaliseValueForStorage(vv, false); + } + + return v; + } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = normaliseValueForStorage(v, false); + } + + return { + $id: value.$id, + $type: value.$type + }; + } else if ('$type' in value && 'edges' in value) { + return { + $type: value.$type, + edges: Object.values(value.edges as any[]).map((v) => v.$id) + }; + } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = normaliseValueForStorage(v, false); + } + } + + return value; +} + +export function recomputeNormalisedValueFromStorage(value: any, normiCache: NormiCache): any { + if (value === null || value === undefined) { + return value; + } else if (typeof value === 'object') { + if ('$id' in value && '$type' in value) { + value = normiCache.get(value.$type)!.get(value.$id); // TODO: Handle `undefined` + } else if ('$type' in value && 'edges' in value) { + value = (value.edges as any[]).map( + (id) => normiCache.get(value.$type)!.get(id) // TODO: Handle `undefined` + ); + } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = recomputeNormalisedValueFromStorage(v, normiCache); + } + } + + return value; +} + +// export function recomputeRQCache(queryClient: QueryClient, normiCache: NormiCache) { +// let c = queryClient.getQueryCache(); + +// // c.getAll().forEach((query) => { +// // const d = query.state.data; +// // if (Array.isArray(d)) { +// // queryClient.setQueryData( +// // query.queryKey, +// // d.map((f) => { +// // if (typeof f?.$id == "string" && normyCache.has(f?.$id)) { +// // return normyCache.get(f.$id); +// // } +// // return f; +// // }) +// // ); +// // } +// // }); +// } + +export function loadDataFromCache(value: any, normiCache: NormiCache): any { + // TODO: If can't be pulled out of the cache refetch + + if (value === null || value === undefined) { + return value; + } else if (typeof value === 'object') { + if ('$id' in value && '$type' in value) { + // if (rootElem) { + let v = Object.assign({}, value); + delete v.$id; + delete v.$type; + + // // TODO: Optimise this to only check fields the backend marks as normalisable or on root + // for (const [k, vv] of Object.entries(v)) { + // v[k] = normaliseValueForStorage(vv, false); + // } + + // return v; + // } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = normaliseValueForStorage(v, false); + } + + return v; // normiCache.get(v.$id)!; + } else if ('$type' in value && 'edges' in value) { + // TODO: This needs to be replicated in Typescript types + return []; + // { + // $type: value.$type, + // edges: Object.values(value.edges as any[]).map((v) => v.$id) + // }; + } + + // TODO: Optimise this to only check fields the backend marks as normalisable or on root + for (const [k, v] of Object.entries(value)) { + value[k] = normaliseValueForStorage(v, false); + } + } + + return value; +} + +// TODO: Optimistic updates diff --git a/packages/client/src/rspc.ts b/packages/client/src/rspc.ts index 086363f16..cc5ba34ba 100644 --- a/packages/client/src/rspc.ts +++ b/packages/client/src/rspc.ts @@ -1,12 +1,11 @@ import { ProcedureDef } from '@rspc/client'; -import { createReactQueryHooks } from '@rspc/react'; +import { internal_createReactHooksFactory } from '@rspc/react'; import { QueryClient } from '@tanstack/react-query'; import { LibraryArgs, Procedures } from './core'; import { getLibraryIdRaw } from './index'; - -export const queryClient = new QueryClient(); -export const rspc = createReactQueryHooks(); +import { normiCustomHooks } from './normi'; +import { Normalized } from './normi/types'; type NonLibraryProcedure = | Exclude }> @@ -17,7 +16,7 @@ type LibraryProcedures = Exclude< { input: never } >; -type MoreConstrainedQueries = T extends any +type StripLibraryArgsFromInput = T extends any ? T['input'] extends LibraryArgs ? { key: T['key']; @@ -27,30 +26,56 @@ type MoreConstrainedQueries = T extends any : never : never; -export const useBridgeQuery = rspc.customQuery>( - (keyAndInput) => keyAndInput as any -); +export const hooks = internal_createReactHooksFactory(); -export const useBridgeMutation = rspc.customMutation>( - (keyAndInput) => keyAndInput -); - -export const useLibraryQuery = rspc.customQuery< - MoreConstrainedQueries> ->((keyAndInput) => { - const library_id = getLibraryIdRaw(); - if (library_id === null) throw new Error('Attempted to do library query with no library set!'); - return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +const nonLibraryHooks = hooks.createHooks< + Procedures, + // Normalized>, + // Normalized> + NonLibraryProcedure<'queries'>, + NonLibraryProcedure<'mutations'> +>({ + internal: { + customHooks: normiCustomHooks({ contextSharing: true }) + } }); -export const useLibraryMutation = rspc.customMutation< - MoreConstrainedQueries> ->((keyAndInput) => { - const library_id = getLibraryIdRaw(); - if (library_id === null) throw new Error('Attempted to do library query with no library set!'); - return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +const libraryHooks = hooks.createHooks< + Procedures, + // Normalized>>, + // Normalized>>, + StripLibraryArgsFromInput>, + StripLibraryArgsFromInput>, + never +>({ + internal: { + customHooks: normiCustomHooks({ contextSharing: true }, () => { + return { + mapQueryKey: (keyAndInput) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) + throw new Error('Attempted to do library operation with no library set!'); + return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; + }, + doMutation: (keyAndInput, next) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) + throw new Error('Attempted to do library operation with no library set!'); + return next([keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]); + } + }; + }) + } }); +export const queryClient = new QueryClient(); +export const rspc = hooks.createHooks(); + +export const useBridgeQuery = nonLibraryHooks.useQuery; +export const useBridgeMutation = nonLibraryHooks.useMutation; +export const useLibraryQuery = libraryHooks.useQuery; +export const useLibraryMutation = libraryHooks.useMutation; + export function useInvalidateQuery() { const context = rspc.useContext(); rspc.useSubscription(['invalidateQuery'], { diff --git a/packages/interface/package.json b/packages/interface/package.json index bc17a2b6a..755aa31af 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -27,12 +27,13 @@ "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-toast": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", + "@rspc/client": "^0.0.0-main-7c0a67c1", "@sd/assets": "workspace:*", "@sd/client": "workspace:*", "@sd/ui": "workspace:*", "@tailwindcss/forms": "^0.5.3", - "@tanstack/react-query": "^4.10.1", - "@tanstack/react-query-devtools": "^4.10.1", + "@tanstack/react-query": "^4.12.0", + "@tanstack/react-query-devtools": "^4.12.0", "@tanstack/react-virtual": "3.0.0-beta.18", "@vitejs/plugin-react": "^2.1.0", "autoprefixer": "^10.4.12", diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 84e3fd591..8db615e57 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -22,7 +22,7 @@ export default function SpacedriveInterface() { {/* The `context={defaultContext}` part is required for this to work on Windows. Why, idk, don't question it */} - {import.meta.env.MODE === 'development' && ( + {import.meta.env.DEV && ( )} diff --git a/packages/interface/src/screens/Overview.tsx b/packages/interface/src/screens/Overview.tsx index 3dbf5a65b..53508aa82 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/packages/interface/src/screens/Overview.tsx @@ -1,5 +1,11 @@ -import { ExclamationCircleIcon, PlusIcon } from '@heroicons/react/24/solid'; -import { useBridgeQuery, useLibraryQuery, usePlatform } from '@sd/client'; +import { PlusIcon } from '@heroicons/react/24/solid'; +import { + onLibraryChange, + queryClient, + useCurrentLibrary, + useLibraryQuery, + usePlatform +} from '@sd/client'; import { Statistics } from '@sd/client'; import { Button, Input } from '@sd/ui'; import { Dialog } from '@sd/ui'; @@ -8,9 +14,8 @@ import clsx from 'clsx'; import { useEffect } from 'react'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; -import create from 'zustand'; +import { proxy } from 'valtio'; -import { Device } from '../components/device/Device'; import useCounter from '../hooks/useCounter'; interface StatItemProps { @@ -26,42 +31,64 @@ const StatItemNames: Partial> = { total_bytes_free: 'Free space' }; -type OverviewStats = Partial>; -type OverviewState = { - overviewStats: OverviewStats; - setOverviewStat: (name: keyof OverviewStats, newValue: string) => void; - setOverviewStats: (stats: OverviewStats) => void; -}; +const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames; -export const useOverviewState = create((set) => ({ - overviewStats: {}, - setOverviewStat: (name, newValue) => - set((state) => ({ - ...state, - overviewStats: { - ...state.overviewStats, - [name]: newValue +export const state = proxy({ + lastRenderedLibraryId: undefined as string | undefined +}); + +onLibraryChange((newLibraryId) => { + state.lastRenderedLibraryId = undefined; + + // TODO: Fix + // This is bad solution to the fact that the hooks don't rerun when opening a library that is already cached. + // This is because the count never drops back to zero as their is no loading state given the libraries data was already in the React Query cache. + queryClient.setQueryData( + [ + 'library.getStatistics', + { + library_id: newLibraryId, + arg: null } - })), - setOverviewStats: (stats) => - set((state) => ({ - ...state, - overviewStats: stats - })) -})); + ], + { + id: 0, + date_captured: '', + total_bytes_capacity: '0', + preview_media_bytes: '0', + library_db_size: '0', + total_object_count: 0, + total_bytes_free: '0', + total_bytes_used: '0', + total_unique_bytes: '0' + } + ); + queryClient.invalidateQueries(['library.getStatistics']); +}); const StatItem: React.FC = (props) => { + const { library } = useCurrentLibrary(); const { title, bytes = '0', isLoading } = props; - // const appProps = useContext(AppPropsContext); - const size = byteSize(+bytes); - const count = useCounter({ name: title, - end: +size.value + end: +size.value, + duration: state.lastRenderedLibraryId === library?.uuid ? 0 : undefined, + saveState: false }); + if (count !== 0 && count == +size.value) { + state.lastRenderedLibraryId = library?.uuid; + } + + useEffect(() => { + return () => { + if (count !== 0) state.lastRenderedLibraryId = library?.uuid; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
= (props) => { export default function OverviewScreen() { const platform = usePlatform(); - const { data: libraryStatistics, isLoading: isStatisticsLoading } = useLibraryQuery([ - 'library.getStatistics' - ]); - const { data: nodeState } = useBridgeQuery(['getNode']); + const { library } = useCurrentLibrary(); + const { data: overviewStats, isLoading: isStatisticsLoading } = useLibraryQuery( + ['library.getStatistics'], + { + initialData: { + id: 0, + date_captured: '', + total_bytes_capacity: '0', + preview_media_bytes: '0', + library_db_size: '0', + total_object_count: 0, + total_bytes_free: '0', + total_bytes_used: '0', + total_unique_bytes: '0' + } + } + ); - const { overviewStats, setOverviewStats } = useOverviewState(); - - // get app props from context - useEffect(() => { - const newStatistics: OverviewStats = { - total_bytes_capacity: '0', - preview_media_bytes: '0', - library_db_size: '0', - total_object_count: '0', - total_bytes_free: '0', - total_bytes_used: '0', - total_unique_bytes: '0' - }; - - Object.entries((libraryStatistics as Statistics) || {}).forEach(([key, value]) => { - newStatistics[key as keyof Statistics] = `${value}`; - }); - - setOverviewStats(newStatistics); - }, [platform, libraryStatistics, setOverviewStats]); - - const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames; + console.log(overviewStats); return (
@@ -129,12 +148,12 @@ export default function OverviewScreen() {
{/* STAT CONTAINER */}
- {Object.entries(overviewStats).map(([key, value]) => { + {Object.entries(overviewStats || []).map(([key, value]) => { if (!displayableStatItems.includes(key)) return null; return ( */} {/* */} +
); } + +// TODO(@Oscar): Remove this +function Debug() { + // const org = useBridgeQuery(['normi.org']); + // console.log(org.data); + + return null; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100f00cc97639fe9a16c2cf06c56d22df22bfb85..0eec8a096c31e89be697bbc336407b288c5815ca 100644 GIT binary patch delta 3352 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 F{sXcdM9=^L delta 1588 zcmcgsZERCj7|uPHc3U?#h787L_rZXT4c2?RyKaTa)^=^zZCyXMer{6lZM(JW_wCnh z6Qjf+F)9I1$g2nwU5Gy%ikW!*BLTxNVq%OM|Cr1V{%8oG(U^cyyOj|9!Cy~ul5-{(D-uXiskbziC?u}!{HdW7nwdzDzx)POx?9Ao(o@XaA9ygG|ytGCJL5Zv;h z8vL|;v!slCS9+Z+tsu3G_Q07qDSHHevEuhS@)5dqAg&+Uv1)1jf+!OWHeG-gE%L1n zHP=X6OITUk#^@a3(@DRA53>GD=fSoIvQ`f7!z*O-I?w8j_Jd`P1OGv8sPZWnRY{dH zo+_k-e1Z`QVIi#QkLOiU20z;%$HRxV;0b2u2G2Vk&r5K1eGQgXH{zkDN?h1?qAavg zj^AQxDvmI{^oH=pMtqam0Vkg!D^`=;%~kkv-v{f+jo9%k%r1FpdqXf_;6g zDxh3Y1`35p)*s8^qs-3Lo6Mhe_yXNoy4!(^BinE{)AE8#FS_&uc@{4HNXWz=ej?7- zV(Z}=apDi+rc^xkH*peein3e~)?~=&i1g?=A!uiv-1In;VD0Ll!;{rf{!x?Bu4QI4 z)=)Ug8FVId(yC6n4O9Mt&1!SxN*63PEuRZ#HN3|>NC_cIYqKk;LSJOj5rFnzi1%R2 zE|i7?w+IR-2~y%qY6%S2BAeJiptq}GjYbvX>$T_`5+}Q);^A%Rq6FJVTi|Yp*yAwH zb55-w42)ZIh0N4U59Rjg3_6P`rZ>luQOdb65DJCoC)r8oK(fDRPh>I%^MY+4#q&ab zFsITEa@h%Yk)b0lkC_kHc~>g!Wy`X9n~@j(>OsqJr3*2TB+MF0SE$DN&qeLAr+=^a=&9{lXIMjv~Bw|A+TH69=tI%fgm>TV`5Kn8- z^b>H#iI(Lh4?f;L@GC{^oykrXv{s$aqs>Q@sYuS`caGa!V>7f@?F!|J9Oq_=^ZGRJ z4MdczGn036suab>Ig{3`2patZwuQ+=p0)U=!-~X!**l||WTrW|wo2@V6LVx0)Va~( zdUrH{&qLOiit!~U*$xU9l8QIn=rsa9nnm}3av=$<%%W8JY*=DvlLDr=f{xVTi9cP=Vodezw2_;_+mQ4CXyssF1~nFyb> z1Q*pQZo=gbQf?~f_lG0NTz^QDXLJdT#%ffmt%X1;O2=oSYQM*zEC!TxHtviu1x+#% zHFzkE2HKw_1sJ)GD&hVtxw0XqPA5}CtE=>XmGV^wS89;%|9QPs-_%Q$&2aZ;)Co;X zvf+m+q~f%XTu$RnvU=P#KM3NtWG(oB=uogxW6Q+Uwglk^qH8aPFacOB* nh{Sm1n)UeAbCu%Nb5;L*{NU-`;FFMh!1_kTRWvNz From aae3da9b8aaf5fe38d13536bd54f1c7103203860 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 18 Oct 2022 20:56:16 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=89=20Fix=20core=20on=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | Bin 179741 -> 180257 bytes apps/mobile/android/app/build.gradle | 6 +- apps/mobile/android/build.gradle | 1 + apps/mobile/app.json | 8 +- apps/mobile/eas.json | 23 +++ .../ios/Spacedrive.xcodeproj/project.pbxproj | 2 +- .../xcschemes/Spacedrive.xcscheme | 20 +-- apps/mobile/package.json | 5 +- apps/mobile/rust/Cargo.toml | 5 + apps/mobile/rust/src/android.rs | 149 +++++++++++------- apps/mobile/rust/src/ios.rs | 84 ++++++---- apps/mobile/scripts/postinstall.js | 13 +- apps/mobile/src/App.tsx | 12 +- apps/mobile/src/hooks/rspc.ts | 114 -------------- .../src/lib/rspcReactNativeTransport.ts | 146 +++++++++++++++++ core/Cargo.toml | 2 + core/src/api/mod.rs | 22 ++- core/src/lib.rs | 61 +++---- 18 files changed, 401 insertions(+), 272 deletions(-) create mode 100644 apps/mobile/eas.json delete mode 100644 apps/mobile/src/hooks/rspc.ts create mode 100644 apps/mobile/src/lib/rspcReactNativeTransport.ts diff --git a/Cargo.lock b/Cargo.lock index 5f8ea18c5ce0532ba64b303f52ad4d35ffa65621..c6e2f875b897e5526a8678e70a9769861d7cdde7 100644 GIT binary patch delta 268 zcmXw!y-EW?6h@gTTv>zI#ej;ajj+O-A8_ZvG9m_b~+mFOwK1H4!P~?%Baez8jURxL#Nj#81qwkV;4wb<#>(Fdlrgu@(YTlFFu7o1lf2 tWm%&|gWee-A^Iqkj9Phm*<;x8{J)(iaEuRl5KH delta 55 zcmV-70LcHLfD4`c3a~Q@gZc`$`U(L>*_Z!Y0u7g7? N= 0.56.0" + }, + "build": { + "development": { + "distribution": "internal", + "android": { + "gradleCommand": ":app:assembleDebug" + }, + "ios": { + "buildConfiguration": "Debug" + } + }, + "preview": { + "distribution": "internal" + }, + "production": {} + }, + "submit": { + "production": {} + } +} diff --git a/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj index ecaea1a36..6db8ad8ec 100644 --- a/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj @@ -161,7 +161,7 @@ dependencies = ( ); name = Spacedrive; - productName = mobilenew; + productName = Spacedrive; productReference = 13B07F961A680F5B00A75B9A /* Spacedrive.app */; productType = "com.apple.product-type.application"; }; diff --git a/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme b/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme index 05655c640..5758d3c28 100644 --- a/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme +++ b/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "Spacedrive.app" + BlueprintName = "Spacedrive" + ReferencedContainer = "container:Spacedrive.xcodeproj"> @@ -35,7 +35,7 @@ BlueprintIdentifier = "00E356ED1AD99517003FC87E" BuildableName = "mobilenewTests.xctest" BlueprintName = "mobilenewTests" - ReferencedContainer = "container:mobilenew.xcodeproj"> + ReferencedContainer = "container:Spacedrive.xcodeproj"> @@ -64,9 +64,9 @@ + BuildableName = "Spacedrive.app" + BlueprintName = "Spacedrive" + ReferencedContainer = "container:Spacedrive.xcodeproj"> @@ -81,9 +81,9 @@ + BuildableName = "Spacedrive.app" + BlueprintName = "Spacedrive" + ReferencedContainer = "container:Spacedrive.xcodeproj"> diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ae89f337e..917a66786 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -3,11 +3,13 @@ "version": "1.0.0", "main": "index.js", "license": "GPL-3.0-only", + "private": true, "scripts": { "start": "expo start --dev-client", "android": "expo run:android", "ios": "expo run:ios", "xcode": "open ios/spacedrive.xcworkspace", + "android-studio": "open -a '/Applications/Android Studio.app' ./android", "lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit", "postinstall": "node scripts/postinstall.js" }, @@ -67,6 +69,5 @@ "metro-minify-terser": "^0.72.1", "react-native-svg-transformer": "^1.0.0", "typescript": "^4.7.4" - }, - "private": true + } } diff --git a/apps/mobile/rust/Cargo.toml b/apps/mobile/rust/Cargo.toml index 06eefe59e..d83af8f06 100644 --- a/apps/mobile/rust/Cargo.toml +++ b/apps/mobile/rust/Cargo.toml @@ -22,6 +22,8 @@ openssl = { version = "0.10.42", features = [ openssl-sys = { version = "0.9.76", features = [ "vendored", ] } # Override features of transitive dependencies to support IOS Simulator on M1 +futures = "0.3.24" +tracing = "0.1.37" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" @@ -31,3 +33,6 @@ objc-foundation = "0.1.1" # This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93 [target.'cfg(not(target_os = "ios"))'.dependencies] jni = "0.19.0" + +[target.'cfg(not(target_os = "ios"))'.features] +default = ["sd-core/android"] \ No newline at end of file diff --git a/apps/mobile/rust/src/android.rs b/apps/mobile/rust/src/android.rs index ed93a44de..2f45b7b6b 100644 --- a/apps/mobile/rust/src/android.rs +++ b/apps/mobile/rust/src/android.rs @@ -1,27 +1,14 @@ use std::panic; use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS}; -use jni::objects::{GlobalRef, JClass, JObject, JString}; -use jni::{JNIEnv, JavaVM}; +use futures::future::join_all; +use jni::objects::{JClass, JObject, JString}; +use jni::JNIEnv; use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap}; use sd_core::Node; +use serde_json::Value; use tokio::sync::mpsc::unbounded_channel; - -// fn print(jvm: &JavaVM, class: &GlobalRef, msg: &str) { -// let env = jvm.attach_current_thread().unwrap(); -// env.call_method( -// class, -// "print", -// "(Ljava/lang/String;)V", -// &[env -// .new_string(msg) -// .expect("Couldn't create java string!") -// .into()], -// ) -// .unwrap() -// .l() -// .unwrap(); -// } +use tracing::{error, info}; #[no_mangle] pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener( @@ -86,63 +73,105 @@ pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg( let callback = env.new_global_ref(callback).unwrap(); RUNTIME.spawn(async move { - let request: Request = serde_json::from_str(&query).unwrap(); + let (node, router) = { + let node = &mut *NODE.lock().await; + match node { + Some(node) => node.clone(), + None => { + let data_dir: String = { + let env = jvm.attach_current_thread().unwrap(); + let data_dir = env + .call_method( + &class, + "getDataDirectory", + "()Ljava/lang/String;", + &[], + ) + .unwrap() + .l() + .unwrap(); - let node = &mut *NODE.lock().await; - let (node, router) = match node { - Some(node) => node.clone(), - None => { - let data_dir: String = { - let env = jvm.attach_current_thread().unwrap(); - let data_dir = env - .call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[]) - .unwrap() - .l() - .unwrap(); + env.get_string(data_dir.into()).unwrap().into() + }; - env.get_string(data_dir.into()).unwrap().into() - }; + let new_node = Node::new(data_dir).await; + let new_node = match new_node { + Ok(new_node) => new_node, + Err(err) => { + info!("677 {:?}", err); - let new_node = Node::new(data_dir).await.unwrap(); - node.replace(new_node.clone()); - new_node + // TODO: Android return? + return; + } + }; + + node.replace(new_node.clone()); + new_node + } } }; - let mut channel = EVENT_SENDER.get().unwrap().clone(); - let mut resp = Sender::ResponseAndChannel(None, &mut channel); - handle_json_rpc( - node.get_request_context(), - request, - &router, - &mut resp, - &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), - ) + let reqs = + match serde_json::from_str::(&query).and_then(|v| match v.is_array() { + true => serde_json::from_value::>(v), + false => serde_json::from_value::(v).map(|v| vec![v]), + }) { + Ok(v) => v, + Err(err) => { + error!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config! + return; + } + }; + + let resps = join_all(reqs.into_iter().map(|request| { + let node = node.clone(); + let router = router.clone(); + async move { + let mut channel = EVENT_SENDER.get().unwrap().clone(); + let mut resp = Sender::ResponseAndChannel(None, &mut channel); + + handle_json_rpc( + node.get_request_context(), + request, + &router, + &mut resp, + &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), + ) + .await; + + match resp { + Sender::ResponseAndChannel(resp, _) => resp, + _ => unreachable!(), + } + } + })) .await; - match resp { - Sender::Response(Some(resp)) => { - let env = jvm.attach_current_thread().unwrap(); - env.call_method( - &callback, - "resolve", - "(Ljava/lang/Object;)V", - &[env - .new_string(serde_json::to_string(&resp).unwrap()) - .expect("Couldn't create java string!") - .into()], + let env = jvm.attach_current_thread().unwrap(); + env.call_method( + &callback, + "resolve", + "(Ljava/lang/Object;)V", + &[env + .new_string( + serde_json::to_string( + &resps.into_iter().filter_map(|v| v).collect::>(), + ) + .unwrap(), ) - .unwrap(); - } - _ => unreachable!(), - } + .expect("Couldn't create java string!") + .into()], + ) + .unwrap(); }); }); if let Err(err) = result { // TODO: Send rspc error or something here so we can show this in the UI. // TODO: Maybe reinitialise the core cause it could be in an invalid state? - println!( + + // TODO: This log statement doesn't work. I recon the JNI env is being dropped before it's called. + error!( "Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}", err ); diff --git a/apps/mobile/rust/src/ios.rs b/apps/mobile/rust/src/ios.rs index a21ead310..d17df064e 100644 --- a/apps/mobile/rust/src/ios.rs +++ b/apps/mobile/rust/src/ios.rs @@ -1,9 +1,11 @@ use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS}; +use futures::future::join_all; use objc::{msg_send, runtime::Object, sel, sel_impl}; use objc_foundation::{INSString, NSString}; use objc_id::Id; use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap}; use sd_core::Node; +use serde_json::Value; use std::{ ffi::{CStr, CString}, os::raw::{c_char, c_void}, @@ -69,39 +71,63 @@ pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_voi let resolve = RNPromise(resolve); RUNTIME.spawn(async move { - let request: Request = serde_json::from_str(&query).unwrap(); + let reqs = + match serde_json::from_str::(&query).and_then(|v| match v.is_array() { + true => serde_json::from_value::>(v), + false => serde_json::from_value::(v).map(|v| vec![v]), + }) { + Ok(v) => v, + Err(err) => { + println!("failed to decode JSON-RPC request: {}", err); // Don't use tracing here because it's before the `Node` is initialised which sets that config! - let node = &mut *NODE.lock().await; - let (node, router) = match node { - Some(node) => node.clone(), - None => { - let doc_dir = CStr::from_ptr(get_data_directory()) - .to_str() - .unwrap() - .to_string(); - let new_node = Node::new(doc_dir).await.unwrap(); - node.replace(new_node.clone()); - new_node + resolve.resolve( + CString::new(serde_json::to_vec(&(vec![] as Vec)).unwrap()) + .unwrap(), + ); // TODO: Proper error handling + return; + } + }; + + let resps = join_all(reqs.into_iter().map(|request| async move { + let node = &mut *NODE.lock().await; + let (node, router) = match node { + Some(node) => node.clone(), + None => { + let data_dir = CStr::from_ptr(get_data_directory()) + .to_str() + .unwrap() + .to_string(); + let new_node = Node::new(data_dir).await.unwrap(); + node.replace(new_node.clone()); + new_node + } + }; + + let mut channel = EVENT_SENDER.get().unwrap().clone(); + let mut resp = Sender::ResponseAndChannel(None, &mut channel); + handle_json_rpc( + node.get_request_context(), + request, + &router, + &mut resp, + &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), + ) + .await; + + match resp { + Sender::ResponseAndChannel(resp, _) => resp, + _ => unreachable!(), } - }; - - let mut channel = EVENT_SENDER.get().unwrap().clone(); - let mut resp = Sender::ResponseAndChannel(None, &mut channel); - handle_json_rpc( - node.get_request_context(), - request, - &router, - &mut resp, - &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), - ) + })) .await; - match resp { - Sender::ResponseAndChannel(Some(resp), _) => { - resolve.resolve(CString::new(serde_json::to_vec(&resp).unwrap()).unwrap()); - } - _ => unreachable!(), - } + resolve.resolve( + CString::new( + serde_json::to_vec(&resps.into_iter().filter_map(|v| v).collect::>()) + .unwrap(), + ) + .unwrap(), + ); }); }); diff --git a/apps/mobile/scripts/postinstall.js b/apps/mobile/scripts/postinstall.js index 8ad068a31..5965af9ea 100644 --- a/apps/mobile/scripts/postinstall.js +++ b/apps/mobile/scripts/postinstall.js @@ -3,11 +3,16 @@ let fs = require('fs-extra'); let path = require('path'); async function copyReactNativeCodegen() { - const sourcePath = path.join(__dirname, '../../../node_modules/react-native-codegen'); - const destPath = path.join(__dirname, '../node_modules/react-native-codegen'); + const paths = [ + ['../../../node_modules/react-native-codegen', '../node_modules/react-native-codegen'], + ['../../../node_modules/jsc-android', '../node_modules/jsc-android'] + ]; - await fs.remove(destPath).catch(() => {}); - await fs.move(sourcePath, destPath); + for (const pathTuple of paths) { + const [src, dest] = [path.join(__dirname, pathTuple[0]), path.join(__dirname, pathTuple[1])]; + await fs.remove(dest).catch(() => {}); + await fs.move(src, dest).catch(() => {}); + } } copyReactNativeCodegen(); diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 472aee36b..b4442a52b 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -1,7 +1,7 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native'; import { createClient } from '@rspc/client'; -import * as sdclient from '@sd/client'; +import { queryClient, rspc, useBridgeQuery, useInvalidateQuery } from '@sd/client'; import { StatusBar } from 'expo-status-bar'; import { useEffect } from 'react'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -10,14 +10,8 @@ import { useDeviceContext } from 'twrnc'; import { useSnapshot } from 'valtio'; import { GlobalModals } from './components/modals/GlobalModals'; -import { - ReactNativeTransport, - queryClient, - rspc, - useBridgeQuery, - useInvalidateQuery -} from './hooks/rspc'; import useCachedResources from './hooks/useCachedResources'; +import { reactNativeLink } from './lib/rspcReactNativeTransport'; import tw from './lib/tailwind'; import RootNavigator from './navigation'; import OnboardingNavigator from './navigation/OnboardingNavigator'; @@ -26,7 +20,7 @@ import { onboardingStore } from './stores/onboardingStore'; import type { Procedures } from './types/bindings'; const client = createClient({ - transport: new ReactNativeTransport() + links: [reactNativeLink()] }); const NavigatorTheme: Theme = { diff --git a/apps/mobile/src/hooks/rspc.ts b/apps/mobile/src/hooks/rspc.ts deleted file mode 100644 index 774f03b7c..000000000 --- a/apps/mobile/src/hooks/rspc.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { OperationType, ProcedureDef, RSPCError, Transport } from '@rspc/client'; -import { createReactQueryHooks } from '@rspc/react'; -import { QueryClient } from '@tanstack/react-query'; -import { NativeEventEmitter, NativeModules } from 'react-native'; - -import { getLibraryIdRaw } from '../stores/libraryStore'; -import { LibraryArgs, Procedures } from '../types/bindings'; - -export const queryClient = new QueryClient(); -export const rspc = createReactQueryHooks(); - -const { SDCore } = NativeModules; -const eventEmitter = new NativeEventEmitter(NativeModules.SDCore); - -// TODO(@Oscar): Replace this with a better abstraction when it's released in rspc. This relies on internal details of rspc which will change without warning. -export class ReactNativeTransport implements Transport { - clientSubscriptionCallback?: (id: string, value: any) => void; - - constructor() { - const subscriptionEventListener = eventEmitter.addListener('SDCoreEvent', (event) => { - const { id, result } = JSON.parse(event); - if (result.type === 'event') { - if (this.clientSubscriptionCallback) this.clientSubscriptionCallback(id, result.data); - } else if (result.type === 'response' || result.type === 'error') { - throw new Error( - `Recieved event of type '${result.type}'. This should be impossible with the React Native transport!` - ); - } else { - console.error(`Received event of unknown method '${result.type}'`); - } - }); - } - - async doRequest(operation: OperationType, key: string, input: any): Promise { - const resp = JSON.parse( - await SDCore.sd_core_msg( - JSON.stringify({ - id: null, - method: operation, - params: { - path: key, - input - } - }) - ) - ); - - const body = resp.result; - if (body.type === 'error') { - const { code, message } = body; - throw new RSPCError(code, message); - } else if (body.type === 'response') { - return body.data; - } else if (body.type !== 'none') { - throw new Error(`RSPC ReactNative doRequest received invalid body type '${body?.type}'`); - } - } -} - -type NonLibraryProcedure = - | Exclude }> - | Extract; - -type LibraryProcedures = Exclude< - Extract }>, - { input: never } ->; - -type MoreConstrainedQueries = T extends any - ? T['input'] extends LibraryArgs - ? { - key: T['key']; - input: E; - result: T['result']; - } - : never - : never; - -export const useBridgeQuery = rspc.customQuery>( - (keyAndInput) => keyAndInput as any -); - -export const useBridgeMutation = rspc.customMutation>( - (keyAndInput) => keyAndInput -); - -export const useLibraryQuery = rspc.customQuery< - MoreConstrainedQueries> ->((keyAndInput) => { - const library_id = getLibraryIdRaw(); - if (library_id === null) throw new Error('Attempted to do library query with no library set!'); - return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; -}); - -export const useLibraryMutation = rspc.customMutation< - MoreConstrainedQueries> ->((keyAndInput) => { - const library_id = getLibraryIdRaw(); - if (library_id === null) throw new Error('Attempted to do library query with no library set!'); - return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; -}); - -export function useInvalidateQuery() { - const context = rspc.useContext(); - rspc.useSubscription(['invalidateQuery'], { - onData: (invalidateOperation) => { - const key = [invalidateOperation.key]; - if (invalidateOperation.arg !== null) { - key.concat(invalidateOperation.arg); - } - context.queryClient.invalidateQueries(key); - } - }); -} diff --git a/apps/mobile/src/lib/rspcReactNativeTransport.ts b/apps/mobile/src/lib/rspcReactNativeTransport.ts new file mode 100644 index 000000000..f79f89859 --- /dev/null +++ b/apps/mobile/src/lib/rspcReactNativeTransport.ts @@ -0,0 +1,146 @@ +import { + Operation, + ProcedureType, + ProceduresDef, + TRPCClientOutgoingMessage, + TRPCLink, + TRPCRequestMessage, + TRPCWebSocketClient, + UnsubscribeFn, + wsLink +} from '@rspc/client'; +import { NativeEventEmitter, NativeModules } from 'react-native'; + +type TCallbacks = any; // TODO + +const { SDCore } = NativeModules; +const eventEmitter = new NativeEventEmitter(NativeModules.SDCore); + +export function reactNativeLink(): TRPCLink { + return wsLink({ + client: createReactNativeClient() + }); +} + +export function createReactNativeClient(): TRPCWebSocketClient { + /** + * outgoing messages buffer whilst not open + */ + let outgoing: TRPCClientOutgoingMessage[] = []; + /** + * pending outgoing requests that are awaiting callback + */ + type TRequest = { + /** + * Reference to the WebSocket instance this request was made to + */ + ws: WebSocket; + type: ProcedureType; + callbacks: TCallbacks; + op: Operation; + }; + const pendingRequests: Record = Object.create(null); + let dispatchTimer: ReturnType | number | null = null; + let state: 'open' | 'closed' = 'open'; + + function handleIncoming(data: any) { + if ('method' in data) { + // + } else { + const req = data.id !== null && pendingRequests[data.id]; + if (!req) { + // do something? + return; + } + req.callbacks.next?.(data); + if ('result' in data && data.result.type === 'stopped') { + req.callbacks.complete(); + } + } + } + + function dispatch() { + if (state !== 'open' || dispatchTimer) { + return; + } + dispatchTimer = setTimeout(() => { + dispatchTimer = null; + + if (outgoing.length === 0) { + return; + } + + let body: any; + if (outgoing.length === 1) { + // single send + body = JSON.stringify(outgoing.pop()); + } else { + // batch send + body = JSON.stringify(outgoing); + } + + SDCore.sd_core_msg(body).then((rawData) => { + const data = JSON.parse(rawData); + if (Array.isArray(data)) { + for (const payload of data) { + handleIncoming(payload); + } + } else { + handleIncoming(data); + } + }); + + // clear + outgoing = []; + }); + } + + eventEmitter.addListener('SDCoreEvent', (event) => { + const data = JSON.parse(event); + handleIncoming(data); + }); + + function request(op: Operation, callbacks: TCallbacks): UnsubscribeFn { + const { type, input, path, id } = op; + const envelope: TRPCRequestMessage = { + id, + method: type, + params: { + input, + path + } + }; + pendingRequests[id] = { + ws: undefined as any, // TODO: Remove this field + type, + callbacks, + op + }; + // enqueue message + outgoing.push(envelope); + dispatch(); + return () => { + const callbacks = pendingRequests[id]?.callbacks; + delete pendingRequests[id]; + outgoing = outgoing.filter((msg) => msg.id !== id); + callbacks?.complete?.(); + if (op.type === 'subscription') { + outgoing.push({ + id, + method: 'subscriptionStop' + }); + dispatch(); + } + }; + } + + return { + close: () => { + state = 'closed'; + // TODO: Close all open subscriptions + // closeIfNoPending(activeConnection); + // TODO + }, + request + }; +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 532f36461..be783ebfa 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,7 @@ p2p = [ ] # This feature controls whether the Spacedrive Core contains the Peer to Peer syncing engine (It isn't required for the hosted core so we can disable it). mobile = [ ] # This feature allows features to be disabled when the Core is running on mobile. +android = ["dep:tracing-android"] ffmpeg = [ "dep:ffmpeg-next", "dep:sd-ffmpeg", @@ -57,6 +58,7 @@ sd-file-ext = { path = "../crates/file-ext" } fs_extra = "1.2.0" tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +tracing-android = { version = "0.2.0", optional = true } async-stream = "0.3.3" once_cell = "1.15.0" ctor = "0.1.23" diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 94a557a33..b8ec70b8e 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -1,5 +1,4 @@ use std::{ - path::PathBuf, sync::Arc, time::{Duration, Instant}, }; @@ -52,8 +51,15 @@ struct NodeState { } pub(crate) fn mount() -> Arc { + let config = Config::new().set_ts_bindings_header("/* eslint-disable */"); + + #[cfg(all(debug_assertions, not(feature = "mobile")))] + let config = config.export_ts_bindings( + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts"), + ); + let r = ::new() - .config(Config::new().set_ts_bindings_header("/* eslint-disable */")) + .config(config) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("getNode", |t| { t(|ctx, _: ()| async move { @@ -96,24 +102,14 @@ pub(crate) fn mount() -> Arc { .build() .arced(); InvalidRequests::validate(r.clone()); // This validates all invalidation calls. - export_ts_bindings(&r); r } -pub fn export_ts_bindings(r: &Router) { - r.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts")) - .expect("Error exporting rspc Typescript bindings!"); - r.export_ts( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"), - ) - .expect("Error exporting rspc Typescript bindings!"); -} - #[cfg(test)] mod tests { /// This test will ensure the rspc router and all calls to `invalidate_query` are valid and also export an updated version of the Typescript bindings. #[test] fn test_and_export_rspc_bindings() { - super::export_ts_bindings(&super::mount()); + super::mount(); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index db2ebf73f..dd0fb5bfa 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -48,8 +48,7 @@ impl Node { let data_dir = data_dir.as_ref(); #[cfg(debug_assertions)] let data_dir = data_dir.join("dev"); - - fs::create_dir_all(&data_dir).await?; + let _ = fs::create_dir_all(&data_dir).await; // This error is ignore because it throwing on mobile despite the folder existing. // dbg!(get_object_kind_from_extension("png")); @@ -59,31 +58,39 @@ impl Node { // )); // TODO: Make logs automatically delete after x time https://github.com/tokio-rs/tracing/pull/2169 - tracing_subscriber::registry() - .with( - EnvFilter::from_default_env() - .add_directive("warn".parse().expect("Error invalid tracing directive!")) - .add_directive( - "sdcore=debug" - .parse() - .expect("Error invalid tracing directive!"), - ) - .add_directive( - "server=debug" - .parse() - .expect("Error invalid tracing directive!"), - ) - .add_directive( - "desktop=debug" - .parse() - .expect("Error invalid tracing directive!"), - ), // .add_directive( - // "rspc=debug" - // .parse() - // .expect("Error invalid tracing directive!"), - // ), - ) - .with(fmt::layer().with_filter(CONSOLE_LOG_FILTER)) + let subscriber = tracing_subscriber::registry().with( + EnvFilter::from_default_env() + .add_directive("warn".parse().expect("Error invalid tracing directive!")) + .add_directive( + "sd-core=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "sd-core-mobile=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "server=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "desktop=debug" + .parse() + .expect("Error invalid tracing directive!"), + ), // .add_directive( + // "rspc=debug" + // .parse() + // .expect("Error invalid tracing directive!"), + // ), + ); + #[cfg(not(feature = "android"))] + let subscriber = subscriber.with(fmt::layer().with_filter(CONSOLE_LOG_FILTER)); + #[cfg(feature = "android")] + let subscriber = subscriber.with(tracing_android::layer("com.spacedrive.app").unwrap()); // TODO: This is not working + subscriber // .with( // Layer::default() // .with_writer(non_blocking) From 5f90c8bfe87f1011c52f7ea20c6cbd81122e9a82 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 18 Oct 2022 20:58:05 +0800 Subject: [PATCH 3/5] Removing `import React` from mobile --- apps/landing/src/App.tsx | 1 - apps/landing/src/components/DocsSidebar.tsx | 4 +--- apps/landing/src/components/HomeCTA.tsx | 5 ++--- apps/landing/src/pages/docs/api.ts | 4 ++-- apps/landing/src/pages/docs/doc.page.tsx | 5 +---- apps/landing/src/pages/roadmap.page.tsx | 2 -- apps/mobile/android/app/build.gradle | 8 ++++---- apps/mobile/src/components/animation/layout.tsx | 4 ++-- apps/mobile/src/components/browse/BrowseLocationItem.tsx | 4 ++-- apps/mobile/src/components/browse/BrowseTagItem.tsx | 4 ++-- apps/mobile/src/components/device/Device.tsx | 1 - apps/mobile/src/components/drawer/DrawerContent.tsx | 1 - .../mobile/src/components/drawer/DrawerLibraryManager.tsx | 4 ++-- apps/mobile/src/components/drawer/DrawerLocationItem.tsx | 4 ++-- apps/mobile/src/components/drawer/DrawerTagItem.tsx | 4 ++-- apps/mobile/src/components/file/FileIcon.tsx | 1 - apps/mobile/src/components/header/Header.tsx | 1 - apps/mobile/src/components/icons/FolderIcon.tsx | 1 - apps/mobile/src/components/layout/CollapsibleView.tsx | 4 ++-- apps/mobile/src/components/layout/Dialog.tsx | 6 +++--- .../src/components/layout/VirtualizedListWrapper.tsx | 1 - .../mobile/src/components/modals/layout/ModalBackdrop.tsx | 1 - apps/mobile/src/components/modals/layout/ModalHandle.tsx | 1 - apps/mobile/src/components/primitive/Button.tsx | 6 +++--- apps/mobile/src/components/primitive/Divider.tsx | 1 - apps/mobile/src/components/primitive/Input.tsx | 4 ++-- apps/mobile/src/containers/OverviewStats.tsx | 4 ++-- apps/mobile/src/screens/Browse.tsx | 1 - apps/mobile/src/screens/Location.tsx | 1 - apps/mobile/src/screens/Overview.tsx | 1 - apps/mobile/src/screens/Photos.tsx | 1 - apps/mobile/src/screens/Spaces.tsx | 1 - apps/mobile/src/screens/Tag.tsx | 1 - apps/mobile/src/screens/modals/Search.tsx | 2 +- apps/mobile/src/screens/modals/settings/Settings.tsx | 1 - apps/mobile/src/screens/onboarding/CreateLibrary.tsx | 1 - apps/mobile/src/screens/onboarding/Onboarding.tsx | 1 - apps/mobile/src/stores/libraryStore.ts | 2 +- apps/mobile/src/stores/modalStore.ts | 4 ++-- .../src/components/location/LocationListItem.tsx | 2 +- packages/ui/src/Dropdown.tsx | 6 +++--- 41 files changed, 42 insertions(+), 69 deletions(-) diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index ca83abe21..cec688781 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -1,4 +1,3 @@ -import { Button } from '@sd/ui'; import React from 'react'; import { PageContextBuiltIn } from 'vite-plugin-ssr'; diff --git a/apps/landing/src/components/DocsSidebar.tsx b/apps/landing/src/components/DocsSidebar.tsx index 7fff8ae15..413778469 100644 --- a/apps/landing/src/components/DocsSidebar.tsx +++ b/apps/landing/src/components/DocsSidebar.tsx @@ -1,10 +1,8 @@ -import { CogIcon } from '@heroicons/react/24/outline'; import { Input } from '@sd/ui'; import clsx from 'clsx'; import { MagnifyingGlass } from 'phosphor-react'; -import React, { useCallback } from 'react'; -import { DocCategory, DocsNavigation } from '../pages/docs/api'; +import { DocsNavigation } from '../pages/docs/api'; import config from '../pages/docs/docs'; interface Props { diff --git a/apps/landing/src/components/HomeCTA.tsx b/apps/landing/src/components/HomeCTA.tsx index 636017c76..80a5fd80b 100644 --- a/apps/landing/src/components/HomeCTA.tsx +++ b/apps/landing/src/components/HomeCTA.tsx @@ -1,9 +1,8 @@ import { Github } from '@icons-pack/react-simple-icons'; import { Button, Input } from '@sd/ui'; import clsx from 'clsx'; -import React, { FormEvent, useState } from 'react'; -// import ReactCanvasConfetti from 'react-canvas-confetti'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; import { ReactComponent as Alert } from '@sd/interface/assets/svg/alert.svg'; import { ReactComponent as Info } from '@sd/interface/assets/svg/info.svg'; diff --git a/apps/landing/src/pages/docs/api.ts b/apps/landing/src/pages/docs/api.ts index c3710922d..ceebb3844 100644 --- a/apps/landing/src/pages/docs/api.ts +++ b/apps/landing/src/pages/docs/api.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import { Component } from 'react'; import { parseMarkdown } from '../../utils/markdownParse'; @@ -15,7 +15,7 @@ export interface Doc { export interface DocSectionConfig { title: string; slug: string; - icon?: React.Component | any; + icon?: Component | any; } export interface DocsConfig { diff --git a/apps/landing/src/pages/docs/doc.page.tsx b/apps/landing/src/pages/docs/doc.page.tsx index b760e43ea..8a2157f25 100644 --- a/apps/landing/src/pages/docs/doc.page.tsx +++ b/apps/landing/src/pages/docs/doc.page.tsx @@ -1,9 +1,6 @@ -import { Disclosure, Menu, Transition } from '@headlessui/react'; import { ChevronRightIcon } from '@heroicons/react/24/solid'; import { Github } from '@icons-pack/react-simple-icons'; -import { Button } from '@sd/ui'; -import { List } from 'phosphor-react'; -import React, { PropsWithChildren, useEffect } from 'react'; +import { PropsWithChildren } from 'react'; import { Helmet } from 'react-helmet'; import '../../atom-one.css'; diff --git a/apps/landing/src/pages/roadmap.page.tsx b/apps/landing/src/pages/roadmap.page.tsx index ef56eed77..ffaa52e01 100644 --- a/apps/landing/src/pages/roadmap.page.tsx +++ b/apps/landing/src/pages/roadmap.page.tsx @@ -1,6 +1,4 @@ -import { Button } from '@sd/ui'; import clsx from 'clsx'; -import React from 'react'; import { Helmet } from 'react-helmet'; import { Folder } from '../../../../packages/interface/src/components/icons/Folder'; diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index c0a11d947..0f068711e 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -9,10 +9,10 @@ cargo { module = "../../rust" libname = "sd_core_mobile" pythonCommand = 'python3' - // profile = 'release', - // targets = ["arm", "arm64", "x86", "x86_64"] - profile = 'debug' - targets = ["arm64"] + profile = 'release', + targets = ["arm", "arm64", "x86", "x86_64"] + // profile = 'debug' + // targets = ["arm64"] targetDirectory = "../.././../../target" // Monorepo moment } diff --git a/apps/mobile/src/components/animation/layout.tsx b/apps/mobile/src/components/animation/layout.tsx index f1dabe3ca..727838f43 100644 --- a/apps/mobile/src/components/animation/layout.tsx +++ b/apps/mobile/src/components/animation/layout.tsx @@ -1,5 +1,5 @@ import { MotiView, useDynamicAnimation } from 'moti'; -import React from 'react'; +import { ReactNode } from 'react'; import { StyleSheet, View } from 'react-native'; import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; import Layout from '~/constants/Layout'; @@ -33,7 +33,7 @@ export const LogoAnimation = ({ children }: { children: any }) => ( ); type AnimatedHeightProps = { - children?: React.ReactNode; + children?: ReactNode; /** * If `true`, the height will automatically animate to 0. Default: `false`. */ diff --git a/apps/mobile/src/components/browse/BrowseLocationItem.tsx b/apps/mobile/src/components/browse/BrowseLocationItem.tsx index 49c271b1d..7af42524e 100644 --- a/apps/mobile/src/components/browse/BrowseLocationItem.tsx +++ b/apps/mobile/src/components/browse/BrowseLocationItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FC } from 'react'; import { Pressable, Text, View } from 'react-native'; import tw from '~/lib/tailwind'; @@ -9,7 +9,7 @@ interface BrowseLocationItemProps { onPress: () => void; } -const BrowseLocationItem: React.FC = (props) => { +const BrowseLocationItem: FC = (props) => { const { folderName, onPress } = props; return ( diff --git a/apps/mobile/src/components/browse/BrowseTagItem.tsx b/apps/mobile/src/components/browse/BrowseTagItem.tsx index e89477f1e..ddd77a0ba 100644 --- a/apps/mobile/src/components/browse/BrowseTagItem.tsx +++ b/apps/mobile/src/components/browse/BrowseTagItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FC } from 'react'; import { ColorValue, Pressable, Text, View } from 'react-native'; import tw from '~/lib/tailwind'; @@ -8,7 +8,7 @@ type BrowseTagItemProps = { onPress: () => void; }; -const BrowseTagItem: React.FC = (props) => { +const BrowseTagItem: FC = (props) => { const { tagName, tagColor, onPress } = props; return ( diff --git a/apps/mobile/src/components/device/Device.tsx b/apps/mobile/src/components/device/Device.tsx index 9c45e7d59..9cfffb9f5 100644 --- a/apps/mobile/src/components/device/Device.tsx +++ b/apps/mobile/src/components/device/Device.tsx @@ -1,5 +1,4 @@ import { Cloud, Desktop, DeviceMobileCamera, Laptop } from 'phosphor-react-native'; -import React from 'react'; import { FlatList, Text, View } from 'react-native'; import { LockClosedIcon } from 'react-native-heroicons/solid'; import tw from '~/lib/tailwind'; diff --git a/apps/mobile/src/components/drawer/DrawerContent.tsx b/apps/mobile/src/components/drawer/DrawerContent.tsx index 49c0b59c8..22af62b30 100644 --- a/apps/mobile/src/components/drawer/DrawerContent.tsx +++ b/apps/mobile/src/components/drawer/DrawerContent.tsx @@ -1,7 +1,6 @@ import { DrawerContentScrollView } from '@react-navigation/drawer'; import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types'; import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; -import React from 'react'; import { ColorValue, Image, Platform, Pressable, Text, View } from 'react-native'; import { CogIcon } from 'react-native-heroicons/solid'; import Layout from '~/constants/Layout'; diff --git a/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx b/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx index 97ac809f0..28d8fec4e 100644 --- a/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx +++ b/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx @@ -1,10 +1,10 @@ +import { useBridgeMutation } from '@sd/client'; import { MotiView } from 'moti'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import { LockClosedIcon } from 'react-native-heroicons/outline'; import { ChevronRightIcon, CogIcon, PlusIcon } from 'react-native-heroicons/solid'; import { useSnapshot } from 'valtio'; -import { useBridgeMutation } from '~/hooks/rspc'; import tw from '~/lib/tailwind'; import { libraryStore, useCurrentLibrary } from '~/stores/libraryStore'; diff --git a/apps/mobile/src/components/drawer/DrawerLocationItem.tsx b/apps/mobile/src/components/drawer/DrawerLocationItem.tsx index c950f2e82..a82e911ca 100644 --- a/apps/mobile/src/components/drawer/DrawerLocationItem.tsx +++ b/apps/mobile/src/components/drawer/DrawerLocationItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FC } from 'react'; import { Pressable, Text, View } from 'react-native'; import tw from '~/lib/tailwind'; @@ -9,7 +9,7 @@ interface DrawerLocationItemProps { onPress: () => void; } -const DrawerLocationItem: React.FC = (props) => { +const DrawerLocationItem: FC = (props) => { const { folderName, onPress } = props; return ( diff --git a/apps/mobile/src/components/drawer/DrawerTagItem.tsx b/apps/mobile/src/components/drawer/DrawerTagItem.tsx index d364e9845..5d405ddd4 100644 --- a/apps/mobile/src/components/drawer/DrawerTagItem.tsx +++ b/apps/mobile/src/components/drawer/DrawerTagItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FC } from 'react'; import { ColorValue, Pressable, Text, View } from 'react-native'; import tw from '~/lib/tailwind'; @@ -8,7 +8,7 @@ type DrawerTagItemProps = { onPress: () => void; }; -const DrawerTagItem: React.FC = (props) => { +const DrawerTagItem: FC = (props) => { const { tagName, tagColor, onPress } = props; return ( diff --git a/apps/mobile/src/components/file/FileIcon.tsx b/apps/mobile/src/components/file/FileIcon.tsx index db646bebe..56b086e75 100644 --- a/apps/mobile/src/components/file/FileIcon.tsx +++ b/apps/mobile/src/components/file/FileIcon.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import Svg, { Path } from 'react-native-svg'; diff --git a/apps/mobile/src/components/header/Header.tsx b/apps/mobile/src/components/header/Header.tsx index df139c78f..99f51b770 100644 --- a/apps/mobile/src/components/header/Header.tsx +++ b/apps/mobile/src/components/header/Header.tsx @@ -3,7 +3,6 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript import { useNavigation } from '@react-navigation/native'; import { MotiView } from 'moti'; import { List } from 'phosphor-react-native'; -import React from 'react'; import { Pressable, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import tw from '~/lib/tailwind'; diff --git a/apps/mobile/src/components/icons/FolderIcon.tsx b/apps/mobile/src/components/icons/FolderIcon.tsx index 311544248..54f994f9f 100644 --- a/apps/mobile/src/components/icons/FolderIcon.tsx +++ b/apps/mobile/src/components/icons/FolderIcon.tsx @@ -1,6 +1,5 @@ import FolderWhite from '@sd/assets/svgs/folder-white.svg'; import Folder from '@sd/assets/svgs/folder.svg'; -import React from 'react'; import { SvgProps } from 'react-native-svg'; type FolderProps = { diff --git a/apps/mobile/src/components/layout/CollapsibleView.tsx b/apps/mobile/src/components/layout/CollapsibleView.tsx index 0d55838ae..5e49a6f93 100644 --- a/apps/mobile/src/components/layout/CollapsibleView.tsx +++ b/apps/mobile/src/components/layout/CollapsibleView.tsx @@ -1,5 +1,5 @@ import { MotiView } from 'moti'; -import React, { useReducer } from 'react'; +import { ReactNode, useReducer } from 'react'; import { Pressable, StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native'; import { ChevronRightIcon } from 'react-native-heroicons/solid'; import tw from '~/lib/tailwind'; @@ -9,7 +9,7 @@ import { AnimatedHeight } from '../animation/layout'; type CollapsibleViewProps = { title: string; titleStyle?: StyleProp; - children: React.ReactNode; + children: ReactNode; containerStyle?: StyleProp; }; diff --git a/apps/mobile/src/components/layout/Dialog.tsx b/apps/mobile/src/components/layout/Dialog.tsx index 4b6a634d7..100969fcf 100644 --- a/apps/mobile/src/components/layout/Dialog.tsx +++ b/apps/mobile/src/components/layout/Dialog.tsx @@ -1,5 +1,5 @@ import { MotiView } from 'moti'; -import React, { useState } from 'react'; +import { ReactNode, useState } from 'react'; import { KeyboardAvoidingView, Modal, Platform, Pressable, Text, View } from 'react-native'; import tw from '~/lib/tailwind'; @@ -8,7 +8,7 @@ import { Button } from '../primitive/Button'; type DialogProps = { title: string; description?: string; - trigger?: React.ReactNode; + trigger?: ReactNode; /** * if `true`, dialog will be visible when mounted. * It can be used when trigger is not provided and/or you need to open the dialog programmatically @@ -19,7 +19,7 @@ type DialogProps = { * It can be used to control dialog state from outside */ setIsVisible?: (v: boolean) => void; - children?: React.ReactNode; + children?: ReactNode; ctaAction?: () => void; ctaLabel?: string; ctaDanger?: boolean; diff --git a/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx b/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx index 6a110b358..9f09840bb 100644 --- a/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx +++ b/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { FlatList } from 'react-native'; export default function VirtualizedListWrapper({ children }) { diff --git a/apps/mobile/src/components/modals/layout/ModalBackdrop.tsx b/apps/mobile/src/components/modals/layout/ModalBackdrop.tsx index d14677b15..6faba98f6 100644 --- a/apps/mobile/src/components/modals/layout/ModalBackdrop.tsx +++ b/apps/mobile/src/components/modals/layout/ModalBackdrop.tsx @@ -1,5 +1,4 @@ import { BottomSheetBackdrop, BottomSheetBackdropProps } from '@gorhom/bottom-sheet'; -import React from 'react'; const ModalBackdrop = (props: BottomSheetBackdropProps) => { return ( diff --git a/apps/mobile/src/components/modals/layout/ModalHandle.tsx b/apps/mobile/src/components/modals/layout/ModalHandle.tsx index 22efbe7d1..c60f488fa 100644 --- a/apps/mobile/src/components/modals/layout/ModalHandle.tsx +++ b/apps/mobile/src/components/modals/layout/ModalHandle.tsx @@ -1,5 +1,4 @@ import { BottomSheetHandle, BottomSheetHandleProps } from '@gorhom/bottom-sheet'; -import React from 'react'; import tw from '../../../lib/tailwind'; diff --git a/apps/mobile/src/components/primitive/Button.tsx b/apps/mobile/src/components/primitive/Button.tsx index d2970e3d2..382084086 100644 --- a/apps/mobile/src/components/primitive/Button.tsx +++ b/apps/mobile/src/components/primitive/Button.tsx @@ -1,6 +1,6 @@ import { VariantProps, cva } from 'class-variance-authority'; import { MotiPressable, MotiPressableProps } from 'moti/interactions'; -import React, { useMemo } from 'react'; +import { FC, useMemo } from 'react'; import { Pressable, PressableProps } from 'react-native'; import tw from '~/lib/tailwind'; @@ -28,14 +28,14 @@ const button = cva(['border rounded-md items-center shadow-sm'], { type ButtonProps = VariantProps & PressableProps; -export const Button: React.FC = ({ variant, size, ...props }) => { +export const Button: FC = ({ variant, size, ...props }) => { const { style, ...otherProps } = props; return ; }; type AnimatedButtonProps = VariantProps & MotiPressableProps; -export const AnimatedButton: React.FC = ({ variant, size, ...props }) => { +export const AnimatedButton: FC = ({ variant, size, ...props }) => { const { style, containerStyle, ...otherProps } = props; return ( & RNTextInputProps; -export const TextInput: React.FC = ({ variant, ...props }) => { +export const TextInput: FC = ({ variant, ...props }) => { const { style, ...otherProps } = props; return ( = ({ title, bytes }) => { +const StatItem: FC<{ title: string; bytes: number }> = ({ title, bytes }) => { const { value, unit } = byteSize(+bytes); const count = useCounter({ name: title, end: Number(value) }); diff --git a/apps/mobile/src/screens/Browse.tsx b/apps/mobile/src/screens/Browse.tsx index 3721f0c03..f3bb13188 100644 --- a/apps/mobile/src/screens/Browse.tsx +++ b/apps/mobile/src/screens/Browse.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { ColorValue, Text, View } from 'react-native'; import BrowseLocationItem from '~/components/browse/BrowseLocationItem'; import BrowseTagItem from '~/components/browse/BrowseTagItem'; diff --git a/apps/mobile/src/screens/Location.tsx b/apps/mobile/src/screens/Location.tsx index 2dd2525e7..3e0e5dd0d 100644 --- a/apps/mobile/src/screens/Location.tsx +++ b/apps/mobile/src/screens/Location.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import tw from '~/lib/tailwind'; import { SharedScreenProps } from '~/navigation/SharedScreens'; diff --git a/apps/mobile/src/screens/Overview.tsx b/apps/mobile/src/screens/Overview.tsx index 61424a7ca..dc7f5ff06 100644 --- a/apps/mobile/src/screens/Overview.tsx +++ b/apps/mobile/src/screens/Overview.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { FlatList, View } from 'react-native'; import Device from '~/components/device/Device'; import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper'; diff --git a/apps/mobile/src/screens/Photos.tsx b/apps/mobile/src/screens/Photos.tsx index c1ee9c4eb..a3cc2f50b 100644 --- a/apps/mobile/src/screens/Photos.tsx +++ b/apps/mobile/src/screens/Photos.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import tw from '~/lib/tailwind'; import { PhotosStackScreenProps } from '~/navigation/tabs/PhotosStack'; diff --git a/apps/mobile/src/screens/Spaces.tsx b/apps/mobile/src/screens/Spaces.tsx index 10c8cc5b9..8662d5eb3 100644 --- a/apps/mobile/src/screens/Spaces.tsx +++ b/apps/mobile/src/screens/Spaces.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import tw from '~/lib/tailwind'; import { SpacesStackScreenProps } from '~/navigation/tabs/SpacesStack'; diff --git a/apps/mobile/src/screens/Tag.tsx b/apps/mobile/src/screens/Tag.tsx index 6f5278551..0b61f81a8 100644 --- a/apps/mobile/src/screens/Tag.tsx +++ b/apps/mobile/src/screens/Tag.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import tw from '~/lib/tailwind'; import { SharedScreenProps } from '~/navigation/SharedScreens'; diff --git a/apps/mobile/src/screens/modals/Search.tsx b/apps/mobile/src/screens/modals/Search.tsx index 3fab3b1f7..82e99fb5a 100644 --- a/apps/mobile/src/screens/modals/Search.tsx +++ b/apps/mobile/src/screens/modals/Search.tsx @@ -1,5 +1,5 @@ import { MagnifyingGlass } from 'phosphor-react-native'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Button } from '~/components/primitive/Button'; diff --git a/apps/mobile/src/screens/modals/settings/Settings.tsx b/apps/mobile/src/screens/modals/settings/Settings.tsx index d30472da8..c5056b924 100644 --- a/apps/mobile/src/screens/modals/settings/Settings.tsx +++ b/apps/mobile/src/screens/modals/settings/Settings.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import tw from '~/lib/tailwind'; import { RootStackScreenProps } from '~/navigation'; diff --git a/apps/mobile/src/screens/onboarding/CreateLibrary.tsx b/apps/mobile/src/screens/onboarding/CreateLibrary.tsx index 87aaafae3..377128fa1 100644 --- a/apps/mobile/src/screens/onboarding/CreateLibrary.tsx +++ b/apps/mobile/src/screens/onboarding/CreateLibrary.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Text, View } from 'react-native'; import { useSnapshot } from 'valtio'; import { AnimatedButton } from '~/components/primitive/Button'; diff --git a/apps/mobile/src/screens/onboarding/Onboarding.tsx b/apps/mobile/src/screens/onboarding/Onboarding.tsx index ebeed11f8..aa9c864c8 100644 --- a/apps/mobile/src/screens/onboarding/Onboarding.tsx +++ b/apps/mobile/src/screens/onboarding/Onboarding.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Image, Text, View } from 'react-native'; import { FadeInUpAnimation, LogoAnimation } from '~/components/animation/layout'; import { AnimatedButton } from '~/components/primitive/Button'; diff --git a/apps/mobile/src/stores/libraryStore.ts b/apps/mobile/src/stores/libraryStore.ts index 20f6197c8..f4ae52014 100644 --- a/apps/mobile/src/stores/libraryStore.ts +++ b/apps/mobile/src/stores/libraryStore.ts @@ -1,7 +1,7 @@ +import { useBridgeQuery } from '@sd/client'; import { useMemo } from 'react'; import { useSnapshot } from 'valtio'; import proxyWithPersist, { PersistStrategy } from 'valtio-persist'; -import { useBridgeQuery } from '~/hooks/rspc'; import { LibraryConfigWrapped } from '~/types/bindings'; import { StorageEngine } from './utils'; diff --git a/apps/mobile/src/stores/modalStore.ts b/apps/mobile/src/stores/modalStore.ts index 4753294f5..834f71177 100644 --- a/apps/mobile/src/stores/modalStore.ts +++ b/apps/mobile/src/stores/modalStore.ts @@ -1,11 +1,11 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; -import React from 'react'; +import { RefObject } from 'react'; import { proxy } from 'valtio'; import { FilePath } from '../types/bindings'; export const fileModalStore = proxy({ - fileRef: null as React.RefObject, + fileRef: null as RefObject, data: null as FilePath | null, setData: (data: FilePath) => { fileModalStore.data = data; diff --git a/packages/interface/src/components/location/LocationListItem.tsx b/packages/interface/src/components/location/LocationListItem.tsx index 921861ded..46dc73d40 100644 --- a/packages/interface/src/components/location/LocationListItem.tsx +++ b/packages/interface/src/components/location/LocationListItem.tsx @@ -4,7 +4,7 @@ import { Location, Node } from '@sd/client'; import { Button, Dialog } from '@sd/ui'; import clsx from 'clsx'; import { Repeat } from 'phosphor-react'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { Folder } from '../icons/Folder'; diff --git a/packages/ui/src/Dropdown.tsx b/packages/ui/src/Dropdown.tsx index 84bde7809..498a4e63b 100644 --- a/packages/ui/src/Dropdown.tsx +++ b/packages/ui/src/Dropdown.tsx @@ -1,7 +1,7 @@ import { Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; import clsx from 'clsx'; -import React from 'react'; +import { Component, PropsWithChildren } from 'react'; import { Link } from 'react-router-dom'; import { Button } from './Button'; @@ -12,7 +12,7 @@ export type DropdownItem = ( icon?: any; selected?: boolean; to?: string; - wrapItemComponent?: React.FC<{ children: React.ReactNode }>; + wrapItemComponent?: Component; } | { name: string; @@ -21,7 +21,7 @@ export type DropdownItem = ( selected?: boolean; onPress?: () => any; to?: string; - wrapItemComponent?: React.FC<{ children: React.ReactNode }>; + wrapItemComponent?: Component; } )[]; From 11c370e4c72f0abf705ffe1bfc6c884b6dc04a3c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 18 Oct 2022 21:52:00 +0800 Subject: [PATCH 4/5] purge unused dependencies --- apps/desktop/package.json | 6 ---- apps/landing/package.json | 18 +++++----- apps/mobile/android/app/build.gradle | 2 +- apps/mobile/package.json | 1 - apps/mobile/src/hooks/useCounter.ts | 12 ------- packages/client/package.json | 10 +----- packages/interface/package.json | 31 +----------------- .../src/components/device/Stores.tsx | 24 +++++++------- .../components/explorer/VirtualizedList.tsx | 2 +- .../components/explorer/inspector/Note.tsx | 20 +++++------ packages/interface/src/hooks/useCounter.ts | 18 ++++------ packages/ui/package.json | 4 +-- packages/ui/src/Dropdown.tsx | 10 +++--- pnpm-lock.yaml | Bin 819099 -> 806483 bytes 14 files changed, 47 insertions(+), 111 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4ace83600..72dc6a031 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -16,7 +16,6 @@ "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tanstack/react-query": "^4.12.0", "@tauri-apps/api": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -25,19 +24,14 @@ "@tauri-apps/cli": "1.1.1", "@tauri-apps/tauricon": "github:tauri-apps/tauricon", "@types/babel-core": "^6.25.7", - "@types/byte-size": "^8.1.0", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", - "@types/react-router-dom": "^5.3.3", - "@types/react-window": "^1.8.5", "@types/tailwindcss": "^3.1.0", "@vitejs/plugin-react": "^2.1.0", - "concurrently": "^7.4.0", "prettier": "^2.7.1", "sass": "^1.55.0", "typescript": "^4.8.4", "vite": "^3.1.4", - "vite-plugin-filter-replace": "^0.1.9", "vite-plugin-svgr": "^2.2.1" } } diff --git a/apps/landing/package.json b/apps/landing/package.json index c0dda0abd..f485636b4 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -18,13 +18,6 @@ "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", "@tryghost/content-api": "^1.11.4", - "@types/compression": "^1.7.2", - "@types/express": "^4.17.14", - "@types/marked": "^4.0.7", - "@types/node": "^18.8.2", - "@types/react": "^18.0.21", - "@types/react-burger-menu": "^2.8.3", - "@types/react-dom": "^18.0.6", "@vitejs/plugin-react": "^2.1.0", "clsx": "^1.2.1", "compression": "^1.7.4", @@ -48,7 +41,7 @@ "vite-plugin-ssr": "^0.4.39" }, "devDependencies": { - "@sd/config": "link:../../packages/config", + "@sd/config": "workspace:*", "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.7", "@types/prismjs": "^1.26.0", @@ -59,6 +52,13 @@ "sass": "^1.55.0", "tailwind": "^4.0.0", "vite-plugin-markdown": "^2.1.0", - "vite-plugin-svgr": "^2.2.1" + "vite-plugin-svgr": "^2.2.1", + "@types/compression": "^1.7.2", + "@types/express": "^4.17.14", + "@types/marked": "^4.0.7", + "@types/node": "^18.8.2", + "@types/react": "^18.0.21", + "@types/react-burger-menu": "^2.8.3", + "@types/react-dom": "^18.0.6" } } diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 0f068711e..1433bd53c 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -9,7 +9,7 @@ cargo { module = "../../rust" libname = "sd_core_mobile" pythonCommand = 'python3' - profile = 'release', + profile = 'release' targets = ["arm", "arm64", "x86", "x86_64"] // profile = 'debug' // targets = ["arm64"] diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 917a66786..212eb8980 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -33,7 +33,6 @@ "expo-linking": "~3.2.2", "expo-splash-screen": "~0.16.2", "expo-status-bar": "~1.4.0", - "immer": "^9.0.15", "intl": "^1.2.5", "lottie-react-native": "^5.1.4", "moti": "^0.18.0", diff --git a/apps/mobile/src/hooks/useCounter.ts b/apps/mobile/src/hooks/useCounter.ts index 5b28a5148..a8547d08d 100644 --- a/apps/mobile/src/hooks/useCounter.ts +++ b/apps/mobile/src/hooks/useCounter.ts @@ -2,18 +2,6 @@ import { useEffect } from 'react'; import { useCountUp } from 'use-count-up'; import { proxy, useSnapshot } from 'valtio'; -// const useCounterStore = create<{ -// counterLastValue: Map; -// setCounterLastValue: (key: string, value: number) => void; -// }>((set) => ({ -// counterLastValue: new Map(), -// setCounterLastValue: (name, lastValue) => -// set((state) => ({ -// ...state, -// counterLastValue: state.counterLastValue.set(name, lastValue) -// })) -// })); - const counterStore = proxy({ counterLastValue: new Map(), setCounterLastValue: (key: string, value: number) => { diff --git a/packages/client/package.json b/packages/client/package.json index 74d624b5f..91bab13d4 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -13,23 +13,15 @@ "lint": "TIMING=1 eslint src --fix", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, - "jest": { - "preset": "scripts/jest/node" - }, "dependencies": { "@rspc/client": "^0.0.0-main-7c0a67c1", "@rspc/react": "^0.0.0-main-7c0a67c1", "@sd/config": "workspace:*", "@tanstack/react-query": "^4.12.0", - "eventemitter3": "^4.0.7", - "immer": "^9.0.15", - "lodash": "^4.17.21", "valtio": "^1.7.0", - "valtio-persist": "^1.0.2", - "zustand": "4.1.1" + "valtio-persist": "^1.0.2" }, "devDependencies": { - "@types/lodash": "^4.14.186", "@types/react": "^18.0.21", "scripts": "*", "tsconfig": "*", diff --git a/packages/interface/package.json b/packages/interface/package.json index 755aa31af..1500c3741 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -19,15 +19,10 @@ "@headlessui/react": "^1.7.3", "@heroicons/react": "^2.0.12", "@loadable/component": "^5.15.2", - "@radix-ui/react-dialog": "^1.0.0", - "@radix-ui/react-dropdown-menu": "^1.0.0", - "@radix-ui/react-icons": "^1.1.1", "@radix-ui/react-progress": "^1.0.0", "@radix-ui/react-slider": "^1.0.0", - "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-toast": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", - "@rspc/client": "^0.0.0-main-7c0a67c1", "@sd/assets": "workspace:*", "@sd/client": "workspace:*", "@sd/ui": "workspace:*", @@ -39,58 +34,34 @@ "autoprefixer": "^10.4.12", "byte-size": "^8.1.0", "clsx": "^1.2.1", - "date-fns": "^2.29.3", "dayjs": "^1.11.5", - "immer": "^9.0.15", - "jotai": "^1.8.4", - "lodash": "^4.17.21", - "moment": "^2.29.4", "phosphor-react": "^1.4.1", - "pretty-bytes": "^6.0.0", "react": "^18.2.0", "react-colorful": "^5.6.1", - "react-countup": "^6.3.1", "react-dom": "^18.2.0", - "react-dropzone": "^14.2.2", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.36.1", - "react-hotkeys-hook": "^3.4.7", "react-json-view": "^1.21.3", - "react-loading-icons": "^1.1.0", "react-loading-skeleton": "^3.1.0", - "react-portal": "^4.2.2", - "react-query": "^3.39.2", "react-router": "6.4.2", "react-router-dom": "6.4.2", - "react-scrollbars-custom": "^4.1.1", - "react-spline": "^1.2.1", - "react-transition-group": "^4.4.5", - "react-virtuoso": "^2.19.1", "rooks": "^5.14.0", "tailwindcss": "^3.1.8", "use-count-up": "^3.0.1", "use-debounce": "^8.0.4", - "valtio": "^1.7.0", - "valtio-persist": "^1.0.2", - "zod": "^3.19.1", - "zustand": "4.1.1" + "valtio": "^1.7.0" }, "devDependencies": { "@sd/config": "workspace:*", "@types/babel-core": "^6.25.7", "@types/byte-size": "^8.1.0", "@types/loadable__component": "^5.13.4", - "@types/lodash": "^4.14.186", "@types/node": "^18.8.2", - "@types/pretty-bytes": "^5.2.0", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "@types/react-router-dom": "^5.3.3", - "@types/react-table": "^7.7.12", - "@types/react-window": "^1.8.5", "@types/tailwindcss": "^3.1.0", "@vitejs/plugin-react": "^1.3.1", - "concurrently": "^7.4.0", "prettier": "^2.7.1", "typescript": "^4.8.4", "vite": "^3.1.4", diff --git a/packages/interface/src/components/device/Stores.tsx b/packages/interface/src/components/device/Stores.tsx index e0a4e94a0..a7a72b388 100644 --- a/packages/interface/src/components/device/Stores.tsx +++ b/packages/interface/src/components/device/Stores.tsx @@ -1,19 +1,19 @@ -import create from 'zustand'; +import { useState } from 'react'; const getLocalStorage = (key: string) => JSON.parse(window.localStorage.getItem(key) || '{}'); const setLocalStorage = (key: string, value: any) => window.localStorage.setItem(key, JSON.stringify(value)); -type NodeState = { - isExperimental: boolean; - setIsExperimental: (experimental: boolean) => void; -}; +export function useNodeStore() { + const [state, setState] = useState( + (getLocalStorage('isExperimental') as boolean) === true || false + ); -export const useNodeStore = create((set) => ({ - isExperimental: (getLocalStorage('isExperimental') as boolean) === true || false, - setIsExperimental: (experimental: boolean) => - set((state) => { + return { + isExperimental: state, + setIsExperimental: (experimental: boolean) => { setLocalStorage('isExperimental', experimental); - return { ...state, isExperimental: experimental }; - }) -})); + setState(experimental); + } + }; +} diff --git a/packages/interface/src/components/explorer/VirtualizedList.tsx b/packages/interface/src/components/explorer/VirtualizedList.tsx index d470c8454..a424fa871 100644 --- a/packages/interface/src/components/explorer/VirtualizedList.tsx +++ b/packages/interface/src/components/explorer/VirtualizedList.tsx @@ -1,7 +1,7 @@ import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client'; import { ExplorerContext, ExplorerItem } from '@sd/client'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useKey, useOnWindowResize } from 'rooks'; diff --git a/packages/interface/src/components/explorer/inspector/Note.tsx b/packages/interface/src/components/explorer/inspector/Note.tsx index 5010f4fd9..bbc999894 100644 --- a/packages/interface/src/components/explorer/inspector/Note.tsx +++ b/packages/interface/src/components/explorer/inspector/Note.tsx @@ -1,8 +1,8 @@ import { useLibraryMutation } from '@sd/client'; import { Object as SDObject } from '@sd/client'; import { TextArea } from '@sd/ui'; -import debounce from 'lodash/debounce'; import { useCallback, useState } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; import { Divider } from './Divider'; import { MetaItem } from './MetaItem'; @@ -19,19 +19,17 @@ export default function Note(props: Props) { const { mutate: fileSetNote } = useLibraryMutation('files.setNote'); - const debouncedNote = useCallback( + const debounce = useDebouncedCallback( (note: string) => - debounce( - () => - fileSetNote({ - id: props.data.id, - note - }), - 2000 - ), - [props.data.id, fileSetNote] + fileSetNote({ + id: props.data.id, + note + }), + 2000 ); + const debouncedNote = useCallback((note: string) => debounce(note), [props.data.id, fileSetNote]); + // when input is updated, cache note function handleNoteUpdate(e: React.ChangeEvent) { if (e.target.value !== note) { diff --git a/packages/interface/src/hooks/useCounter.ts b/packages/interface/src/hooks/useCounter.ts index 7132e8f2f..d9e01e249 100644 --- a/packages/interface/src/hooks/useCounter.ts +++ b/packages/interface/src/hooks/useCounter.ts @@ -1,21 +1,15 @@ import { useEffect } from 'react'; import { useCountUp } from 'use-count-up'; -import create from 'zustand'; +import { proxy, useSnapshot } from 'valtio'; -const useCounterStore = create<{ - counterLastValue: Map; - setCounterLastValue(key: string, value: number): void; -}>((set) => ({ +const counterStore = proxy({ counterLastValue: new Map(), - setCounterLastValue: (name, lastValue) => - set((state) => ({ - ...state, - counterLastValue: state.counterLastValue.set(name, lastValue) - })) -})); + setCounterLastValue: (name: string, lastValue: number) => + counterStore.counterLastValue.set(name, lastValue) +}); const useCounterState = (key: string) => { - const { counterLastValue, setCounterLastValue } = useCounterStore(); + const { counterLastValue, setCounterLastValue } = useSnapshot(counterStore); return { lastValue: counterLastValue.get(key), diff --git a/packages/ui/package.json b/packages/ui/package.json index 17794be2c..70c8c4bee 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,11 +34,11 @@ "react-router-dom": "6.4.2", "react-loading-icons": "^1.1.0", "react-spring": "^9.5.5", - "storybook": "^6.5.12", - "tailwindcss": "^3.1.8", "tailwindcss-radix": "^2.6.0" }, "devDependencies": { + "tailwindcss": "^3.1.8", + "storybook": "^6.5.12", "@babel/core": "^7.19.3", "@sd/config": "workspace:*", "@storybook/addon-actions": "^6.5.12", diff --git a/packages/ui/src/Dropdown.tsx b/packages/ui/src/Dropdown.tsx index 498a4e63b..5a3b33b90 100644 --- a/packages/ui/src/Dropdown.tsx +++ b/packages/ui/src/Dropdown.tsx @@ -1,7 +1,7 @@ import { Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; import clsx from 'clsx'; -import { Component, PropsWithChildren } from 'react'; +import { Fragment, PropsWithChildren } from 'react'; import { Link } from 'react-router-dom'; import { Button } from './Button'; @@ -12,7 +12,7 @@ export type DropdownItem = ( icon?: any; selected?: boolean; to?: string; - wrapItemComponent?: Component; + wrapItemComponent?: React.FC; } | { name: string; @@ -21,7 +21,7 @@ export type DropdownItem = ( selected?: boolean; onPress?: () => any; to?: string; - wrapItemComponent?: Component; + wrapItemComponent?: React.FC; } )[]; @@ -65,7 +65,7 @@ export const Dropdown: React.FC = (props) => { = (props) => { {item.map((button, index) => ( {({ active }) => { - const WrappedItem = button.wrapItemComponent + const WrappedItem: any = button.wrapItemComponent ? button.wrapItemComponent : (props: React.PropsWithChildren) => <>{props.children}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1807e652b8eda987548141a8d66e6dff4abb6c41..e211396c16958532a8c185c2210b457488543cd1 100644 GIT binary patch delta 29966 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
  • J0SZz21^w@~eD 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 bvBdzm`1G|5m)D+&_Fu;=!G-G>QUCt}H5VbN delta 36720 zcmd433A7wloj-nR>3&<@%gerRguEnl^18cvsp>#Tdf)f1-hd>%clA=$-PPUI-GUGW z1XKu=a6wUnj)RV)pi#;}ATTKI5E&6clz^k+0t9r%1qOb%yWe|R7{~vd|IsrXPF~lo zTlapK&;8zezxVsS|8}SPBX6tk{R4ui#XlliL?!M@U;sxcbVPLRStKr%c~<6`N;B8k-N@A{sZ0*m zQ_ZSp4dRwvYsBwfxJ9hZPJ{P9gRT|tk$qP@c)?!rC2Vp)7ntZG%fZQu&>i9l`J*Cl z+AMC?tehuKE6|BX!y@&;Hq*@Q*Fd_p;+>=45)XMtMbF|za@cB~DIBYfeO`u8V2cJ_EgHra zi4W}UzdkN{$Cr!gWtWJ@skPwV;}cT@+QHOzd_vr~oDkRVSUE+&HdPDFiznUdMA!7i z;-*F0kKHppiD3F!wOHT3R(xiOV{MJ-)rz?)t4o)444dLJdcD3;tu+KjJTbXk^e>$q zaI#}kV_0Yr60BBk6cdoDIikhamS)5g%gz@sSsp}4@pH?!A%u9r=sNM0<$J^xQ_IBz zGWj$y3kf@;kz=71f5IoL)m#JW&oB!`qdR6^`ym;k5x?(VEuPx+g+;@cbMNlh{5=$y zemuHDeAm`35PN+ZPCj}bsQnIE3?E$z$)qaTCYw#yQ{d)bPOJdWyn(M5*XSl7_uMu${P(VH zTPLZ5IljWM&HdCY(JxsqzOrrg;N5z0>&zPXYwwJIZIkV3_>R8I*SR{$mpX!mBpU5H z%hiEefvq`qeCByn+`N6YxQSRVda*^~{!LrO4e|pJsIS2$f{F_e5h%2i>w~|;I5_U+Dx0| z+5%H9)w{KFy;NwkscKWBtFc}2m0c^vCuR+>OZD`c#XMJGvb#(Cp3Ul6-7G9q)-cHP zS+s#GzJqN9dIh#t+^8BK&~=e&?J`nv8rS1+pk%p>ctmxc_5374qn>Y7b zd^5#n_iToZizhDJaHbKEiTjCbcbwDBf78nlY2cAN$2NeIFH9^Fx%I2X_itUYRW;Y~ znVmo|4~W}8{0a(b6WCuFk@eYJeQdSnlgJQi%~VONA+^oAUR!EuI)!q!(aZ|@bR{>y zLs~mv%5RpZRQ4T>7cq_7}uX;vZtCUNO!+4a+ZXB zJwmOR-o0QZPn&UZ-;NC;FerANZ6k-~d`_0xR5Q0b&q9zLfM7PvYQ(P^*8BfkeV4dq zih=d|c6i?Uo`r!~LOs`Nm8(K9&9|u)Q;W8V|6y1;U2B}?55CodCC4Y0i!b9##hE=D zCNo@%ZMJIt7qc3$v3#m&!DE*IT(pe_qqfLbVg1JTbmje09&v zm4lZ&9Ilm0I@REJXCRun>Yxdz)3ci3YtRM_Na`EZvtBhDDVArN(CqClG`ImjT1cJl ztV(UB(QI)%2OEdq8wO2?(-+<@cGj;I|8vI{XsnlPf#ZpTV+qnZSEE1Uor!)p#B|V7 zKRCKx{M1xT>|V5Ao@G)Mt{|RrT`2yOWm^25i<+E=CgfpO4Q}?J+r%666Cm3}De=vV zCP2Q4>T!rE9;BM+g%GRbgQ8o=arpTKqGJ2xz)Hdi@Yn-;TgbpXt`~j#2=VnzE5I$O z(d8G;0cJP|=NO&C%K$D1iHfGEVGHeQwpq&Pn!PI1BD!soNaqFveQF*t0}%EBvr2q^ zdfnnP;i8{af%;NxtN7GDa%ur95Guo&6sWxDwyD!#>0{*<<9hMZJ=3Cb|L*w-ciGv% z8Q=z_H0XKYR1}qoABAh1+xF`riblliJWC;xmWn^zziH7tn)+Y@kNi8jbx?hB9!diw zoh}E$#Q0`Vn;4%Gk9xE#-iat^9>KH%J%7;3)b4pSsrz8+FAG+^6GQ_Hou+j0?$Z$J zBWTc^=r?`>QGt)XG_gv2lGv$Vu$X-q{Y4a=V?!0-<{5N3T$-(%TV1RY-6nouP{Ha& zqf5ny$Cpo^Rt&U5Lo>DJ^il{KtaFxelk+xf!DKByZL-c>*)3XjwzWEAMaP-XpPzZ( zxsG|z{JLEHgG)Py`kxFIFex~Rzi=mJgK8X|Q)y4UKBxg#e+OG7ip~|sR(L(=nzYc% zWOAicE8ps9G`d_>*T};a1EB-K+p*=xZuGkl*wDw{1y_~74@BVfe8K2iVEG_6y;wC% z9OP3xzaLJ~I=DvO?c63l8#F_si!C{35A8${-LZQ3mx%a_56nbr4U@y`*18f2w?eR# zTsC#xU~t0%k4KR!#KKy(>Z@lfUbf~_F~tpiCWWqpwv$JR1s4#rc_3q zHo?>=Q@j=2^{=BFz>)Jt7KvBBYeYn&Z;GG!#s2ixP~rI&37rC;JpZv8ku z3ExbMr>;?pzy9qsIP#^@NkntYfei>H?&sEvdk!eUH}4!kq?+}F&Jb_L2xUpBrW|C8)Thd6s#9;51d7Nj7=m_Z zm1RD`hnY-SX)=3)G~IM5>*Du6x*tsTskh<&Vs!0<=Ek?#)fKGE*^Pim3ZIbp@^1?AAQd>+=Jcq?eQIS zyy?uxsySam!)TjiQ)?lbDx$7aIofqcHyxp1gYLu|w~D1bv4T=3P(5=ph%u@m>a=r%INiWy_E`4cE;W2m@4B} zhq6Jw+)?Y|sdBzKFN1fg@7T%r5q+a`^Ap&1+Qd1E7FWX+uV)I1q(`lfw>1WbfmImm zE^RGiX#`F7q)#C@I?gWN@FX=#t64Cc=uAB8MWRZ5FIEF zjV)wv1+79U=k$7g7K4ZAWmJr{=vOxFZEdoyC|4E5jt>|=J+?u-=LVa2>rtrA4~!g} ze*eL>;0TUv7H|4gQ*3;3De%Xzb)Zfm`yyqF#Sw%CxZ=zube)RcV7Aun)}+dv(C7`h zB5!tD(-C`G%QFtU&SW+4g_JU)YN-6Q&`tV0-fGfH8m#Jany(v6g3TP$W_h4~K)wN- zSc7nL<9O?_BX@s&jrj1fHODT0JYU` z*`A^jiG;ptE19`sS?RPJoCz%vYPL#hTg#HmRr3Xcsdbz-PcM)&Qkt;S#8!%7C%8*H zvO%2w)fbNa*K>_&xNp>hEixHgYronCpWBIy9>ZUGa{?S4L)L?h3S?aT>K|8$n{VGC z{`rqji>DqniktsDaqPA?=&^yyYHv+Ti)F~wNdJQk(nE5jxD@V&1oqq^SXd`AgCiZg+x_;$-KJ6BX=|4VG_~z;UiAJpOOOWkRApKn-CocY=R6N z{q!R6$Gb+~4Nj~_#-uwpA(zO3>l359z>)39cwgq98j5=*P;E!Hh;KbNAuZpI6h^>5 z%4I90BeTelaYQ3kG|1Dac=X0a;6@E12S437Ixbc9$W6HTSbc`hg(weaD6*C?*{qlS zq>qm(s_wSA+SaAHRNNL$k}3!+VjH($6nM@~DXX<2vx_9!PxsV&qGMC*wZkt-eI& z<}vBA{m5^S{?rT(e`yqMVXhwqRTFZx_@hf^BK4xKo7bzghF-Bw@C9eQDwJ5hU1fbi zGrMJ3r zt~#^>+-rvON`Ikol@a--c=sol&QGoVSx|b@f?PYC&Wm<)D%f)-2*Tp3Sc(R-gY@}3 z1$Wt2GL=jkTnvi(a?7y z1U(QS7IA-ZM7;AoaAr-e=>ublc(Aipy4{Za3}?9&bD4QnceyZ-zI95?O1{(tveECwcbg! zqRpBoRf6-=8lYM?E(51r_!jBj!^mC){NqXaOjKu98Z8D5Y1Ef`+G^J1F}mY$vWS*D zZBI22>r~q6L@eKOWSl~h5Ng_(rl)o^OQmK_K^B^Rt=i0)yBVh|lj?Z#LdwXe!}_{W zV&03~j*iW5nYM%PeE``c?YtKG`30b1o|vIoqp4}rmwd^jqro*jA;xYpb&Gb==XTj$ zj)FHG*Ttx|-mE0qaJI|Dn%+=887GZir`2rnB22PoMWtf%LATpu zORLLmMctJ4N8NR6qg~;wPM3$%r7aOxBNcqr>i|vcy*ke%@DgC|V)jAml^QipF4w7lphQ?mP&}p^L-| z-gq*Qss|Nt;a&9#)l`l5#`G4ZU@I58tjCeeFd3~T5DWqJ(3!n4RKaO8W)PH^|Pkmq5+ zRnH+?q)&evxdXnHOy5QH^R*;ctGMtm{REPne*l|*j+mscoj|a&O2Ee7Lj%A4N#yPM zeg^ff`Zok$sMqgV`uY!$dlyQkpF!4vmw%0nNca2@aiVjdO0f8~g*tG_jLb)jU?@;|%iUWoahE0eC-#A zYN2F-0RS0*cKip(3aR`OLM~JoeDdb6kSi8S&iX_N6we@q{&&m3=2wvQQs=klkPMFe z2pIzxypGV)o>!2>+0DH4J4AoF0@(XUWL(OgLc(X(S`PxRAtT_x?-Avos?=}m%nCyd z^che}u)%v}^J=*FPe^c~B2?a_H1h_s1C~hsH<4c>;1i$1&Vv)_3Q)#SXeZxBrlcF* zLVPHq`|>mB6(~5&jw}bYw~$5P&p5gmT!W#X0pGt*wmlL{`uwz~qM*}7zF^X+TdicT zP+`h>r#noW{7SDOM)>pA5=&;Pb_Wj zP!vG4a7}vg&WTCztPI@`ZhRY=2J3Kisic#kD_2Q9@VDs204qjT1-YL_-BLEdh-GVKb zKDq(raPioGt_1fQ(J`Pn4}Ea{X|Xi96{Qf+=%8ysz>E@or&bhlhAx&)%%HOf_*VkG z76iKJG&pERwSX%^=^yc+E5P&~G%CT0(6Z0h$qov^(TmYV;2R3`O_06AYZ-W(LKWbN3t(3#?I;fR??bN!uUOIT;79;nA#qCdA!P8~oHrDx_o6mB!5R{@ zB3N-GJTW1vj_2J>owY}6u|%w__vTDdufC&9XSi;l-6@oUb}kpu=(H5kW;}sRGvbYL zUO^S$9EEy5%ymd-L1EC5z<3z7fa#0T$UI?baI%8x!Q(u-5)_+|-G^PM9DLA){u6k- z0~v>9da$^Q2G16T0_@kJ$3W4G3Q|dr%7zSpQ+afW^e-f;889wE#y^YB_tXG~y0HDD zMbrpBeg!%ueeog`Iw$4?lvwR_1$`} z7|PZynUF8$tP&wTTd4I+T-YD5b7mN!QJB zvo+^*IUB_S=?g2{0sBCpM;)jDp7f(v%?(Hox=8x>Lvv69@05`qK(Htm2I`^VXnHk+ zJ~)u7bUcTyl7S|TnrV+WX^oI=mUJY9M7YFf46aO$rJ}4hdI4GJ6nJX?wS z1= zI86)Uo*#U3)(HY{PN)z-;o*S4*n`^p0}QTfGw6F|O)kYZ!GN@Cmb2*_sbZnU7qhvx z${$x4Ql!hxIaHmbF=^rmcQdc#NzxD|t!z1&^JtV_8xwLyb)H%%pffj(;c7k`wrFe~ z*4nnKLyl1M90PTZ)}eV>-IuWre6Iry?pd~JsDA*h=`4VP_TP3boD)CSLPz@52In8q zTgZ{2Z4H@J`Wn|(__e-TSFOr1YO_AicTI*M=P&h0I?Ykxil&}*H$8>2$G|uZdIO=e zB$^IYy3^5n9r}ccu(f&~T5lqCbyi~x27|-3V;{xyVtt2g9RT)uFDjQVbfXxuF#Hq) ze1KCIpiACKHVOPeI6*-jOoN~HhEvSmKA%g$&dcE=v@IL^(~04>E1~-IDpUi0eg*oq z)8+SFiS9;DKf-ET`?hEjeCevW)})sYqxist@4b8Wy%3MD2T*EI?`Q;0SRcH4*v+{n z4AQ!MXcfGIvFOdl6|6q2b+SUp9cF7ZUsF^(<%C_&^nx~>zE#l|TH!cRHJLnqZ`5FG zbjXgYQnuy@d&%wTrF|_&F=YW9GI7bR7l6$lLU&4~JJA&0-)GCgm-k|;q?eAOA0ecjPoc;0HGeU_ z0o+WZGt%~F(J5rrd|<&jF%IB0Gn_T=e-0Wd_Q&WOGD&y=y+?)^z^hllFo!n}U@O5D z=V5E5_Ak*FFj)NBHLw_#Xu#X6Fu8Q^Z_p(pV&_L2q@TWyUW6kCY5gBiVggn?d_86Y zH(iXamp+eT&avUA>%j-lmrvdHGjs`f=xw;ZxNs46i~QVI;w#n>X~{C|aeT0HJo+1S zT9U2CZp7vv7fWARhizSWoRHq!i0L75lJ-38b{V1tH|McUFcK%ISRtW1F!?wv>K`S6 z``(Lf0Xt-}Nhw5N(1j1_28Yha#zE*pYzO$|?;#ZKeGg`mPF#q=^}8A%d$H@F*TsKe zt@ICjv0otI4Ktk2zih^qNT1q=9qD&Tnz0xH?zCV!@t4dD_=5#|U;kv{I&2$wmmJ#z zU{m|SMjOV#(m{O1XKk1jF^J#4WhXfCU33)udHv`Fn6+d3z{fv|On`UY1)rHa8?O8p~WV^^HYn~fTJZ0o>s`Q z5vdx%u+xsa7r~(|h;8Vk`^_gu-Qo>TP2Rd$c8>4P435tr%i!J-`b$@Wzznh(yu2MT z^<9@XaAgt`q42ieb_q5D{_8My0XTAM>>}}KMGii*byNXf{XHDOugqYpz=lgP7}No( zc}xK=yA=BlqLy}EhCPS^l^ekY~| z4;8SNprAip#3G|$>_TjCumCJf0N>B6HeaM_HPEDfg!JjXNB;PAyYk>Q6*dl4Vjj^ZU z)Q1qHAH4mQ38nPm%du(rDfq$x>^$l5E3ohO>&;{A-XU}daL3i~i(w@ID@}quT5JRO z;}w`f`pGqzh@NH#@^xOHiFrRG1dt5rZ$!XrzeiU{Q#W9f2-x|4>jr^)-2X2Bq($Obs*T^peaz>WA4aK#aPR670< z>_7V~L3HJ%4IjfEUl?HHVB!J&6I(#F3%#&|ZZ7>i_7miEuZerH>-ua#!EXY;-YA~}(_g~217xRcN2Fvi zxs8o@!mnHvIW6eZ`brev48@l@L{m#Tz@!m(*i+{R)5}f)fwiNv6IJN{=_M>L% zM`37QK-~5>?jG!a2+M$b@5khT`!a-SpJO^3>~-OJR6{6bY*_%cbjP@Az~pCA(YURkDJ8UFU9oHR!If1Bq_S|E z{P1zvC^&Q+)1T(K-!C*{1BS0UH52*Ucb*2mFJrsFx9)?cVW47%$xmn0aUo3W$vjQy zqq=ZDO?iWS1fEFJ-AuWYrEM4qUh9Zy0>!Q|U?%fXWi`u(?KNZDpUqOSTDx50 z8>)OPK>I<;2M_e$sba|P#GvbWp8HDv+Y)ewlJ9>QTZbz3x9ybeq|Is~r%(B`rnr%- zgc?r0N?i!BIaxIC8QXS_hWnkKjq1N~-vb420TYj4OXiu?`)HlcTPzI}(vdPTe;9+!a%yt}M7)=5Rh0s<<=o zh}*)F0ZO1~O|so8Wd$|d67j8Oq3BZAlCCIOvotmKCMEESaI{f(u+T9U1WO*p|J&i) z@AnKS(|?EF=QzIO5p3N6)Z^ev z-4-7ZG=i!`1)SQbDHk)wja5cT*hx+R>R-ZT#I#aACKf-lV4v47;oF7X8LnD`k(+Gu z^xX9D8#Cd*H5CKS*NrrCd=B;u&nV?P=|r zS6Bz2a@T_;Pob;&>wraLa~#9mfSYd_ECc=?Y~MJ<^+vG&5y*X?-?Iu9Se6f2?$6kB zb^BbOQFkR+3|ly(V87jlJ~j|9Um$QeizQ=?#LXm;W&I6rCe*UV1eMF$ zuDOaSMHk%s3Al$kT97RVe|Q+10?Nm*aSX<;g9{(UHY|rf4(^9|ZzwYG^lrbtpDqKY zrbjn{_dnX-b$z$LzWq7^LrlMl$wvEi;CMK#qrV%)Fmm_iTL!Y10q3LG;{P4t;J1&$ zy~seufl`L1N@g|(BRkke$lA^Dg?u5XO!q=+t`K&cbzW1BZV^J&%DJ4SUOZ05)mn?C z+@wpiAze0g34PaVVWJLJtM^qXBA|&`Ps><5dT0e`Y=bNL$FCk)+Fw)4Z~HnnIeo62 zhab*gi49wJlCB3Ed;+dbY@4#yZrG|9K3$#l1)^Al-OVR4q- zZKJ(Ngxc0#A>7W}^Hk8HHl-Y?3eh&5VZ$H(I@Uc?a?gJq8-dZpXUI}`3|j~HG2&y` z^#l0;%Qvtm2A`}4v1`WGNcwMLYZ2+dx3JxdfcH7L1N7gItOiq0!bGi)JqOp&r=EsR z`CmSTc}BsLf5c`$;-}b?@Sq2JLskrGNv`Ly^c39x8dr_&h*;Zvl`tt|&5XlsRX8)& zT+Uqg(aK8GW=}=C_H->%)fSXQx7mtl4W&lKVd&`UA-6~A&=~dIyjPnrro(BoDwB)o z6fnLj&U%2_y2v1&D2?BG&&XOa@FTdB9c1)?J72+`S@RcnNx*mwG6TN!Ds~Ed{Bzh^ z@X#r243fFTg|7n7pMpYAfqP!Vw6wab)uyA#meZ#S@nH?;Wb9d8nTl35&SX51WNX?e zA0U#-u+C$M8U2D*K0#k&J_*1=ozMU5d^;x$A6VB&R57=))D zdfx^KhNspi&Ipka1G`sBx4wn#K?bpvVERqCihlKN?7@XPr^|2s75?DD)0p%%1pngj zNV=~E=|&7cHhi3RkL&a2$Vh@Z#_ydw^Ti%+Yu`?Vc!%$Ro>bD0E72~n*p&HL>RU*1PMElev>rW3}HE|<}UN~F_IgpEa8 zz?!cGnw};dNvGMaGGdO9DT`nPySL!~1b)5+zYSb}9{ypV-iqJ02BsV+_cyz>9Irex z$3zJuxF^t^(X6ZJwbhE2n%-IYE|AZ=KaP!qdt{(EHlRjN5ILg_$|^^=i?ppwuhj~(CzqB;DHZ9PumOI@vlo?+JV1-jqe_W$}3@@ z{4iK49irh1O5qDBV?xscLr|5bdNE1GSx2ELI0O}psLOH1o=Mm7Y1~BJ9ZOhsg4&QH z?dF!h>`|NJ7K5EpJ8WGt0#Dnf9~N+*$t#63+9Q!6)#3>zw`UH-5Q#)}Dcd#xPKgyY_V zNiZw&Y&29*l6+ApWV0r_&S?fqmY|!!ksY{Jx^)-C^I&uwn8iO0KC8r^8axdK(d{bS zA(vj!CWGk|G$ur`4!=lhN)-`Eq)bCEGC?!8qN5aNF^6&Jj49yUV<{x9%=Y7AgYXKXca zIPlGVzcB;7u8aM6ybu0qxD!xci|?S7PPL|OtXJ)tY`5->hLeF3pKkerl>`h%Bod_G zTNaAuyeFREs}-jzlhU$f4=K2m+CnK#syr5LfO4>c+SL{4Xw)d=YIMj1Qs01qU;8ci z`2YqYE(e=^_?mM@+gnb236yg1kyS(4?him;_FmUtKt;sua=6NVu34;}%fxj2Q0|j4e!Z-h~#v$Zl^>7?Q=*#fF6s~?K)L)He=AJ;gG4>1 zA>q=qB4o=B3)?Iwz4~yl=envT+XgxHM3JaUimC7}L ztx!@}P0d80>j;(NscOxYH^XISGTF=bJS{pFO}5P~cRieSMa>jl%*L3SQy`;mUnDQ& zjRD3V>m|xYq2(+xMo>SD%uS&K6X}cMmx=eBKl$HHmG3l|P>SLp2OJ!~O1=tQn#3>Y z8_s>;1Pa~%pL+vd`T$pS^1cb31byZfrpv!#O8;;2Iy9_g5}$;I^?&5}woCB7*QIZ3 zBi*i56AB1^yCUu~c^btw-!U>-y3kF#qCsWRPML~~i&K>;SJ;@ymz8F|7|nF@Du)oz zx|(DxnbgCO?M{|c*Ywt|Pt)w#B4EouPn*P_+ssOUahj#hX-{a3n~tI}@_k1*N7Csd+1& zxHC&wg;8uAXzqEGQt_T88PWp?ntueSB29(-o{i@NwOBs8f(JPiC-&>UA%7l z{`p+o0THl!Na`E`spH7b<>$WcQwM71i5)b z3>>9lXj9PPWU4Ag)AZ)TZfjAI?i5|wh$Bepy$x+E-Z6CSbfm5~WNJDh8KB~WpmN|l zh;Ieh*rFK;9K<)H2)ygzpGH@M6DfQJc#i`&3=VYP*TkoQKZDC9GL4&2@SY~V4D7Ao zYosq_aVs)>p3cCn%M}%TOu8VCUvy?6cP0GtLj~M$X5oJn@$a7T;jb9{x-%XxTDfQn z++D)|<#d^JT^R?%zNP&v{_Qz3REO~qiKyc@&ow^vUf9{M8~6=~1j)3|m0l&_GU?S8 zz62jO*nbB1ybD&{)x%HDH3bVtCEXSMy?LKQ>C}~Y02|idd=0ExcrOkwWf;7I&l9L* zcprW#0zP>iE=rc`@teoM!4JW1Ub+Q;9R)Z24rT@Y>{k2)4j%b1{#)s*AHhwiH1RQ< z$D|M3g@1h!lq@b~K99c_k$!&<{vot~1&1$kAI~dEk$m7OjvB592V3L<6>b1Gh*kAH&CClB5J5V30RVSNbN- z_5VKiE&OAsblrFH5^~?;cpe4s{suZOaVPMnQ1IAl*)l2jZ}{uj7~IS5PQgs)8aVkC z`7WBQH7uoe)8MGqnWQRH(V0V~W|-3Q)^3q^1c(f0DW{U|kUN?;TQ~zx!n7fCFZyY@j&EjQ<}|!a0{Y64$FUjdrl%o$bJZ>dPdtmmC;@QjIUL5&{TTmk|HTKM z#m|>EJ%`KW67eGbTMX|D5vad~u9ED(z^_I6xvPeLM)Fdi`UT8*e)-j967>@PVMIFl zEBy8Tl}UGfk)mtyZlPPHdaTQ=j4@o#8#U@eN~)Y|=}P)?Bx#MhN-lvgflYy4Z{XSxHXVuf{k#kQ*B37y#Se0 zXWVQg;nOn=yn*LwbgQ`ecMkA*xoij2$>Afb`wsl6zRu(lHX>^v(tAf`8%L#krezIz zAHpj@aiwfpimZ}l5eYtrFllDJ>;wYuS=g*hv$G{$sk==j+X<+RvZQo$qwF~hn4`!H zZOOY7b-gu^vAdHNbudPk!ZjF`8YI(}V1y4kZQ5R*Ha3&`dRgI6YLvlHu>f77HI0Gi z70L+F7E}sHipiQ)xs0o%w1hm)lpAb$LB0jNx?B$L-q|YqJ`8bz_mymh<7~tEvI$Jm zZ2XdYXU3rdV!wlc~b{OyH$e z3&mWz!n7H#1slu3%UlMp0Oymf8*8x@E(NcYAVB>%bYzE=vJXhVRm#4I-u=&n43X~F z$l$-<)5=~%qzCn~YY-5iWDiQ~46@CG;k|c{>^zjzNvS=uO_+4!UfEL^s2gQ3N-2}9 zgh_Ym^nI`F2D!vUWSiv@NXovAL&zAfo}pEGq1VeA%jQ7RuLxGP zIc3gQC?-voeAH**%>_ZDi4de;FgXk@)@lfANy?uPl*%CCA$_&HvS;e?WTTYy^F606 zSQdhAqqQ#mg)9p{MJbsNKS3{eD|b*RA_% zbfsjZJbE8V6|(+7F`(meHdi2J53y!rAXM!d1--7$Dck}PgMOB}p~ZyM*&ONURWj{} zk0RCfGOq-|r^Ymrwku1>q>sE?_McNV zN+p%3>9Y-MrRIqN%L&%a-Ghcx*CnOPoT?1z~2#_h5-Q%eVuI^#k) zB{@8|@#>BFMk{B5J6|iCQbr2~6HF*GXu7R-uN-z)xd_u?1*@y?OkssUaA`OMq7Wov=R8UV^iSuI9$pcy;8mn)V_@_5ntJ~ zrhmc3Dll_5JX13KU~GD$29nnQkFTW!wtrY%9o$_7N5Cwx8_r4C#H#f$Qf%RVpJpSM z+g)ZV@K&MzO)UhN$c}H9X6}~hP>K1B?9kkFbZGO)D)9P0%dU~G`J4>8`agP)Y&|${ z4>BXIx<^K$Ab20LBckvJv!wt`&y6xprJf;Kr>kb98pev2k1Ac+K%O?RNnR7J`vtqR z!1SEvcFRXJTKc>|9#u2dP*Zfm70)M?kyx`@q=EW!c*tPO4#GN%4WysoiD$s@t0*9ewin@p-Wv8`_U{}@yq_>cu>Ow=I=F4dzq_5{Pu~^VU z_e>NF;@$!$=iI}x2Qf+YHQ5S83V&UuL8K2oCbNu!&pjbqBfb5&>=k&=8aR1)>;mzm zQU-s1|3%p<@YNS(OTagODO)xy*a_f}ocp~Bs9ut-ov(QE=dyM1H(V)f2i2d+rUCaa z6N{vMzmN^?Z5iI;_Wz^F-ahDn{p>XmQqwQUaPaD{WFyj(FUhVNkq(`bIZ#P@UDlKj zS0Z)i$TBJLC)tJxIQfp^5?mhVQ1GdnVUjFQ5lqwJXA=#d%9*AbiXK^kxzQRvYgK4% z0ZrBrB%>ZbNh{o;h{7CF+4)$j>*zoWm!JqInTfYmc4bp&)VXTJBuB9n6J6iFeh%VBr0DFC*XxHGxadu8@BT1$VBNpOkJ`Bj4K3k-X{Y(G`eZ`r11AK2%z= zQT{>MSTR?r3D`+`WwoFdI`O*GQ*627Xv?CfeYX*cOiRthGoGLZ1HI$fg?&*dAs zPTUeud9uxXfK69y7A~6281sC^MRE;2Q#aCVM`bqKov~nFG@HPoA0jIt1-0EUZaCr{ zUnYHctNbtucFxHERr=G6{4E)f?UvsreP*|O8ww6JWw>-wDgPZVy`YsZLZmlz@)in_ zW!#3(L{qL@FsN`;46bgs*Vbl=rh399RHLB;Ji~_f7n-bbow?O5D|H2%nNI3#8isVR z>6D&lllf+>k|OPeijdL4yHqk+g_X?2;58xz>19fuL?n8T+=57VUnp;3(gpkE-;+r) ztNeNtL~fJKM2y`An{r2M-d?5T%nGGmqonY3?9~#*lnGC!EW}e8N4?>4DNCK6K5Ma< zGD*@&vK5C{WouhwlsBJ_xdeY!(Tx!CULvM7*uc$?L*&$+AJ*rAbviNq2|z_%)TPr>SCgQRM4kmnKUXdqHEqWKCE?UQLqVyg68GXxTd5lCs(g8_k|9(;>MC z-wd70=cyO5^T8AAN8l`UDRj|aOUsuHZvpVCn>vNNq;(cx`hT^><_VJtjzLd3M%NYb zN@#(TFa0&TN<5GUR$Qw7^^eDO_|0?kXEMUFuAkpM%b_Acn?TDJG=#e z>-Vue;x*a4a8OZP#z3-lOO{K*!$VV?EaYn(ymC`-%2Z4Ggw5M%n!V8iM=Lvo#i_25 zT1z!iA?9)H&snCiygmA)UZ2QWspvC}$_o7e@E71G@Hm@chjK6X%_5S8pmt6cIY@Ru;3St~Us`D?we!X^0xD0e3nYXsL(X1ul`x z!3VN%3a$JS+q$5>sgbkrQ_52Dd@Ah9wKCqCMXyQtyneOI+<*tonX)66@wWATc%f<9 zqpj$jcV*Tj4Xr!g$(s8cc%9LG}j8SEQcg>uOpLZXSTSilj>KVAz z`zUCOJszc}%XbZ3t+hkNIZx17NhjTO*V-}1x~Z<6A>4(CDOYjV0)>)3lw|3YyTPck z4qw1t2<2Q2#vP^g3b(}`6j-(D%zm##rp{p;9AxA&@VQImBSY}oO@Zm^14L~tm>jDzmjx~@bxpsCf%5M64n5@f7m>_jf%nCAQB;eI}p_h)udWk z&{D-_(@OP}y0DpvJG;3wW77GkXoRfAHBEgr=OiP+s6$W+@SdE2(rD9$%e6NKG46-og z3=CCOwk&0vqf6+>V&n|_awH`W413Bqy16&R{nBP5e59BCBpktf(`^lXiF1PH9wbx*GTUV>T<1 zaJ#fkN2m-}gV~nfZfTg>4pW0k@OF4HPFur*Bgut-5@VRfcQ6LR`Vch!Av^2q_PhQ001cU%gne z=wVb0QByT6Eo+akxs&x;-NX7Ri#Zi*Royu%%bS&zQac(Bci5qVPJI++0s6qiYOwek z7!&hELB0_#8(%5Nx5Gaxi}Ed_^Ue}b56f4K^&Kc`-7GYo?=H$$PUy8~6>MF&t2hVG zp5YdJcOGsX(|i`LLQYrwT2X#JZ2eb7`6l>hj2Zmd$;dauCl@0>5B{k#@@?>s$P8+J zW%%bOgLk);1{JrIC85-(orbtiCd^8f@sA;h(!p{r05qmgJWrlD;DUBi=uU z+X-TI`B%Yf(~DOAzvj+6UaIQq`=6P6@0@lKkRnaE2mxh)>Fs%fa;Dcay?!iA@4b&= zi$;wVN!SK85ffuHK_Ql%#BQuXBdCeN3i>80(I`nQ(ZuInXXYZ9=gs@y^M2lcm|@>D zr|z@&+H3vRccF*+Fy2cVq7|b*oZS$8a?BsDZi-$TM()tJTcQ`|9$80^!g~KP1wykS znHyc-gmL`Vwx}?9EVX+S^HY`dfmk^U$q_!M6Ox3wHVq)4ETMoi*se$7l@`QXJ0*Wq zp#zebKO}W�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 Date: Tue, 18 Oct 2022 22:04:19 +0800 Subject: [PATCH 5/5] use React.PropsWithChildren --- apps/landing/src/App.tsx | 7 +++---- apps/landing/src/components/Footer.tsx | 3 ++- apps/landing/src/components/Markdown.tsx | 5 ++--- apps/landing/src/components/NavBar.tsx | 4 ++-- apps/landing/src/components/TeamMember.tsx | 7 ++----- apps/landing/src/renderer/usePageContext.tsx | 7 +++---- .../mobile/src/components/animation/layout.tsx | 8 ++++---- .../src/components/layout/CollapsibleView.tsx | 7 +++---- .../components/dialog/DeleteLibraryDialog.tsx | 13 ++++++------- .../explorer/ExplorerContextMenu.tsx | 7 ++----- .../explorer/ExplorerOptionsPanel.tsx | 18 +++++++++--------- .../src/components/key/KeyManager.tsx | 16 +++++++--------- .../interface/src/components/layout/Card.tsx | 4 ++-- .../interface/src/components/layout/Modal.tsx | 14 +++++++------- .../src/components/layout/Sidebar.tsx | 9 +++++---- .../components/primitive/InputContainer.tsx | 6 +++--- .../interface/src/components/primitive/Tag.tsx | 5 ++--- .../components/settings/SettingsContainer.tsx | 4 +++- .../src/components/settings/SettingsHeader.tsx | 16 +++++++++------- .../src/components/tooltip/Tooltip.tsx | 9 +++++++-- .../src/components/transitions/SlideUp.tsx | 3 ++- packages/ui/src/Input.tsx | 14 ++++++++------ packages/ui/src/Select.tsx | 12 +++--------- 23 files changed, 96 insertions(+), 102 deletions(-) diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index cec688781..dcfe8d8db 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import { PageContextBuiltIn } from 'vite-plugin-ssr'; import { Footer } from './components/Footer'; @@ -11,10 +11,9 @@ import '@sd/ui/style'; export default function App({ children, pageContext -}: { - children: React.ReactNode; +}: PropsWithChildren<{ pageContext: PageContextBuiltIn; -}) { +}>) { return ( diff --git a/apps/landing/src/components/Footer.tsx b/apps/landing/src/components/Footer.tsx index 8b400dc83..456c2720e 100644 --- a/apps/landing/src/components/Footer.tsx +++ b/apps/landing/src/components/Footer.tsx @@ -7,8 +7,9 @@ import { Twitter } from '@icons-pack/react-simple-icons'; import AppLogo from '@sd/assets/images/logo.png'; +import { PropsWithChildren } from 'react'; -function FooterLink(props: { children: string | JSX.Element; link: string; blank?: boolean }) { +function FooterLink(props: PropsWithChildren<{ link: string; blank?: boolean }>) { return ( ) { useEffect(() => { Prism.highlightAll(); }, []); diff --git a/apps/landing/src/components/NavBar.tsx b/apps/landing/src/components/NavBar.tsx index ad9c7101b..d3d4fec2b 100644 --- a/apps/landing/src/components/NavBar.tsx +++ b/apps/landing/src/components/NavBar.tsx @@ -10,12 +10,12 @@ import AppLogo from '@sd/assets/images/logo.png'; import { Dropdown, DropdownItem } from '@sd/ui'; import clsx from 'clsx'; import { DotsThreeVertical, List } from 'phosphor-react'; -import { useEffect, useState } from 'react'; +import { PropsWithChildren, useEffect, useState } from 'react'; import { positions } from '../pages/careers.page'; import { getWindow } from '../utils'; -function NavLink(props: { link?: string; children: string }) { +function NavLink(props: PropsWithChildren<{ link?: string }>) { return ( ) { return ( (undefined as any); function PageContextProvider({ pageContext, children -}: { +}: PropsWithChildren<{ pageContext: PageContextBuiltIn; - children: ReactNode; -}) { +}>) { return {children}; } diff --git a/apps/mobile/src/components/animation/layout.tsx b/apps/mobile/src/components/animation/layout.tsx index 727838f43..bbf735643 100644 --- a/apps/mobile/src/components/animation/layout.tsx +++ b/apps/mobile/src/components/animation/layout.tsx @@ -1,18 +1,18 @@ import { MotiView, useDynamicAnimation } from 'moti'; -import { ReactNode } from 'react'; +import { PropsWithChildren, ReactNode } from 'react'; import { StyleSheet, View } from 'react-native'; import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; import Layout from '~/constants/Layout'; import tw from '~/lib/tailwind'; // Anything wrapped with FadeIn will fade in on mount. -export const FadeInAnimation = ({ children, delay }: { children: any; delay?: number }) => ( +export const FadeInAnimation = ({ children, delay }: PropsWithChildren<{ delay?: number }>) => ( {children} ); -export const FadeInUpAnimation = ({ children, delay }: { children: any; delay?: number }) => ( +export const FadeInUpAnimation = ({ children, delay }: PropsWithChildren<{ delay?: number }>) => ( ); -export const LogoAnimation = ({ children }: { children: any }) => ( +export const LogoAnimation = ({ children }: PropsWithChildren) => ( ; - children: ReactNode; containerStyle?: StyleProp; -}; +}>; const CollapsibleView = ({ title, titleStyle, containerStyle, children }: CollapsibleViewProps) => { const [hide, toggle] = useReducer((hide) => !hide, false); diff --git a/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx b/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx index f84e7ad14..04718ac8b 100644 --- a/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx +++ b/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx @@ -1,14 +1,13 @@ import { useBridgeMutation } from '@sd/client'; import { Dialog } from '@sd/ui'; import { useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; +import { PropsWithChildren, useState } from 'react'; -interface Props { - children: React.ReactNode; - libraryUuid: string; -} - -export default function DeleteLibraryDialog(props: Props) { +export default function DeleteLibraryDialog( + props: PropsWithChildren<{ + libraryUuid: string; + }> +) { const [openDeleteModal, setOpenDeleteModal] = useState(false); const queryClient = useQueryClient(); diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index e39fe4874..5d5c8f260 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -17,6 +17,7 @@ import { Trash, TrashSimple } from 'phosphor-react'; +import { PropsWithChildren } from 'react'; import { useSnapshot } from 'valtio'; const AssignTagMenuItems = (props: { objectId: number }) => { @@ -59,11 +60,7 @@ const AssignTagMenuItems = (props: { objectId: number }) => { ); }; -interface Props { - children: React.ReactNode; -} - -export default function ExplorerContextMenu(props: Props) { +export default function ExplorerContextMenu(props: PropsWithChildren) { const store = getExplorerStore(); return ( diff --git a/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx b/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx index 8ac7e08da..087354983 100644 --- a/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx +++ b/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx @@ -1,17 +1,17 @@ import { Select, SelectOption } from '@sd/ui'; -import { useState } from 'react'; +import { PropsWithChildren, useState } from 'react'; import Slider from '../primitive/Slider'; -const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
    {children}
    -); +function Heading({ children }: PropsWithChildren) { + return
    {children}
    ; +} -const SubHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
    {children}
    -); +function SubHeading({ children }: PropsWithChildren) { + return
    {children}
    ; +} -export const ExplorerOptionsPanel: React.FC = () => { +export function ExplorerOptionsPanel() { const [sortBy, setSortBy] = useState('name'); const [stackBy, setStackBy] = useState('kind'); const [size, setSize] = useState([50]); @@ -44,4 +44,4 @@ export const ExplorerOptionsPanel: React.FC = () => {
  • ); -}; +} diff --git a/packages/interface/src/components/key/KeyManager.tsx b/packages/interface/src/components/key/KeyManager.tsx index c4cd6212a..6dd51d02b 100644 --- a/packages/interface/src/components/key/KeyManager.tsx +++ b/packages/interface/src/components/key/KeyManager.tsx @@ -5,14 +5,12 @@ import { KeyIcon, LockClosedIcon, LockOpenIcon, - PlusIcon, - TrashIcon, XMarkIcon } from '@heroicons/react/24/solid'; import { Button, Input } from '@sd/ui'; import clsx from 'clsx'; -import { Eject, EjectSimple, Plus } from 'phosphor-react'; -import { useState } from 'react'; +import { Plus } from 'phosphor-react'; +import { PropsWithChildren, useState } from 'react'; import { Toggle } from '../primitive'; import { DefaultProps } from '../primitive/types'; @@ -33,11 +31,11 @@ interface FakeKey { nodes?: string[]; // will be node object } -const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
    {children}
    -); +function Heading({ children }: PropsWithChildren) { + return
    {children}
    ; +} -const Key: React.FC<{ data: FakeKey; index: number }> = ({ data, index }) => { +function Key({ data, index }: { data: FakeKey; index: number }) { const odd = (index || 0) % 2 === 0; return ( @@ -117,7 +115,7 @@ const Key: React.FC<{ data: FakeKey; index: number }> = ({ data, index }) => { ); -}; +} export function KeyManager(props: KeyManagerProps) { const [showKey, setShowKey] = useState(false); diff --git a/packages/interface/src/components/layout/Card.tsx b/packages/interface/src/components/layout/Card.tsx index 66112e180..72483d627 100644 --- a/packages/interface/src/components/layout/Card.tsx +++ b/packages/interface/src/components/layout/Card.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; -import { ReactNode } from 'react'; +import { PropsWithChildren } from 'react'; -export default function Card(props: { children: ReactNode; className?: string }) { +export default function Card(props: PropsWithChildren<{ className?: string }>) { return (
    = (props) => { +export function Model( + props: PropsWithChildren<{ + full?: boolean; + }> +) { return (
    = (props) => {
    ); -}; +} diff --git a/packages/interface/src/components/layout/Sidebar.tsx b/packages/interface/src/components/layout/Sidebar.tsx index 8102055ab..636db47c7 100644 --- a/packages/interface/src/components/layout/Sidebar.tsx +++ b/packages/interface/src/components/layout/Sidebar.tsx @@ -5,6 +5,7 @@ import { LocationCreateArgs } from '@sd/client'; import { Button, Dropdown, OverlayPanel } from '@sd/ui'; import clsx from 'clsx'; import { CheckCircle, CirclesFour, Planet, WaveTriangle } from 'phosphor-react'; +import { PropsWithChildren } from 'react'; import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom'; import { useOperatingSystem } from '../../hooks/useOperatingSystem'; @@ -14,7 +15,7 @@ import { JobsManager } from '../jobs/JobManager'; import RunningJobsWidget from '../jobs/RunningJobsWidget'; import { MacTrafficLights } from '../os/TrafficLights'; -export const SidebarLink = (props: NavLinkProps & { children: React.ReactNode }) => ( +export const SidebarLink = (props: PropsWithChildren) => ( {({ isActive }) => ( ( ); -const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
    {children}
    -); +function Heading({ children }: PropsWithChildren) { + return
    {children}
    ; +} // cute little helper to decrease code clutter const macOnly = (platform: string | undefined, classnames: string) => diff --git a/packages/interface/src/components/primitive/InputContainer.tsx b/packages/interface/src/components/primitive/InputContainer.tsx index 60f01d3ec..48531c588 100644 --- a/packages/interface/src/components/primitive/InputContainer.tsx +++ b/packages/interface/src/components/primitive/InputContainer.tsx @@ -1,15 +1,15 @@ import clsx from 'clsx'; +import { PropsWithChildren } from 'react'; import { DefaultProps } from './types'; interface InputContainerProps extends DefaultProps { title: string; description?: string; - children: React.ReactNode; mini?: boolean; } -export const InputContainer: React.FC = (props) => { +export function InputContainer(props: PropsWithChildren) { return (
    = (props) => { {props.mini && props.children}
    ); -}; +} diff --git a/packages/interface/src/components/primitive/Tag.tsx b/packages/interface/src/components/primitive/Tag.tsx index 96f76c067..6e5c1d674 100644 --- a/packages/interface/src/components/primitive/Tag.tsx +++ b/packages/interface/src/components/primitive/Tag.tsx @@ -1,14 +1,13 @@ import clsx from 'clsx'; -import { ReactNode } from 'react'; +import { PropsWithChildren, ReactNode } from 'react'; import { DefaultProps } from './types'; export interface TagProps extends DefaultProps { - children: ReactNode; color: 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple' | 'pink'; } -export function Tag(props: TagProps) { +export function Tag(props: PropsWithChildren) { return (
    ( +import { PropsWithChildren } from 'react'; + +export const SettingsContainer = ({ children }: PropsWithChildren) => (
    {children}
    ); diff --git a/packages/interface/src/components/settings/SettingsHeader.tsx b/packages/interface/src/components/settings/SettingsHeader.tsx index b103493d8..dd1a22a73 100644 --- a/packages/interface/src/components/settings/SettingsHeader.tsx +++ b/packages/interface/src/components/settings/SettingsHeader.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { ReactNode } from 'react'; +import { PropsWithChildren, ReactNode } from 'react'; interface SettingsHeaderProps { title: string; @@ -24,11 +24,13 @@ export const SettingsIcon = ({ component: Icon, ...props }: any) => ( ); -export const SettingsHeading: React.FC<{ className?: string; children: string }> = ({ +export function SettingsHeading({ children, className -}) => ( -
    - {children} -
    -); +}: PropsWithChildren<{ className?: string }>) { + return ( +
    + {children} +
    + ); +} diff --git a/packages/interface/src/components/tooltip/Tooltip.tsx b/packages/interface/src/components/tooltip/Tooltip.tsx index 13538063c..eb2f4aa17 100644 --- a/packages/interface/src/components/tooltip/Tooltip.tsx +++ b/packages/interface/src/components/tooltip/Tooltip.tsx @@ -1,13 +1,18 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import { PropsWithChildren } from 'react'; export interface TooltipProps { - children: React.ReactNode; label: string; position?: 'top' | 'right' | 'bottom' | 'left'; className?: string; } -export const Tooltip = ({ children, label, position = 'bottom', className }: TooltipProps) => { +export const Tooltip = ({ + children, + label, + position = 'bottom', + className +}: PropsWithChildren) => { return ( diff --git a/packages/interface/src/components/transitions/SlideUp.tsx b/packages/interface/src/components/transitions/SlideUp.tsx index 9a16e5183..504cc922e 100644 --- a/packages/interface/src/components/transitions/SlideUp.tsx +++ b/packages/interface/src/components/transitions/SlideUp.tsx @@ -1,6 +1,7 @@ import { Transition } from '@headlessui/react'; +import { PropsWithChildren } from 'react'; -export default function SlideUp(props: { children: React.ReactNode }) { +export default function SlideUp(props: PropsWithChildren) { return ( { ); }; -export const Label: React.FC<{ slug?: string; children: string }> = (props) => ( - -); +export function Label(props: PropsWithChildren<{ slug?: string }>) { + return ( + + ); +} diff --git a/packages/ui/src/Select.tsx b/packages/ui/src/Select.tsx index d2b3ea82b..051105353 100644 --- a/packages/ui/src/Select.tsx +++ b/packages/ui/src/Select.tsx @@ -2,17 +2,16 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/s import * as SelectPrimitive from '@radix-ui/react-select'; import { ReactComponent as ChevronDouble } from '@sd/assets/svgs/chevron-double.svg'; import clsx from 'clsx'; -import { CaretDown } from 'phosphor-react'; +import { PropsWithChildren } from 'react'; interface SelectProps { value: string; size?: 'sm' | 'md' | 'lg'; className?: string; onChange: (value: string) => void; - children: React.ReactNode; } -export function Select(props: SelectProps) { +export function Select(props: PropsWithChildren) { return ( ) { return (