Merge pull request #419 from spacedriveapp/fix-reactivity

upgrade rspc + fix reactivity + add in disabled Normi code
This commit is contained in:
Oscar Beaumont
2022-10-18 23:10:37 +08:00
committed by GitHub
106 changed files with 1144 additions and 659 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

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

View File

@@ -11,12 +11,11 @@
"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",
"@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"
}
}

View File

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

View File

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

View File

@@ -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<Procedures>({
transport: new TauriTransport()
const isDev = import.meta.env.DEV;
const client = hooks.createClient({
links: [...(isDev ? [loggerLink()] : []), tauriLink()]
});
async function getOs(): Promise<OperatingSystem> {
@@ -52,11 +53,11 @@ function App() {
}, []);
return (
<rspc.Provider client={client} queryClient={queryClient}>
<hooks.Provider client={client} queryClient={queryClient}>
<PlatformProvider platform={platform}>
<SpacedriveInterface />
</PlatformProvider>
</rspc.Provider>
</hooks.Provider>
);
}

View File

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

View File

@@ -1,5 +1,4 @@
import { Button } from '@sd/ui';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { PageContextBuiltIn } from 'vite-plugin-ssr';
import { Footer } from './components/Footer';
@@ -12,10 +11,9 @@ import '@sd/ui/style';
export default function App({
children,
pageContext
}: {
children: React.ReactNode;
}: PropsWithChildren<{
pageContext: PageContextBuiltIn;
}) {
}>) {
return (
<React.StrictMode>
<PageContextProvider pageContext={pageContext}>

View File

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

View File

@@ -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 (
<a
href={props.link}

View File

@@ -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';

View File

@@ -2,17 +2,16 @@ import clsx from 'clsx';
import Prism from 'prismjs';
import 'prismjs/components/prism-rust';
import 'prismjs/components/prism-typescript';
import { useEffect } from 'react';
import { PropsWithChildren, useEffect } from 'react';
import '../atom-one.css';
interface MarkdownPageProps {
children: React.ReactNode;
classNames?: string;
articleClassNames?: string;
}
function MarkdownPage(props: MarkdownPageProps) {
function MarkdownPage(props: PropsWithChildren<MarkdownPageProps>) {
useEffect(() => {
Prism.highlightAll();
}, []);

View File

@@ -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 (
<a
href={props.link ?? '#'}

View File

@@ -1,5 +1,6 @@
import { Github, Twitch, Twitter } from '@icons-pack/react-simple-icons';
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
export interface TeamMemberProps {
// Name of team member
@@ -23,14 +24,10 @@ export interface TeamMemberProps {
}
interface LinkProps {
// Elements inside anchor tag
children: React.ReactNode;
// Anchor href
href: string;
}
function Link(props: LinkProps) {
function Link(props: PropsWithChildren<LinkProps>) {
return (
<a
className="duration-300 hover:scale-105 hover:opacity-80"

View File

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

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,6 +1,6 @@
// `usePageContext` allows us to access `pageContext` in any React component.
// More infos: https://vite-plugin-ssr.com/pageContext-anywhere
import { ReactNode, createContext, useContext } from 'react';
import { PropsWithChildren, ReactNode, createContext, useContext } from 'react';
import { PageContextBuiltIn } from 'vite-plugin-ssr';
import type { PageContext } from './types';
@@ -13,10 +13,9 @@ const Context = createContext<PageContextBuiltIn>(undefined as any);
function PageContextProvider({
pageContext,
children
}: {
}: PropsWithChildren<{
pageContext: PageContextBuiltIn;
children: ReactNode;
}) {
}>) {
return <Context.Provider value={pageContext}>{children}</Context.Provider>;
}

View File

@@ -8,9 +8,11 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
cargo {
module = "../../rust"
libname = "sd_core_mobile"
// profile = 'release',
pythonCommand = 'python3'
profile = 'release'
targets = ["arm", "arm64", "x86", "x86_64"]
// profile = 'debug'
// targets = ["arm64"]
targetDirectory = "../.././../../target" // Monorepo moment
}

View File

@@ -8,6 +8,7 @@ buildscript {
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '31')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '31')
reactNativeVersion = "0.69.4" // https://github.com/expo/expo/issues/18129
if (findProperty('android.kotlinVersion')) {
kotlinVersion = findProperty('android.kotlinVersion')
}

View File

@@ -2,6 +2,7 @@
"expo": {
"name": "Spacedrive",
"slug": "spacedrive",
"owner": "spacedrive",
"version": "0.0.1",
"orientation": "portrait",
"jsEngine": "hermes",
@@ -19,6 +20,11 @@
"android": {
"package": "com.spacedrive.app"
},
"privacy": "hidden"
"privacy": "hidden",
"extra": {
"eas": {
"projectId": "0cbf4456-87fb-499c-8dfa-554bfa5129f3"
}
}
}
}

23
apps/mobile/eas.json Normal file
View File

@@ -0,0 +1,23 @@
{
"cli": {
"version": ">= 0.56.0"
},
"build": {
"development": {
"distribution": "internal",
"android": {
"gradleCommand": ":app:assembleDebug"
},
"ios": {
"buildConfiguration": "Debug"
}
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}

View File

@@ -161,7 +161,7 @@
dependencies = (
);
name = Spacedrive;
productName = mobilenew;
productName = Spacedrive;
productReference = 13B07F961A680F5B00A75B9A /* Spacedrive.app */;
productType = "com.apple.product-type.application";
};

View File

@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobilenew.app"
BlueprintName = "mobilenew"
ReferencedContainer = "container:mobilenew.xcodeproj">
BuildableName = "Spacedrive.app"
BlueprintName = "Spacedrive"
ReferencedContainer = "container:Spacedrive.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -35,7 +35,7 @@
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "mobilenewTests.xctest"
BlueprintName = "mobilenewTests"
ReferencedContainer = "container:mobilenew.xcodeproj">
ReferencedContainer = "container:Spacedrive.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
@@ -64,9 +64,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobilenew.app"
BlueprintName = "mobilenew"
ReferencedContainer = "container:mobilenew.xcodeproj">
BuildableName = "Spacedrive.app"
BlueprintName = "Spacedrive"
ReferencedContainer = "container:Spacedrive.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
@@ -81,9 +81,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobilenew.app"
BlueprintName = "mobilenew"
ReferencedContainer = "container:mobilenew.xcodeproj">
BuildableName = "Spacedrive.app"
BlueprintName = "Spacedrive"
ReferencedContainer = "container:Spacedrive.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>

View File

@@ -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"
},
@@ -19,11 +21,11 @@
"@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": "workspace:*",
"@sd/client": "workspace:*",
"@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",
@@ -31,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",
@@ -67,6 +68,5 @@
"metro-minify-terser": "^0.72.1",
"react-native-svg-transformer": "^1.0.0",
"typescript": "^4.7.4"
},
"private": true
}
}

View File

@@ -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"]

View File

@@ -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::<Value>(&query).and_then(|v| match v.is_array() {
true => serde_json::from_value::<Vec<Request>>(v),
false => serde_json::from_value::<Request>(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::<Vec<_>>(),
)
.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
);

View File

@@ -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::<Value>(&query).and_then(|v| match v.is_array() {
true => serde_json::from_value::<Vec<Request>>(v),
false => serde_json::from_value::<Request>(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<Request>)).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::<Vec<_>>())
.unwrap(),
)
.unwrap(),
);
});
});

View File

@@ -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();

View File

@@ -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<Procedures>({
transport: new ReactNativeTransport()
links: [reactNativeLink()]
});
const NavigatorTheme: Theme = {

View File

@@ -1,18 +1,18 @@
import { MotiView, useDynamicAnimation } from 'moti';
import React 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 }>) => (
<MotiView from={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ type: 'timing', delay }}>
{children}
</MotiView>
);
export const FadeInUpAnimation = ({ children, delay }: { children: any; delay?: number }) => (
export const FadeInUpAnimation = ({ children, delay }: PropsWithChildren<{ delay?: number }>) => (
<MotiView
from={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
@@ -22,7 +22,7 @@ export const FadeInUpAnimation = ({ children, delay }: { children: any; delay?:
</MotiView>
);
export const LogoAnimation = ({ children }: { children: any }) => (
export const LogoAnimation = ({ children }: PropsWithChildren) => (
<MotiView
from={{ opacity: 0.8, translateY: Layout.window.width / 2 }}
animate={{ opacity: 1, translateY: 0 }}
@@ -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`.
*/

View File

@@ -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<BrowseLocationItemProps> = (props) => {
const BrowseLocationItem: FC<BrowseLocationItemProps> = (props) => {
const { folderName, onPress } = props;
return (

View File

@@ -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<BrowseTagItemProps> = (props) => {
const BrowseTagItem: FC<BrowseTagItemProps> = (props) => {
const { tagName, tagColor, onPress } = props;
return (
<Pressable onPress={onPress}>

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<DrawerLocationItemProps> = (props) => {
const DrawerLocationItem: FC<DrawerLocationItemProps> = (props) => {
const { folderName, onPress } = props;
return (
<Pressable onPress={onPress}>

View File

@@ -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<DrawerTagItemProps> = (props) => {
const DrawerTagItem: FC<DrawerTagItemProps> = (props) => {
const { tagName, tagColor, onPress } = props;
return (
<Pressable onPress={onPress}>

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Text, View } from 'react-native';
import Svg, { Path } from 'react-native-svg';

View File

@@ -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';

View File

@@ -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 = {

View File

@@ -1,17 +1,16 @@
import { MotiView } from 'moti';
import React, { useReducer } from 'react';
import { PropsWithChildren, 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';
import { AnimatedHeight } from '../animation/layout';
type CollapsibleViewProps = {
type CollapsibleViewProps = PropsWithChildren<{
title: string;
titleStyle?: StyleProp<TextStyle>;
children: React.ReactNode;
containerStyle?: StyleProp<ViewStyle>;
};
}>;
const CollapsibleView = ({ title, titleStyle, containerStyle, children }: CollapsibleViewProps) => {
const [hide, toggle] = useReducer((hide) => !hide, false);

View File

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

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { FlatList } from 'react-native';
export default function VirtualizedListWrapper({ children }) {

View File

@@ -1,5 +1,4 @@
import { BottomSheetBackdrop, BottomSheetBackdropProps } from '@gorhom/bottom-sheet';
import React from 'react';
const ModalBackdrop = (props: BottomSheetBackdropProps) => {
return (

View File

@@ -1,5 +1,4 @@
import { BottomSheetHandle, BottomSheetHandleProps } from '@gorhom/bottom-sheet';
import React from 'react';
import tw from '../../../lib/tailwind';

View File

@@ -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<typeof button> & PressableProps;
export const Button: React.FC<ButtonProps> = ({ variant, size, ...props }) => {
export const Button: FC<ButtonProps> = ({ variant, size, ...props }) => {
const { style, ...otherProps } = props;
return <Pressable style={tw.style(button({ variant, size }), style as string)} {...otherProps} />;
};
type AnimatedButtonProps = VariantProps<typeof button> & MotiPressableProps;
export const AnimatedButton: React.FC<AnimatedButtonProps> = ({ variant, size, ...props }) => {
export const AnimatedButton: FC<AnimatedButtonProps> = ({ variant, size, ...props }) => {
const { style, containerStyle, ...otherProps } = props;
return (
<MotiPressable

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { StyleProp, Text, View, ViewStyle } from 'react-native';
import tw from '~/lib/tailwind';

View File

@@ -1,5 +1,5 @@
import { VariantProps, cva } from 'class-variance-authority';
import React from 'react';
import { FC } from 'react';
import { TextInput as RNTextInput, TextInputProps as RNTextInputProps } from 'react-native';
import tw from '~/lib/tailwind';
@@ -20,7 +20,7 @@ const input = cva(['text-sm rounded-md border shadow-sm'], {
type InputProps = VariantProps<typeof input> & RNTextInputProps;
export const TextInput: React.FC<InputProps> = ({ variant, ...props }) => {
export const TextInput: FC<InputProps> = ({ variant, ...props }) => {
const { style, ...otherProps } = props;
return (
<RNTextInput

View File

@@ -1,5 +1,5 @@
import byteSize from 'byte-size';
import React from 'react';
import { FC } from 'react';
import { ScrollView, Text, View } from 'react-native';
import { Statistics } from '~/types/bindings';
@@ -17,7 +17,7 @@ type OverviewStatsProps = {
stats: Statistics | undefined;
};
const StatItem: React.FC<{ title: string; bytes: number }> = ({ title, bytes }) => {
const StatItem: FC<{ title: string; bytes: number }> = ({ title, bytes }) => {
const { value, unit } = byteSize(+bytes);
const count = useCounter({ name: title, end: Number(value) });

View File

@@ -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<Procedures>();
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<any> {
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<T extends keyof Procedures> =
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
| Extract<Procedures[T], { input: never }>;
type LibraryProcedures<T extends keyof Procedures> = Exclude<
Extract<Procedures[T], { input: LibraryArgs<any> }>,
{ input: never }
>;
type MoreConstrainedQueries<T extends ProcedureDef> = T extends any
? T['input'] extends LibraryArgs<infer E>
? {
key: T['key'];
input: E;
result: T['result'];
}
: never
: never;
export const useBridgeQuery = rspc.customQuery<NonLibraryProcedure<'queries'>>(
(keyAndInput) => keyAndInput as any
);
export const useBridgeMutation = rspc.customMutation<NonLibraryProcedure<'mutations'>>(
(keyAndInput) => keyAndInput
);
export const useLibraryQuery = rspc.customQuery<
MoreConstrainedQueries<LibraryProcedures<'queries'>>
>((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<LibraryProcedures<'mutations'>>
>((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);
}
});
}

View File

@@ -2,18 +2,6 @@ import { useEffect } from 'react';
import { useCountUp } from 'use-count-up';
import { proxy, useSnapshot } from 'valtio';
// const useCounterStore = create<{
// counterLastValue: Map<string, number>;
// setCounterLastValue: (key: string, value: number) => void;
// }>((set) => ({
// counterLastValue: new Map<string, number>(),
// setCounterLastValue: (name, lastValue) =>
// set((state) => ({
// ...state,
// counterLastValue: state.counterLastValue.set(name, lastValue)
// }))
// }));
const counterStore = proxy({
counterLastValue: new Map<string, number>(),
setCounterLastValue: (key: string, value: number) => {

View File

@@ -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<TProcedures extends ProceduresDef>(): TRPCLink<TProcedures> {
return wsLink<TProcedures>({
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<number | string, TRequest> = Object.create(null);
let dispatchTimer: ReturnType<typeof setTimeout> | 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
};
}

View File

@@ -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';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Text, View } from 'react-native';
import tw from '~/lib/tailwind';
import { SharedScreenProps } from '~/navigation/SharedScreens';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Text, View } from 'react-native';
import tw from '~/lib/tailwind';
import { SharedScreenProps } from '~/navigation/SharedScreens';

View File

@@ -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';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Text, View } from 'react-native';
import tw from '~/lib/tailwind';
import { RootStackScreenProps } from '~/navigation';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Text, View } from 'react-native';
import { useSnapshot } from 'valtio';
import { AnimatedButton } from '~/components/primitive/Button';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<BottomSheetModalMethods>,
fileRef: null as RefObject<BottomSheetModalMethods>,
data: null as FilePath | null,
setData: (data: FilePath) => {
fileModalStore.data = data;

View File

@@ -14,6 +14,11 @@ export type Procedures = {
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: Array<IndexerRule> } |
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, 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<number>, result: Tag | null } |
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
@@ -92,6 +97,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<NormalisedUser>, owner: NormalisedUser, non_normalised_data: Array<null> }
export interface NormalisedUser { $type: string, $id: any, id: string, name: string }
export interface NormalizedVec<T> { $type: string, edges: Array<T> }
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 interface ObjectValidatorArgs { id: number, path: string }

View File

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

View File

@@ -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<Procedures>({
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 (
<div className="App">
<rspc.Provider client={client} queryClient={queryClient}>
<hooks.Provider client={client} queryClient={queryClient}>
<PlatformProvider platform={platform}>
<SpacedriveInterface />
</PlatformProvider>
</rspc.Provider>
</hooks.Provider>
</div>
);
}

View File

@@ -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",
@@ -36,6 +37,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,10 +54,11 @@ 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"] }
tracing-android = { version = "0.2.0", optional = true }
async-stream = "0.3.3"
once_cell = "1.15.0"
ctor = "0.1.23"

View File

@@ -1,5 +1,4 @@
use std::{
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
@@ -39,6 +38,7 @@ mod files;
mod jobs;
mod libraries;
mod locations;
mod normi;
mod tags;
pub mod utils;
pub mod volumes;
@@ -51,13 +51,15 @@ struct NodeState {
}
pub(crate) fn mount() -> Arc<Router> {
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 = <Router>::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)
.query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION")))
.query("getNode", |t| {
t(|ctx, _: ()| async move {
@@ -68,6 +70,7 @@ pub(crate) fn mount() -> Arc<Router> {
})
})
})
.merge("normi.", normi::mount())
.merge("library.", libraries::mount())
.merge("volumes.", volumes::mount())
.merge("tags.", tags::mount())
@@ -99,24 +102,14 @@ pub(crate) fn mount() -> Arc<Router> {
.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();
}
}

91
core/src/api/normi.rs Normal file
View File

@@ -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<User>,
#[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)
})
}

View File

@@ -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<TArg>| {
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<TArg>| {
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<TArg>| {
// TODO(@Oscar): Upstream rspc work to allow this to work

View File

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

View File

@@ -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.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",
"eventemitter3": "^4.0.7",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"@tanstack/react-query": "^4.12.0",
"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": "*",

View File

@@ -14,6 +14,11 @@ export type Procedures = {
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: Array<IndexerRule> } |
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, 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<number>, result: Tag | null } |
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
@@ -92,6 +97,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<NormalisedUser>, owner: NormalisedUser, non_normalised_data: Array<null> }
export interface NormalisedUser { $type: string, $id: any, id: string, name: string }
export interface NormalizedVec<T> { $type: string, edges: Array<T> }
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 interface ObjectValidatorArgs { id: number, path: string }

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
import { ProcedureDef } from '@rspc/client';
// https://stackoverflow.com/a/54487392
export type OmitDistributive<T, K extends PropertyKey> = T extends any
? T extends object
? Id<OmitRecursively<T, K>>
: T
: never;
export type Id<T> = {} & { [P in keyof T]: T[P] }; // Cosmetic use only makes the tooltips expand the type can be removed
export type OmitRecursively<T extends any, K extends PropertyKey> = Omit<
{ [P in keyof T]: OmitDistributive<T[P], K> },
K
>;
/**
* is responsible for normalizing the Typescript type before the type is exposed back to the user.
*
* @internal
*/
export type Normalized<T extends ProcedureDef> = T extends any
? {
key: T['key'];
// TODO: Typescript transformation for arrays
result: OmitRecursively<T['result'], '$id' | '$type'>;
input: T['input'];
}
: never;

View File

@@ -0,0 +1,183 @@
export type NormiCache = Map<string /* $type */, Map<string /* $id */, any>>;
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<K, A, B>(map: Map<K, Map<A, B>>, key: K): Map<A, B> {
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

View File

@@ -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<Procedures>();
import { normiCustomHooks } from './normi';
import { Normalized } from './normi/types';
type NonLibraryProcedure<T extends keyof Procedures> =
| Exclude<Procedures[T], { input: LibraryArgs<any> }>
@@ -17,7 +16,7 @@ type LibraryProcedures<T extends keyof Procedures> = Exclude<
{ input: never }
>;
type MoreConstrainedQueries<T extends ProcedureDef> = T extends any
type StripLibraryArgsFromInput<T extends ProcedureDef> = T extends any
? T['input'] extends LibraryArgs<infer E>
? {
key: T['key'];
@@ -27,30 +26,56 @@ type MoreConstrainedQueries<T extends ProcedureDef> = T extends any
: never
: never;
export const useBridgeQuery = rspc.customQuery<NonLibraryProcedure<'queries'>>(
(keyAndInput) => keyAndInput as any
);
export const hooks = internal_createReactHooksFactory();
export const useBridgeMutation = rspc.customMutation<NonLibraryProcedure<'mutations'>>(
(keyAndInput) => keyAndInput
);
export const useLibraryQuery = rspc.customQuery<
MoreConstrainedQueries<LibraryProcedures<'queries'>>
>((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<NonLibraryProcedure<'queries'>>,
// Normalized<NonLibraryProcedure<'mutations'>>
NonLibraryProcedure<'queries'>,
NonLibraryProcedure<'mutations'>
>({
internal: {
customHooks: normiCustomHooks({ contextSharing: true })
}
});
export const useLibraryMutation = rspc.customMutation<
MoreConstrainedQueries<LibraryProcedures<'mutations'>>
>((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<StripLibraryArgsFromInput<LibraryProcedures<'queries'>>>,
// Normalized<StripLibraryArgsFromInput<LibraryProcedures<'mutations'>>>,
StripLibraryArgsFromInput<LibraryProcedures<'queries'>>,
StripLibraryArgsFromInput<LibraryProcedures<'mutations'>>,
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<Procedures>();
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'], {

View File

@@ -19,77 +19,49 @@
"@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",
"@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",
"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",

View File

@@ -22,7 +22,7 @@ export default function SpacedriveInterface() {
<ErrorBoundary FallbackComponent={ErrorFallback}>
<QueryClientProvider client={queryClient} contextSharing={true}>
{/* 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 && (
<ReactQueryDevtools position="bottom-right" context={defaultContext} />
)}
<MemoryRouter>

View File

@@ -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<NodeState>((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);
}
};
}

View File

@@ -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();

View File

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

View File

@@ -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 }) => (
<div className="text-xs font-semibold text-gray-300">{children}</div>
);
function Heading({ children }: PropsWithChildren) {
return <div className="text-xs font-semibold text-gray-300">{children}</div>;
}
const SubHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="mb-1 text-xs font-medium text-gray-300">{children}</div>
);
function SubHeading({ children }: PropsWithChildren) {
return <div className="mb-1 text-xs font-medium text-gray-300">{children}</div>;
}
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 = () => {
</div>
</div>
);
};
}

View File

@@ -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';

View File

@@ -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<HTMLTextAreaElement>) {
if (e.target.value !== note) {

View File

@@ -1,12 +1,6 @@
import { Button, Input, Select, SelectOption, Tabs } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Tabs } from '@sd/ui';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
import { KeyList } from './KeyList';
import { KeyMounter } from './KeyMounter';

View File

@@ -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 (
<div
className={clsx(

View File

@@ -2,13 +2,13 @@ import { Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/solid';
import { ButtonLink } from '@sd/ui';
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
export interface ModalProps {
full?: boolean;
children: React.ReactNode;
}
export const Modal: React.FC<ModalProps> = (props) => {
export function Model(
props: PropsWithChildren<{
full?: boolean;
}>
) {
return (
<div
className={clsx('absolute w-screen h-screen z-30', {
@@ -51,4 +51,4 @@ export const Modal: React.FC<ModalProps> = (props) => {
</div>
</div>
);
};
}

View File

@@ -5,6 +5,7 @@ import { LocationCreateArgs } from '@sd/client';
import { Button, CategoryHeading, 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<NavLinkProps>) => (
<NavLink {...props}>
{({ isActive }) => (
<span

View File

@@ -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';

View File

@@ -1,15 +1,15 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { DefaultProps } from './types';
interface InputContainerProps extends DefaultProps<HTMLDivElement> {
title: string;
description?: string;
children: React.ReactNode;
mini?: boolean;
}
export const InputContainer: React.FC<InputContainerProps> = (props) => {
export function InputContainer(props: PropsWithChildren<InputContainerProps>) {
return (
<div className="flex flex-row">
<div
@@ -23,4 +23,4 @@ export const InputContainer: React.FC<InputContainerProps> = (props) => {
{props.mini && props.children}
</div>
);
};
}

View File

@@ -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<TagProps>) {
return (
<div
className={clsx(

View File

@@ -1,3 +1,5 @@
export const SettingsContainer = ({ children }: { children: React.ReactNode }) => (
import { PropsWithChildren } from 'react';
export const SettingsContainer = ({ children }: PropsWithChildren) => (
<div className="flex flex-col flex-grow w-full max-w-4xl space-y-6">{children}</div>
);

View File

@@ -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) => (
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
);
export const SettingsHeading: React.FC<{ className?: string; children: string }> = ({
export function SettingsHeading({
children,
className
}) => (
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-400', className)}>
{children}
</div>
);
}: PropsWithChildren<{ className?: string }>) {
return (
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-400', className)}>
{children}
</div>
);
}

View File

@@ -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<TooltipProps>) => {
return (
<TooltipPrimitive.Provider>
<TooltipPrimitive.Root>

View File

@@ -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 (
<Transition
show

View File

@@ -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<string, number>;
setCounterLastValue(key: string, value: number): void;
}>((set) => ({
const counterStore = proxy({
counterLastValue: new Map<string, number>(),
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),

Some files were not shown because too many files have changed in this diff Show More