files and attachments pane

pull/37/head
Niko PLP 5 months ago
parent b6680d7cab
commit 00398272bd
  1. 1
      Cargo.lock
  2. 25
      ng-app/index-native.html
  3. 112
      ng-app/src-tauri/src/lib.rs
  4. 52
      ng-app/src/api.ts
  5. 17
      ng-app/src/apps/ListView.svelte
  6. 1
      ng-app/src/lib/Document.svelte
  7. 2
      ng-app/src/lib/FullLayout.svelte
  8. 1
      ng-app/src/lib/Pane.svelte
  9. 719
      ng-app/src/lib/Test.svelte
  10. 237
      ng-app/src/lib/panes/Files.svelte
  11. 7
      ng-app/src/lib/panes/History.svelte
  12. 7
      ng-app/src/locales/de.json
  13. 13
      ng-app/src/locales/en.json
  14. 39
      ng-app/src/store.ts
  15. 2
      ng-app/src/tab.ts
  16. 2
      ng-broker/src/server_broker.rs
  17. 19
      ng-net/src/app_protocol.rs
  18. 1
      ng-sdk-js/Cargo.toml
  19. 226
      ng-sdk-js/src/lib.rs
  20. 1
      ng-verifier/src/request_processor.rs
  21. 29
      ngaccount/web/index.html

1
Cargo.lock generated

@ -3498,6 +3498,7 @@ name = "ng-sdk-js"
version = "0.1.0-preview.1" version = "0.1.0-preview.1"
dependencies = [ dependencies = [
"async-std", "async-std",
"futures",
"getrandom 0.1.16", "getrandom 0.1.16",
"gloo-timers", "gloo-timers",
"js-sys", "js-sys",

@ -56,9 +56,34 @@
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NextGraph</title> <title>NextGraph</title>
<style>
.splashing {
height: 95vh;display: grid;
}
</style>
</head> </head>
<body> <body>
<div id="splash" class="splashing">
<div style="margin: auto; display: grid;">
<svg
style="width:100px;height:100px;margin: 0 auto 20px ;"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 225 225"
>
<g>
<circle
r="106.98013"
cy="112.90476"
cx="109.88096"
style="fill:#ffffff;stroke:none;stroke-width:0.268375" />
<path
d="M 98.343352,190.26108 C 80.403778,187.53354 65.011938,179.57839 52.608228,166.62327 38.602093,151.99448 31.178059,133.41381 31.178059,112.98841 c 0,-10.21889 1.700058,-19.44396 5.221234,-28.332119 4.28678,-10.820699 10.037295,-19.39063 18.535095,-27.62263 4.72982,-4.58187 6.60687,-6.10643 11.28099,-9.16256 11.89869,-7.779841 24.173884,-11.879991 38.095802,-12.724761 19.80437,-1.2017 39.11165,5.11306 54.60284,17.858751 1.50718,1.24006 2.72951,2.35934 2.71628,2.48729 -0.0132,0.12795 -3.85821,3.63443 -8.54442,7.79217 -4.6862,4.157729 -10.04724,8.96276 -11.91342,10.677819 -1.86617,1.715071 -3.54094,3.11831 -3.7217,3.11831 -0.18075,0 -1.39985,-0.745188 -2.70911,-1.655969 -7.53011,-5.23834 -15.99428,-7.82188 -25.62597,-7.82188 -12.731628,0 -23.249192,4.3379 -32.143882,13.257541 -6.39594,6.413868 -10.70387,14.555268 -12.50018,23.623578 -0.69099,3.48832 -0.68968,13.53072 0.002,17.00893 3.70508,18.62577 18.31886,33.10194 36.642322,36.29729 4.16439,0.72621 11.98099,0.71223 15.98975,-0.0286 14.03187,-2.59311 25.86047,-11.36806 32.26533,-23.93578 0.77379,-1.51834 1.26018,-2.88461 1.08086,-3.03616 -0.17934,-0.15156 -6.87448,-1.1779 -14.87813,-2.28078 -9.7795,-1.34758 -14.92353,-2.21379 -15.68471,-2.64117 -1.52067,-0.85379 -2.83611,-2.88806 -2.83611,-4.3859 0,-1.1732 2.02687,-15.86876 2.49085,-18.05962 0.29676,-1.40127 2.42559,-3.4934 3.84317,-3.77691 0.62227,-0.12445 8.82712,0.85555 18.28065,2.18348 9.43343,1.32511 17.26269,2.29453 17.39833,2.15427 0.13566,-0.14026 1.11808,-6.54833 2.18313,-14.24014 1.10778,-8.000208 2.20407,-14.60184 2.56177,-15.426229 0.34392,-0.792599 1.11019,-1.849131 1.70287,-2.34782 2.06321,-1.736079 3.1433,-1.785011 12.20439,-0.55291 9.63637,1.310309 10.70873,1.56224 12.28077,2.88503 1.64359,1.382979 2.2732,2.810909 2.25906,5.123309 -0.007,1.10173 -0.92172,8.29645 -2.03332,15.98826 -1.11158,7.69182 -1.97159,14.04091 -1.91113,14.1091 0.0605,0.0682 7.16644,1.11143 15.79109,2.31832 11.10566,1.55407 16.00827,2.38757 16.80223,2.85657 1.53015,0.90389 2.48023,2.64785 2.45017,4.49756 -0.0462,2.84349 -2.41252,18.12279 -2.97521,19.21089 -0.66164,1.27949 -2.60244,2.54696 -3.92109,2.56074 -0.51973,0.005 -7.87449,-0.95937 -16.34391,-2.144 -8.46944,-1.18464 -15.47588,-2.077 -15.56986,-1.98301 -0.094,0.094 -1.18792,7.34163 -2.43097,16.10589 -1.44004,10.15311 -2.49792,16.43621 -2.91556,17.31631 -0.72531,1.52848 -2.76261,3.06291 -4.53817,3.41802 -0.95688,0.19138 -10.90014,-0.92798 -13.59859,-1.53084 -0.5471,-0.12223 -1.89146,0.67252 -4.50941,2.66588 -11.2627,8.57562 -24.34195,13.90917 -38.35741,15.64164 -4.40038,0.54395 -15.72658,0.43298 -19.853658,-0.19451 z"
style="fill:#4972a5;fill-opacity:1;stroke:#4972a5;stroke-width:0.377976;stroke-opacity:1" />
</g>
</svg>
</div>
</div>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

@ -338,6 +338,25 @@ async fn decode_invitation(invite: String) -> Option<Invitation> {
decode_invitation_string(invite) decode_invitation_string(invite)
} }
#[tauri::command(rename_all = "snake_case")]
async fn file_get(
session_id: u64,
stream_id: &str,
reference: BlockRef,
branch_nuri: String,
app: tauri::AppHandle,
) -> Result<(), String> {
let branch_nuri =
NuriV0::new_from(&branch_nuri).map_err(|e| format!("branch_nuri: {}", e.to_string()))?;
let mut nuri = NuriV0::new_from_obj_ref(&reference);
nuri.copy_target_from(&branch_nuri);
let mut request = AppRequest::new(AppRequestCommandV0::FileGet, nuri, None);
request.set_session_id(session_id);
app_request_stream(request, stream_id, app).await
}
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
async fn app_request_stream( async fn app_request_stream(
request: AppRequest, request: AppRequest,
@ -382,6 +401,69 @@ async fn app_request_stream(
Ok(()) Ok(())
} }
#[tauri::command(rename_all = "snake_case")]
async fn file_save_to_downloads(
session_id: u64,
reference: ObjectRef,
filename: String,
branch_nuri: String,
app: tauri::AppHandle,
) -> Result<(), String> {
let branch_nuri =
NuriV0::new_from(&branch_nuri).map_err(|e| format!("branch_nuri: {}", e.to_string()))?;
let mut nuri = NuriV0::new_from_obj_ref(&reference);
nuri.copy_target_from(&branch_nuri);
let mut request = AppRequest::new(AppRequestCommandV0::FileGet, nuri, None);
request.set_session_id(session_id);
let (mut reader, cancel) = nextgraph::local_broker::app_request_stream(request)
.await
.map_err(|e| e.to_string())?;
let mut file_vec: Vec<u8> = vec![];
while let Some(app_response) = reader.next().await {
match app_response {
AppResponse::V0(AppResponseV0::FileMeta(filemeta)) => {
file_vec = Vec::with_capacity(filemeta.size as usize);
}
AppResponse::V0(AppResponseV0::FileBinary(mut bin)) => {
if !bin.is_empty() {
file_vec.append(&mut bin);
}
}
AppResponse::V0(AppResponseV0::EndOfStream) => break,
_ => return Err("invalid response".to_string()),
}
}
let mut i: usize = 0;
loop {
let dest_filename = if i == 0 {
filename.clone()
} else {
filename
.rsplit_once(".")
.map(|(l, r)| format!("{l} ({}).{r}", i.to_string()))
.or_else(|| Some(format!("{filename} ({})", i.to_string())))
.unwrap()
};
let path = app
.path()
.resolve(dest_filename, BaseDirectory::Download)
.unwrap();
if path.exists() {
i = i + 1;
} else {
write(path, &file_vec).map_err(|e| e.to_string())?;
break;
}
}
Ok(())
}
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
async fn doc_fetch_private_subscribe() -> Result<AppRequest, String> { async fn doc_fetch_private_subscribe() -> Result<AppRequest, String> {
let request = AppRequest::new( let request = AppRequest::new(
@ -494,7 +576,7 @@ async fn sparql_query(
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
async fn app_request(request: AppRequest, _app: tauri::AppHandle) -> Result<AppResponse, String> { async fn app_request(request: AppRequest) -> Result<AppResponse, String> {
//log_debug!("app request {:?}", request); //log_debug!("app request {:?}", request);
nextgraph::local_broker::app_request(request) nextgraph::local_broker::app_request(request)
@ -502,19 +584,40 @@ async fn app_request(request: AppRequest, _app: tauri::AppHandle) -> Result<AppR
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
#[tauri::command(rename_all = "snake_case")]
async fn app_request_with_nuri_command(
nuri: String,
command: AppRequestCommandV0,
session_id: u64,
payload: Option<AppRequestPayloadV0>,
) -> Result<AppResponse, String> {
let nuri = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?;
let payload = payload.map(|p| AppRequestPayload::V0(p));
let request = AppRequest::V0(AppRequestV0 {
session_id,
command,
nuri,
payload,
});
app_request(request).await
}
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
async fn upload_chunk( async fn upload_chunk(
session_id: u64, session_id: u64,
upload_id: u32, upload_id: u32,
chunk: serde_bytes::ByteBuf, chunk: serde_bytes::ByteBuf,
nuri: NuriV0, nuri: String,
_app: tauri::AppHandle, _app: tauri::AppHandle,
) -> Result<AppResponse, String> { ) -> Result<AppResponse, String> {
//log_debug!("upload_chunk {:?}", chunk); //log_debug!("upload_chunk {:?}", chunk);
let mut request = AppRequest::new( let mut request = AppRequest::new(
AppRequestCommandV0::FilePut, AppRequestCommandV0::FilePut,
nuri, NuriV0::new_from(&nuri).map_err(|e| e.to_string())?,
Some(AppRequestPayload::V0( Some(AppRequestPayload::V0(
AppRequestPayloadV0::RandomAccessFilePutChunk((upload_id, chunk)), AppRequestPayloadV0::RandomAccessFilePutChunk((upload_id, chunk)),
)), )),
@ -725,7 +828,10 @@ impl AppBuilder {
doc_fetch_repo_subscribe, doc_fetch_repo_subscribe,
cancel_stream, cancel_stream,
app_request_stream, app_request_stream,
file_get,
file_save_to_downloads,
app_request, app_request,
app_request_with_nuri_command,
upload_chunk, upload_chunk,
get_device_name, get_device_name,
sparql_query, sparql_query,

@ -39,6 +39,7 @@ const mapping = {
"user_connect": ["info","user_id","location"], "user_connect": ["info","user_id","location"],
"user_disconnect": ["user_id"], "user_disconnect": ["user_id"],
"app_request": ["request"], "app_request": ["request"],
"app_request_with_nuri_command": ["nuri", "command", "session_id", "payload"],
"sparql_query": ["session_id","sparql","nuri"], "sparql_query": ["session_id","sparql","nuri"],
"sparql_update": ["session_id","sparql","nuri"], "sparql_update": ["session_id","sparql","nuri"],
"test": [ ], "test": [ ],
@ -46,6 +47,7 @@ const mapping = {
"doc_fetch_private_subscribe": [], "doc_fetch_private_subscribe": [],
"doc_fetch_repo_subscribe": ["repo_o"], "doc_fetch_repo_subscribe": ["repo_o"],
"branch_history": ["session_id", "nuri"], "branch_history": ["session_id", "nuri"],
"file_save_to_downloads": ["session_id", "reference", "filename", "branch_nuri"],
} }
@ -162,7 +164,42 @@ const handler = {
} }
return ret; return ret;
} }
else if (path[0] === "app_request_stream") { else if (path[0] === "file_get") {
let stream_id = (lastStreamId += 1).toString();
//console.log("stream_id",stream_id);
let { getCurrent } = await import("@tauri-apps/plugin-window");
//let session_id = args[0];
let callback = args[3];
let unlisten = await getCurrent().listen(stream_id, async (event) => {
//console.log(event.payload);
if (event.payload.V0.FileBinary) {
event.payload.V0.FileBinary = Uint8Array.from(event.payload.V0.FileBinary);
}
let ret = callback(event.payload);
if (ret === true) {
await tauri.invoke("cancel_stream", {stream_id});
} else if (ret.then) {
ret.then(async (val)=> {
if (val === true) {
await tauri.invoke("cancel_stream", {stream_id});
}
});
}
})
try {
await tauri.invoke("file_get",{stream_id, session_id:args[0], reference: args[1], branch_nuri:args[2]});
} catch (e) {
unlisten();
await tauri.invoke("cancel_stream", {stream_id});
throw e;
}
return () => {
unlisten();
tauri.invoke("cancel_stream", {stream_id});
}
} else if (path[0] === "app_request_stream") {
let stream_id = (lastStreamId += 1).toString(); let stream_id = (lastStreamId += 1).toString();
//console.log("stream_id",stream_id); //console.log("stream_id",stream_id);
let { getCurrent } = await import("@tauri-apps/plugin-window"); let { getCurrent } = await import("@tauri-apps/plugin-window");
@ -170,7 +207,7 @@ const handler = {
let request = args[0]; let request = args[0];
let callback = args[1]; let callback = args[1];
let unlisten = await getCurrent().listen(stream_id, (event) => { let unlisten = await getCurrent().listen(stream_id, async (event) => {
//console.log(event.payload); //console.log(event.payload);
if (event.payload.V0.FileBinary) { if (event.payload.V0.FileBinary) {
event.payload.V0.FileBinary = Uint8Array.from(event.payload.V0.FileBinary); event.payload.V0.FileBinary = Uint8Array.from(event.payload.V0.FileBinary);
@ -184,7 +221,16 @@ const handler = {
let removes_json_str = new TextDecoder().decode(Uint8Array.from(event.payload.V0.Patch.graph.removes)); let removes_json_str = new TextDecoder().decode(Uint8Array.from(event.payload.V0.Patch.graph.removes));
event.payload.V0.Patch.graph.removes = JSON.parse(removes_json_str); event.payload.V0.Patch.graph.removes = JSON.parse(removes_json_str);
} }
callback(event.payload).then(()=> {}) let ret = callback(event.payload);
if (ret === true) {
await tauri.invoke("cancel_stream", {stream_id});
} else if (ret.then) {
ret.then(async (val)=> {
if (val === true) {
await tauri.invoke("cancel_stream", {stream_id});
}
});
}
}) })
try { try {
await tauri.invoke("app_request_stream",{stream_id, request}); await tauri.invoke("app_request_stream",{stream_id, request});

@ -12,7 +12,6 @@
<script lang="ts"> <script lang="ts">
import { import {
get_blob,
} from "../store"; } from "../store";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte"; import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
@ -35,20 +34,4 @@
<div class="flex"> {triple}</div> <div class="flex"> {triple}</div>
{/each} {/each}
{#each commits.files as file}
<div class="flex">
{file.name}
{#await get_blob(file)}
<div class="row">
<Spinner />
</div>
{:then url}
{#if url}
<img src={url} title={file.nuri} alt={file.name} />
{/if}
{/await}
</div>
{/each}
</div> </div>

@ -15,7 +15,6 @@
active_session, active_session,
cannot_load_offline, cannot_load_offline,
online, online,
get_blob,
} from "../store"; } from "../store";
import { import {

@ -429,7 +429,7 @@
backdropClass="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 menu-bg-modal" backdropClass="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 menu-bg-modal"
> >
<div class="static"> <div class="static">
<div class="absolute top-2 right-4 w-10 h-10" role="button" aria-label="Close menu" title="Close menu" <div class="absolute top-2 right-4 w-10 h-10 bg-white" role="button" aria-label="Close menu" title="Close menu"
on:click={closeModal} on:click={closeModal}
on:keypress={closeModal} on:keypress={closeModal}
tabindex="0"> tabindex="0">

@ -15,6 +15,7 @@
const panes = { const panes = {
"history": "History", "history": "History",
"files": "Files",
}; };
const load_pane = async (pane_name) => { const load_pane = async (pane_name) => {

@ -10,737 +10,24 @@
--> -->
<script lang="ts"> <script lang="ts">
import {
createGitgraph,
templateExtend,
TemplateName,
} from "./panes/history/gitgraph-js/gitgraph";
import ng from "../api"; import ng from "../api";
import { import {
branch_subscribe, branch_subscribe,
active_session, active_session,
cannot_load_offline,
online, online,
get_blob,
} from "../store"; } from "../store";
import { link } from "svelte-spa-router";
import { onMount, onDestroy, tick } from "svelte"; import { onMount, onDestroy, tick } from "svelte";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte"; import { Button, Progressbar, Spinner } from "flowbite-svelte";
import DataClassIcon from "./DataClassIcon.svelte";
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
let is_tauri = import.meta.env.TAURI_PLATFORM; let is_tauri = import.meta.env.TAURI_PLATFORM;
let upload_progress: null | { total: number; current: number; error?: any } =
null;
let commits = $active_session && branch_subscribe($active_session.private_store_id, true);
let gitgraph;
let next = [
{
hash: "I",
subject: "niko2",
author: "",
parents: ["G"],
},
{
hash: "T",
subject: "niko2",
author: "",
parents: ["D", "H"],
},
{
hash: "Z",
subject: "niko2",
author: "",
parents: ["E"],
},
{
hash: "L",
subject: "niko2",
author: "",
parents: ["H"],
},
{
hash: "J",
subject: "niko2",
author: "",
parents: ["L", "Z", "I"],
},
{
hash: "K",
subject: "niko2",
author: "",
parents: ["G", "E"],
},
{
hash: "X",
subject: "niko2",
author: "",
parents: ["I"],
},
{
hash: "Q",
subject: "niko2",
author: "",
parents: ["L", "X"],
},
];
function add() {
let n = next.shift();
if (n) gitgraph.commit(n);
}
onMount(async () => { onMount(async () => {
const graphContainer = document.getElementById("graph-container");
gitgraph = createGitgraph(graphContainer, {
template: templateExtend(TemplateName.Metro, {
branch: { label: { display: false } },
commit: { message: { displayAuthor: false } },
}),
});
gitgraph.swimlanes(["A", "F", "C"]);
gitgraph.import([
{
hash: "A",
subject: "niko2",
branch: "A",
parents: [],
author: "",
x: 0,
y: 0,
},
{
hash: "B",
subject: "niko2",
branch: "A",
author: "",
parents: ["A"],
x: 0,
y: 1,
},
{
hash: "D",
subject: "niko2",
branch: "A",
author: "",
parents: ["B"],
x: 0,
y: 2,
},
{
hash: "C",
subject: "niko2",
branch: "C",
author: "",
parents: ["B"],
x: 2,
y: 3,
},
{
hash: "F",
subject: "niko2",
branch: "F",
author: "",
parents: ["B", "C"],
x: 1,
y: 4,
},
{
hash: "G",
subject: "niko2",
branch: "F",
parents: ["F"],
author: "",
x: 1,
y: 5,
},
{
hash: "E",
subject: "niko2",
branch: "C",
author: "",
parents: ["C"],
x: 2,
y: 6,
},
// {
// hash: "H",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["D", "G"],
// x: 0,
// y: 7,
// },
// {
// hash: "I",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["D", "H"],
// x: 0,
// y: 8,
// },
// {
// hash: "H",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["D", "G", "E"],
// x: 0,
// y: 7,
// },
]);
// window.setTimeout(() => {
// gitgraph.commit({
// hash: "H",
// subject: "niko2",
// author: "",
// parents: ["D", "G", "E"],
// });
// }, 0);
window.setTimeout(() => {
gitgraph.commit({
hash: "H",
subject: "niko2",
author: "",
parents: ["G", "E"],
});
}, 0);
// window.setTimeout(() => {
// gitgraph.commit({
// hash: "H",
// subject: "niko2",
// author: "",
// parents: ["G"],
// });
// }, 0);
// gitgraph.swimlanes(["A", "B", false, "D"]);
// gitgraph.import([
// {
// hash: "A",
// subject: "niko2",
// branch: "A",
// parents: [],
// author: "",
// x: 0,
// y: 0,
// },
// {
// hash: "C",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["A"],
// x: 0,
// y: 1,
// },
// {
// hash: "D",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["C"],
// x: 0,
// y: 2,
// },
// {
// hash: "E",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["D"],
// x: 0,
// y: 3,
// },
// {
// hash: "B",
// subject: "niko2",
// branch: "C",
// author: "",
// parents: ["A"],
// x: 2,
// y: 4,
// },
// {
// hash: "G",
// subject: "niko2",
// branch: "C",
// parents: ["B"],
// author: "",
// x: 2,
// y: 5,
// },
// {
// hash: "F",
// subject: "niko2",
// branch: "B",
// author: "",
// parents: ["D", "G"],
// x: 1,
// y: 6,
// },
// {
// hash: "H",
// subject: "niko2",
// branch: "D",
// author: "",
// parents: ["G"],
// x: 3,
// y: 7,
// },
// // {
// // hash: "I",
// // subject: "niko2",
// // branch: "A",
// // author: "",
// // parents: ["E", "F", "H"],
// // x: 0,
// // y: 8,
// // },
// ]);
// gitgraph.swimlanes([
// "A",
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// false,
// ]);
// gitgraph.import([
// {
// hash: "A",
// subject: "niko2",
// branch: "A",
// parents: [],
// author: "",
// x: 0,
// y: 0,
// },
// {
// hash: "B",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["A"],
// x: 0,
// y: 1,
// },
// {
// hash: "C",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["B"],
// x: 0,
// y: 2,
// },
// {
// hash: "D",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["C"],
// x: 0,
// y: 3,
// },
// {
// hash: "E",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["D"],
// x: 0,
// y: 4,
// },
// {
// hash: "J",
// subject: "niko2",
// branch: "J",
// parents: ["A"],
// author: "",
// x: 2,
// y: 5,
// },
// {
// hash: "K",
// subject: "niko2",
// branch: "J",
// author: "",
// parents: ["J"],
// x: 2,
// y: 6,
// },
// {
// hash: "L",
// subject: "niko2",
// branch: "L",
// author: "",
// parents: ["A"],
// x: 3,
// y: 7,
// },
// {
// hash: "M",
// subject: "niko2",
// branch: "L",
// author: "",
// parents: ["L"],
// x: 3,
// y: 8,
// },
// {
// hash: "G",
// subject: "niko2",
// branch: "G",
// author: "",
// parents: ["C", "K", "M"],
// x: 1,
// y: 9,
// },
// {
// hash: "H",
// subject: "niko2",
// branch: "G",
// author: "",
// parents: ["G"],
// x: 1,
// y: 10,
// },
// {
// hash: "I",
// subject: "niko2",
// branch: "G",
// author: "",
// parents: ["H"],
// x: 1,
// y: 11,
// },
// {
// hash: "F",
// subject: "niko2",
// branch: "A",
// author: "",
// parents: ["E", "I"],
// x: 0,
// y: 12,
// },
// {
// hash: "1",
// subject: "niko2",
// branch: "1",
// author: "",
// parents: ["A"],
// x: 4,
// y: 13,
// },
// {
// hash: "2",
// subject: "niko2",
// branch: "2",
// author: "",
// parents: ["A"],
// x: 5,
// y: 14,
// },
// {
// hash: "3",
// subject: "niko2",
// branch: "3",
// author: "",
// parents: ["A"],
// x: 6,
// y: 15,
// },
// {
// hash: "4",
// subject: "niko2",
// branch: "4",
// author: "",
// parents: ["A"],
// x: 7,
// y: 16,
// },
// {
// hash: "5",
// subject: "niko2",
// branch: "5",
// author: "",
// parents: ["A"],
// x: 8,
// y: 17,
// },
// {
// hash: "6",
// subject: "niko2",
// branch: "6",
// author: "",
// parents: ["A"],
// x: 9,
// y: 18,
// },
// {
// hash: "7",
// subject: "niko2",
// branch: "7",
// author: "",
// parents: ["A"],
// x: 10,
// y: 19,
// },
// {
// hash: "8",
// subject: "niko2",
// branch: "8",
// author: "",
// parents: ["A"],
// x: 11,
// y: 20,
// },
// {
// hash: "9",
// subject: "niko2",
// branch: "9",
// author: "",
// parents: ["A"],
// x: 12,
// y: 21,
// },
// ]);
}); });
let fileinput;
function uploadFile(upload_id, nuri, file, success) {
let chunkSize = 1_048_564;
let fileSize = file.size;
let offset = 0;
let readBlock = null;
upload_progress = { total: fileSize, current: offset };
let onLoadHandler = async function (event) {
let result = event.target.result;
if (event.target.error == null) {
offset += result.byteLength;
upload_progress = { total: fileSize, current: offset };
// console.log("chunk", result);
let res = await ng.upload_chunk(
$active_session.session_id,
upload_id,
result,
nuri
);
//console.log("chunk upload res", res);
// if (onChunkRead) {
// onChunkRead(result);
// }
} else {
// if (onChunkError) {
// onChunkError(event.target.error);
// }
return;
}
// If finished:
if (offset >= fileSize) {
//console.log("file uploaded");
let res = await ng.upload_chunk(
$active_session.session_id,
upload_id,
[],
nuri
);
//console.log("end upload res", res);
if (success) {
upload_progress = { total: fileSize, current: fileSize };
success(res);
} else {
upload_progress = { total: fileSize, current: fileSize, error: true };
}
// Make progress bar disappear
setTimeout(() => {
upload_progress = null;
}, 2_500);
return;
}
readBlock(offset, chunkSize, file);
};
readBlock = function (offset, length, file) {
let fileReader = new FileReader();
let blob = file.slice(offset, length + offset);
fileReader.onload = onLoadHandler;
fileReader.readAsArrayBuffer(blob);
};
readBlock(offset, chunkSize, file);
return;
}
const onFileSelected = async (e) => {
let image = e.target.files[0];
if (!image) return;
//console.log(image);
let nuri = {
target: "PrivateStore",
entire_store: false,
access: [],
locator: [],
};
let start_request = {
V0: {
command: "FilePut",
nuri,
payload: {
V0: {
RandomAccessFilePut: image.type,
},
},
session_id: $active_session.session_id,
},
};
let start_res = await ng.app_request(start_request);
let upload_id = start_res.V0.FileUploading;
uploadFile(upload_id, nuri, image, async (reference) => {
if (reference) {
let request = {
V0: {
command: "FilePut",
nuri,
payload: {
V0: {
AddFile: {
filename: image.name,
object: reference.V0.FileUploaded,
},
},
},
session_id: $active_session.session_id,
},
};
await ng.app_request(request);
}
});
fileinput.value = "";
};
</script> </script>
<div>
<div id="graph-container"></div>
{#if $cannot_load_offline}
<div class="row p-4">
<Alert color="yellow">
{@html $t("doc.cannot_load_offline")}
<a href="#/user">{$t("pages.user_panel.title")}</a>.
</Alert>
</div>
{:else}
<div class="row pt-2">
<Button
type="button"
on:click={() => {
add();
}}
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mr-2 mb-2"
>
g
</Button>
<Button
disabled={!$online && !is_tauri}
type="button"
on:click={() => {
fileinput.click();
}}
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mr-2 mb-2"
>
<svg
class="w-8 h-8 mr-2 -ml-1"
fill="none"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
{$t("pages.test.add_image")}
</Button>
<input
style="display:none"
type="file"
accept=".jpg, .jpeg, .png"
on:change={(e) => onFileSelected(e)}
bind:this={fileinput}
/>
</div>
{#if upload_progress !== null}
<div class="mx-6 mt-2">
<Progressbar
progress={(
(100 * upload_progress.current) /
upload_progress.total
).toFixed(0)}
labelOutside={$t("pages.test.upload_progress")}
/>
</div>
{/if}
{#if commits}
{#await commits.load()}
<p>{$t("connectivity.loading")}...</p>
{:then}
{#each $commits.graph as triple} {triple}<br/> {/each}
{#each $commits.heads as head} {head} <br/> {/each}
{#each $commits.files as file}
<p>
{file.name}
{#await get_blob(file)}
<div class="ml-2">
<Spinner />
</div>
{:then url}
{#if url}
<img src={url} title={file.nuri} alt={file.name} />
{/if}
{/await}
</p>
{/each}
{/await}
{/if}
{/if}
</div>

@ -0,0 +1,237 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<script lang="ts">
import ng from "../../api";
import {
branch_subscribe,
active_session,
online,
get_blob,
} from "../../store";
import { cur_tab } from "../../tab";
import {
ExclamationTriangle,
ArrowDownTray,
ArrowUpTray,
} from "svelte-heros-v2";
import { onMount, onDestroy, tick } from "svelte";
import { Button, Progressbar, Spinner } from "flowbite-svelte";
import { t } from "svelte-i18n";
let is_tauri = import.meta.env.TAURI_PLATFORM;
let upload_progress: null | { total: number; current: number; error?: any } = null;
let commits = $active_session && branch_subscribe($cur_tab.branch.nuri, false);
let fileinput;
let file_urls = {};
const prepare_url = (nuri) => {
if (!file_urls[nuri]) {
file_urls[nuri] = {
click: false
};
}
return true;
}
const download = async (file) => {
if (is_tauri) {
await ng.file_save_to_downloads($active_session.session_id, file.reference, file.name, "did:ng:"+$cur_tab.branch.nuri);
} else {
file_urls[file.nuri].url = await get_blob(file, false);
//console.log(file.name);
//console.log(file_urls[file.nuri].click);
await tick();
file_urls[file.nuri].click.click();
}
}
const isImage = async (url) : Promise<boolean> => {
if ( typeof url === 'string' ) {
let blob = await fetch(url).then(r => r.blob());
return blob.type.startsWith("image/");
}
return false;
}
function uploadFile(upload_id, nuri, file, success) {
console.log(nuri);
let chunkSize = 1_048_564;
let fileSize = file.size;
let offset = 0;
let readBlock = null;
upload_progress = { total: fileSize, current: offset };
let onLoadHandler = async function (event) {
let result = event.target.result;
if (event.target.error == null) {
offset += result.byteLength;
upload_progress = { total: fileSize, current: offset };
// console.log("chunk", result);
let res = await ng.upload_chunk(
$active_session.session_id,
upload_id,
result,
nuri
);
//console.log("chunk upload res", res);
// if (onChunkRead) {
// onChunkRead(result);
// }
} else {
// if (onChunkError) {
// onChunkError(event.target.error);
// }
return;
}
// If finished:
if (offset >= fileSize) {
//console.log("file uploaded");
let res = await ng.upload_chunk(
$active_session.session_id,
upload_id,
[],
nuri
);
//console.log("end upload res", res);
if (success) {
upload_progress = { total: fileSize, current: fileSize };
success(res);
} else {
upload_progress = { total: fileSize, current: fileSize, error: true };
}
// Make progress bar disappear
setTimeout(() => {
upload_progress = null;
}, 1_000);
return;
}
readBlock(offset, chunkSize, file);
};
readBlock = function (offset, length, file) {
let fileReader = new FileReader();
let blob = file.slice(offset, length + offset);
fileReader.onload = onLoadHandler;
fileReader.readAsArrayBuffer(blob);
};
readBlock(offset, chunkSize, file);
return;
}
const onFileSelected = async (e) => {
let image = e.target.files[0];
if (!image) return;
//console.log(image.type);
let start_request_payload = {
RandomAccessFilePut: image.type,
};
let nuri = "did:ng:"+$cur_tab.branch.nuri;
let start_res = await ng.app_request_with_nuri_command(nuri, "FilePut", $active_session.session_id, start_request_payload);
let upload_id = start_res.V0.FileUploading;
uploadFile(upload_id, nuri, image, async (reference) => {
if (reference) {
let file_put_payload = {
AddFile: {
filename: image.name,
object: reference.V0.FileUploaded,
},
};
await ng.app_request_with_nuri_command(nuri, "FilePut", $active_session.session_id, file_put_payload);
}
});
fileinput.value = "";
};
</script>
<div class="w-full">
<div class="row pt-2 w-full">
<Button
disabled={!$online && !is_tauri}
type="button"
on:click={() => {
fileinput.click();
}}
class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mr-2 mb-2"
>
<ArrowUpTray class="w-8 h-8 mr-2 -ml-1"/>
{$t("doc.file.upload")}
</Button>
<input
style="display:none"
type="file"
on:change={(e) => onFileSelected(e)}
bind:this={fileinput}
/>
</div>
{#if upload_progress !== null}
<div class="mx-6 mt-2">
<Progressbar
progress={(
(100 * upload_progress.current) /
upload_progress.total
).toFixed(0)}
labelOutside={$t("doc.file.upload_progress")}
/>
</div>
{/if}
{#if commits}
{#await commits.load()}
<p>{$t("connectivity.loading")}...</p>
{:then}
{#each $commits.files as file}
<p class="mb-5">
{#await get_blob(file, true)}
<div class="ml-2">
<Spinner />
</div>
{:then url}
{#await isImage(url) then is}
{#if is}
<img src={url} title={file.nuri} alt={file.name} />
{/if}
{/await}
<span class="ml-2 text-gray-600">{file.name}<br/></span>
{#if url === false}
<span><ExclamationTriangle tabindex="-1" class="ml-2 w-6 h-8 focus:outline-none" style="display:inline"/>{$t("errors.cannot_load_this_file")}</span>
{:else if prepare_url(file.nuri)}
<a bind:this={file_urls[file.nuri].click}
href={file_urls[file.nuri].url || ""}
target="_blank"
download={file.name}
></a>
<button class="ml-2 select-none p-1 pb-0 text-gray-600" style="box-shadow:none;" on:click={()=>download(file)}>
<span><ArrowDownTray tabindex="-1" class="w-6 h-8 mr-3 focus:outline-none" style="display:inline"/>{$t("doc.file.download")}</span>
</button>
{/if}
{/await}
</p>
{/each}
{/await}
{/if}
</div>

@ -13,9 +13,6 @@
import { import {
branch_subscribe, branch_subscribe,
active_session, active_session,
cannot_load_offline,
online,
get_blob
} from "../../store"; } from "../../store";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { onMount, onDestroy, tick } from "svelte"; import { onMount, onDestroy, tick } from "svelte";
@ -36,7 +33,7 @@
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte"; import { Button, Progressbar, Spinner, Alert } from "flowbite-svelte";
import { cur_tab, nav_bar, can_have_header, header_icon, header_title, header_description, cur_branch, set_header_in_view, edit_header_button, cur_app, load_official_app } from "../../tab"; import { cur_tab } from "../../tab";
import ng from "../../api"; import ng from "../../api";
import { import {
@ -139,7 +136,7 @@
{#if commit[1].final_consistency}<ShieldCheck tabindex="-1" class="w-5 h-5 absolute text-primary-600" style="top:9px;right:20px;" /> {#if commit[1].final_consistency}<ShieldCheck tabindex="-1" class="w-5 h-5 absolute text-primary-600" style="top:9px;right:20px;" />
{:else if commit[1].signature}<ShieldCheck tabindex="-1" class="w-5 h-5 absolute text-green-600" style="top:9px;right:20px;" /> {:else if commit[1].signature}<ShieldCheck tabindex="-1" class="w-5 h-5 absolute text-green-600" style="top:9px;right:20px;" />
{/if} {/if}
<Icon tabindex="-1" class="w-5 h-5 outline-none absolute " style="top:9px;right:0px;" variation="outline" color="currentColor" icon={commit_type_icons[commit[1].commit_type]} /> <Icon tabindex="-1" class="w-5 h-5 focus:outline-none absolute " style="top:9px;right:0px;" variation="outline" color="currentColor" icon={commit_type_icons[commit[1].commit_type]} />
{#if commit[1].commit_type==="TransactionBoth"}<Cloud tabindex="-1" class="w-5 h-5 absolute " style="top:28px;right:0px;" />{/if} {#if commit[1].commit_type==="TransactionBoth"}<Cloud tabindex="-1" class="w-5 h-5 absolute " style="top:28px;right:0px;" />{/if}
<b>{commit[0].substring(0,7)}</b><br/> <b>{commit[0].substring(0,7)}</b><br/>
<span class="text-xs leading-tight">{commit[1].author.substring(0,9)}</span> <span class="text-xs leading-tight">{commit[1].author.substring(0,9)}</span>

@ -74,11 +74,6 @@
"description": "Auf diesen Gerät scheint es noch kein Wallet zu geben.<br />Wenn du bereits ein Wallet hast, wähle \"Anmelden\". Andernfalls wähle \"Wallet erstellen\".", "description": "Auf diesen Gerät scheint es noch kein Wallet zu geben.<br />Wenn du bereits ein Wallet hast, wähle \"Anmelden\". Andernfalls wähle \"Wallet erstellen\".",
"create_wallet": "Wallet erstellen" "create_wallet": "Wallet erstellen"
}, },
"test": {
"cannot_load_offline": "Du bist offline und nutzt die Web-App. Du musst dich mindestens ein mal mit dem Broker verbinden, bevor du die App lokal nutzen kannst. Das liegt daran, dass die Web-App keine lokalen Kopien deiner Dokumente speichern kann.<br /><br />Sobald du ein mal verbunden warst, wirst du auch bei Verbindungsabbruch zu einigen Funktionen Zugriff haben. Das Senden von Binärdateien ist dann nicht möglich, da dein Browser nur begrenzte Speicherkapazitäten von circa 5MB zur Verfügung stellt.<br /><br />Diese Einschränkungen fallen weg, sobald das Feature \"UserStorage for Web\" veröffentlicht wurde!<br /><br />Deinen Verbindungsstatus siehst du auf",
"add_image": "Bild hinzufügen",
"upload_progress": "Lade hoch..."
},
"login": { "login": {
"heading": "Wie öffente ich mein Wallet? Es gibt zwei Optionen:", "heading": "Wie öffente ich mein Wallet? Es gibt zwei Optionen:",
"with_pazzle": "Mit deinem Pazzle", "with_pazzle": "Mit deinem Pazzle",
@ -329,7 +324,7 @@
"ServerError": "Server-Fehler.", "ServerError": "Server-Fehler.",
"InvalidResponse": "Ungültige Antwort erhalten.", "InvalidResponse": "Ungültige Antwort erhalten.",
"BootstrapError": "Fehler beim Bootstrapping", "BootstrapError": "Fehler beim Bootstrapping",
"NotAServerError": "Kein Server.", "NotAServerError": "Kein Server-Fehler.",
"VerifierError": "Fehler während der Überprüfung.", "VerifierError": "Fehler während der Überprüfung.",
"SiteNotFoundOnBroker": "Die Seite kann nicht auf dem Broker gefunden werden", "SiteNotFoundOnBroker": "Die Seite kann nicht auf dem Broker gefunden werden",
"BrokerConfigErrorStr": "{error}", "BrokerConfigErrorStr": "{error}",

@ -27,6 +27,11 @@
"chat": "Chat" "chat": "Chat"
} }
}, },
"file": {
"download": "Download",
"upload_progress": "Uploading...",
"upload": "Upload file"
},
"errors": { "errors": {
"InvalidNuri": "Invalid NURI" "InvalidNuri": "Invalid NURI"
}, },
@ -273,11 +278,6 @@
"description": "We could not find a wallet saved on this device.<br /> If you already have a wallet, select \"Log in\", otherwise, select \"Create Wallet\" here below.", "description": "We could not find a wallet saved on this device.<br /> If you already have a wallet, select \"Log in\", otherwise, select \"Create Wallet\" here below.",
"create_wallet": "Create Wallet" "create_wallet": "Create Wallet"
}, },
"test": {
"cannot_load_offline": "You are offline and using the web app. You need to connect to the broker at least once before you can start using the app locally because the web app does not keep a local copy of your documents.<br /><br /> Once connected, if you lose connectivity again, you will be able to have limited access to some functionalities. Sending binary files won't be possible, because the limit of local storage in your browser is around 5MB.<br /><br /> All those limitations will be lifted once the \"UserStorage for Web\" feature will be released. Stay tuned! <br /><br /> Check your connection status in the ",
"add_image": "Add Image",
"upload_progress": "Uploading..."
},
"login": { "login": {
"heading": "How to open your wallet? You have 2 options:", "heading": "How to open your wallet? You have 2 options:",
"with_pazzle": "With your Pazzle", "with_pazzle": "With your Pazzle",
@ -550,7 +550,8 @@
"IncompatibleQrCode": "You scanned a NextGraph QR-Code that is of the wrong type.", "IncompatibleQrCode": "You scanned a NextGraph QR-Code that is of the wrong type.",
"NotARendezVous": "You scanned an invalid QR-Code.", "NotARendezVous": "You scanned an invalid QR-Code.",
"camera_unavailable": "Camera is unavailable.", "camera_unavailable": "Camera is unavailable.",
"ServerAlreadyRunningInOtherProcess": "App is already running on this device. Check it and close it." "ServerAlreadyRunningInOtherProcess": "App is already running on this device. Check it and close it.",
"cannot_load_this_file": "Cannot load this file"
}, },
"connectivity": { "connectivity": {
"stopped": "Stopped", "stopped": "Stopped",

@ -452,11 +452,11 @@ export const branch_subscribe = function(nuri:string, in_tab:boolean) {
//console.log("sub"); //console.log("sub");
let already_subscribed = all_branches[nuri]; let already_subscribed = all_branches[nuri];
if (!already_subscribed) { if (!already_subscribed) {
const { subscribe, set, update } = writable({graph:[], discrete:[], files:[], history: {start:()=>{}, stop:()=>{}, take:()=>{}, commits:false}, heads: []}); // create the underlying writable store const { subscribe, set, update } = writable({graph:[], discrete:[], files:[], history: {start:()=>{}, stop:()=>{}, commits:false}, heads: []}); // create the underlying writable store // take:()=>{},
update((old)=> { update((old)=> {
old.history.start = () => update((o) => {o.history.commits = true; return o;}) ; old.history.start = () => update((o) => {o.history.commits = true; return o;}) ;
old.history.stop = () => update((o) => {o.history.commits = false; return o;}) ; old.history.stop = () => update((o) => {o.history.commits = false; return o;}) ;
old.history.take = () => { let res: boolean | Array<{}> = false; update((o) => {res = o.history.commits; o.history.commits = []; return o;}); return res;} //old.history.take = () => { let res: boolean | Array<{}> = false; update((o) => {res = o.history.commits; o.history.commits = []; return o;}); return res;}
return old;}); return old;});
let count = 0; let count = 0;
let unsub = () => { }; let unsub = () => { };
@ -535,6 +535,10 @@ export const branch_subscribe = function(nuri:string, in_tab:boolean) {
} }
old.graph.sort(); old.graph.sort();
} }
tab_update(nuri, ($cur_tab) => {
$cur_tab.branch.files = old.files.length;
return $cur_tab;
});
} else if (response.V0.Patch) { } else if (response.V0.Patch) {
let i = old.heads.length; let i = old.heads.length;
while (i--) { while (i--) {
@ -570,6 +574,10 @@ export const branch_subscribe = function(nuri:string, in_tab:boolean) {
old.graph.sort(); old.graph.sort();
} else if (response.V0.Patch.other?.FileAdd) { } else if (response.V0.Patch.other?.FileAdd) {
old.files.unshift(response.V0.Patch.other.FileAdd); old.files.unshift(response.V0.Patch.other.FileAdd);
tab_update(nuri, ($cur_tab) => {
$cur_tab.branch.files = old.files.length;
return $cur_tab;
});
} else { } else {
} }
@ -637,36 +645,25 @@ export const branch_subscribe = function(nuri:string, in_tab:boolean) {
}; };
let blob_cache = {}; let blob_cache = {};
export async function get_blob(ref: { nuri: string; reference: { key: any; id: any; }; }) { export async function get_blob(ref: { nuri: string; reference: { key: any; id: any; }; }, only_img: boolean) {
if (!ref) return false; if (!ref) return false;
const cached = blob_cache[ref.nuri]; const cached = blob_cache[ref.nuri];
if (cached) { if (cached && (((await cached) !== true) || only_img )) {
return cached; return cached;
} }
let prom = new Promise(async (resolve) => { let prom = new Promise(async (resolve) => {
try { try {
let nuri = {
target: "PrivateStore",
entire_store: false,
access: [{ Key: ref.reference.key }],
locator: [],
object: ref.reference.id,
};
let file_request = {
V0: {
command: "FileGet",
nuri,
session_id: get(active_session).session_id,
},
};
let final_blob; let final_blob;
let content_type; let content_type;
let unsub = await ng.app_request_stream(file_request, async (blob) => { let branch_nuri = "did:ng:"+get(cur_tab).branch.nuri;
let cancel = await ng.file_get(get(active_session).session_id, ref.reference, branch_nuri, async (blob) => {
//console.log("GOT APP RESPONSE", blob); //console.log("GOT APP RESPONSE", blob);
if (blob.V0.FileMeta) { if (blob.V0.FileMeta) {
content_type = blob.V0.FileMeta.content_type; content_type = blob.V0.FileMeta.content_type;
if (only_img && !content_type.startsWith("image/")) {
resolve(true);
return true;// to cancel
}
final_blob = new Blob([], { type: content_type }); final_blob = new Blob([], { type: content_type });
} else if (blob.V0.FileBinary) { } else if (blob.V0.FileBinary) {
if (blob.V0.FileBinary.byteLength > 0) { if (blob.V0.FileBinary.byteLength > 0) {

@ -428,7 +428,7 @@ export const save = async () => {
} }
export const all_files_count = derived(cur_tab, ($cur_tab) => { export const all_files_count = derived(cur_tab, ($cur_tab) => {
let total = $cur_tab.branch.attachments + $cur_tab.branch.files; let total = $cur_tab.branch.files;
return total ? `(${total})` : ""; return total ? `(${total})` : "";
}); });

@ -328,7 +328,7 @@ impl IServerBroker for ServerBroker {
{ {
let mut state = self.state.write().await; let mut state = self.state.write().await;
if state.wallet_rendezvous.contains_key(&rendezvous) { if state.wallet_rendezvous.contains_key(&rendezvous) {
let _ = sender.send(Err(ServerError::BrokerError)); let _ = sender.send(Err(ServerError::BrokerError)).await;
sender.close_channel(); sender.close_channel();
return receiver; return receiver;
} else { } else {

@ -187,6 +187,9 @@ pub struct NuriV0 {
} }
impl NuriV0 { impl NuriV0 {
pub fn copy_target_from(&mut self, nuri: &NuriV0) {
self.target = nuri.target.clone();
}
pub fn commit_graph_name(commit_id: &ObjectId, overlay_id: &OverlayId) -> String { pub fn commit_graph_name(commit_id: &ObjectId, overlay_id: &OverlayId) -> String {
format!("{DID_PREFIX}:c:{commit_id}:v:{overlay_id}") format!("{DID_PREFIX}:c:{commit_id}:v:{overlay_id}")
} }
@ -261,6 +264,20 @@ impl NuriV0 {
}) })
} }
pub fn new_from_obj_ref(obj_ref: &ObjectRef) -> Self {
Self {
identity: None,
target: NuriTargetV0::None,
entire_store: false,
object: Some(obj_ref.id),
branch: None,
overlay: None,
access: vec![NgAccessV0::Key(obj_ref.key.clone())],
topic: None,
locator: vec![],
}
}
pub fn new_private_store_target() -> Self { pub fn new_private_store_target() -> Self {
Self { Self {
identity: None, identity: None,
@ -319,7 +336,7 @@ impl NuriV0 {
let key = decode_sym_key(k)?; let key = decode_sym_key(k)?;
Ok(Self { Ok(Self {
identity: None, identity: None,
target: NuriTargetV0::PrivateStore, target: NuriTargetV0::None,
entire_store: false, entire_store: false,
object: Some(id), object: Some(id),
branch: None, branch: None,

@ -29,6 +29,7 @@ getrandom = { version = "0.1.1", features = ["wasm-bindgen"] }
rand = { version = "0.7", features = ["getrandom"] } rand = { version = "0.7", features = ["getrandom"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
sys-locale = { version = "0.3.1", features = ["js"] } sys-locale = { version = "0.3.1", features = ["js"] }
futures = "0.3.24"
ng-repo = { path = "../ng-repo" } ng-repo = { path = "../ng-repo" }
ng-net = { path = "../ng-net" } ng-net = { path = "../ng-net" }
ng-client-ws = { path = "../ng-client-ws" } ng-client-ws = { path = "../ng-client-ws" }

@ -23,6 +23,8 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// use js_sys::Reflect; // use js_sys::Reflect;
use async_std::stream::StreamExt; use async_std::stream::StreamExt;
use futures::channel::mpsc;
use futures::SinkExt;
use js_sys::{Array, Object}; use js_sys::{Array, Object};
use oxrdf::Triple; use oxrdf::Triple;
use sys_locale::get_locales; use sys_locale::get_locales;
@ -40,7 +42,7 @@ use ng_net::broker::*;
use ng_net::types::{BindAddress, ClientInfo, ClientInfoV0, ClientType, CreateAccountBSP, IP}; use ng_net::types::{BindAddress, ClientInfo, ClientInfoV0, ClientType, CreateAccountBSP, IP};
use ng_net::utils::{ use ng_net::utils::{
decode_invitation_string, parse_ip_and_port_for, retrieve_local_bootstrap, retrieve_local_url, decode_invitation_string, parse_ip_and_port_for, retrieve_local_bootstrap, retrieve_local_url,
spawn_and_log_error, Receiver, ResultSend, spawn_and_log_error, Receiver, ResultSend, Sender,
}; };
use ng_net::{actor::*, actors::admin::*}; use ng_net::{actor::*, actors::admin::*};
use ng_net::{WS_PORT, WS_PORT_REVERSE_PROXY}; use ng_net::{WS_PORT, WS_PORT_REVERSE_PROXY};
@ -51,6 +53,7 @@ use ng_wallet::types::*;
use ng_wallet::*; use ng_wallet::*;
use nextgraph::local_broker::*; use nextgraph::local_broker::*;
use nextgraph::verifier::CancelFn;
use crate::model::*; use crate::model::*;
@ -811,25 +814,76 @@ pub async fn test() {
log_debug!("{:?}", client_info); log_debug!("{:?}", client_info);
} }
// #[wasm_bindgen]
// pub async fn app_request_stream_with_nuri_command(
// nuri: String,
// command: JsValue,
// session_id: JsValue,
// callback: &js_sys::Function,
// payload: JsValue,
// ) -> Result<JsValue, String> {
// let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
// .map_err(|_| "Deserialization error of session_id".to_string())?;
// let nuri = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?;
// let command = serde_wasm_bindgen::from_value::<AppRequestCommandV0>(command)
// .map_err(|_| "Deserialization error of AppRequestCommandV0".to_string())?;
// let payload = if !payload.is_undefined() && payload.is_object() {
// Some(AppRequestPayload::V0(
// serde_wasm_bindgen::from_value::<AppRequestPayloadV0>(payload)
// .map_err(|_| "Deserialization error of AppRequestPayloadV0".to_string())?,
// ))
// } else {
// None
// };
// let request = AppRequest::V0(AppRequestV0 {
// session_id,
// command,
// nuri,
// payload,
// });
// app_request_stream_(request, callback).await
// }
// #[wasm_bindgen]
// pub async fn app_request_stream(
// // js_session_id: JsValue,
// request: JsValue,
// callback: &js_sys::Function,
// ) -> Result<JsValue, String> {
// let request = serde_wasm_bindgen::from_value::<AppRequest>(request)
// .map_err(|_| "Deserialization error of AppRequest".to_string())?;
// app_request_stream_(request, callback).await
// }
#[wasm_bindgen] #[wasm_bindgen]
pub async fn app_request_stream( pub async fn app_request_stream(
// js_session_id: JsValue, // js_session_id: JsValue,
request: JsValue, request: JsValue,
callback: &js_sys::Function, callback: &js_sys::Function,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
// let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(js_session_id)
// .map_err(|_| "Deserialization error of session_id".to_string())?;
let request = serde_wasm_bindgen::from_value::<AppRequest>(request) let request = serde_wasm_bindgen::from_value::<AppRequest>(request)
.map_err(|_| "Deserialization error of AppRequest".to_string())?; .map_err(|_| "Deserialization error of AppRequest".to_string())?;
app_request_stream_(request, callback).await
}
async fn app_request_stream_(
request: AppRequest,
callback: &js_sys::Function,
) -> Result<JsValue, String> {
let (reader, cancel) = nextgraph::local_broker::app_request_stream(request) let (reader, cancel) = nextgraph::local_broker::app_request_stream(request)
.await .await
.map_err(|e: NgError| e.to_string())?; .map_err(|e: NgError| e.to_string())?;
let (canceller_tx, canceller_rx) = mpsc::unbounded();
async fn inner_task( async fn inner_task(
mut reader: Receiver<AppResponse>, mut reader: Receiver<AppResponse>,
callback: js_sys::Function, callback: js_sys::Function,
mut canceller_tx: Sender<()>,
) -> ResultSend<()> { ) -> ResultSend<()> {
while let Some(app_response) = reader.next().await { while let Some(app_response) = reader.next().await {
let app_response = nextgraph::verifier::prepare_app_response_for_js(app_response)?; let app_response = nextgraph::verifier::prepare_app_response_for_js(app_response)?;
@ -881,10 +935,27 @@ pub async fn app_request_stream(
Ok(jsval) => { Ok(jsval) => {
let promise_res: Result<js_sys::Promise, JsValue> = jsval.dyn_into(); let promise_res: Result<js_sys::Promise, JsValue> = jsval.dyn_into();
match promise_res { match promise_res {
Ok(promise) => { Ok(promise) => match JsFuture::from(promise).await {
let _ = JsFuture::from(promise).await; Ok(js_value) => {
if js_value == JsValue::TRUE {
log_debug!("cancel because true");
reader.close();
canceller_tx.send(()).await;
canceller_tx.close_channel();
break;
}
} }
Err(_) => {} Err(_) => {}
},
Err(returned_val) => {
if returned_val == JsValue::TRUE {
log_debug!("cancel because true");
reader.close();
canceller_tx.send(()).await;
canceller_tx.close_channel();
break;
}
}
} }
} }
Err(e) => { Err(e) => {
@ -895,12 +966,23 @@ pub async fn app_request_stream(
Ok(()) Ok(())
} }
spawn_and_log_error(inner_task(reader, callback.clone())); async fn inner_canceller(mut canceller_rx: Receiver<()>, cancel: CancelFn) -> ResultSend<()> {
if let Some(_) = canceller_rx.next().await {
log_info!("cancelling");
cancel();
}
Ok(())
}
spawn_and_log_error(inner_canceller(canceller_rx, cancel));
spawn_and_log_error(inner_task(reader, callback.clone(), canceller_tx.clone()));
let cb = Closure::once(move || { let cb = Closure::once(move || {
log_info!("cancelling"); log_info!("trying to cancel");
//sender.close_channel() //sender.close_channel()
cancel(); canceller_tx.unbounded_send(());
canceller_tx.close_channel();
}); });
//Closure::wrap(Box::new(move |sender| sender.close_channel()) as Box<FnMut(Sender<Commit>)>); //Closure::wrap(Box::new(move |sender| sender.close_channel()) as Box<FnMut(Sender<Commit>)>);
let ret = cb.as_ref().clone(); let ret = cb.as_ref().clone();
@ -922,6 +1004,43 @@ pub async fn app_request(request: JsValue) -> Result<JsValue, String> {
Ok(serde_wasm_bindgen::to_value(&response).unwrap()) Ok(serde_wasm_bindgen::to_value(&response).unwrap())
} }
#[wasm_bindgen]
pub async fn app_request_with_nuri_command(
nuri: String,
command: JsValue,
session_id: JsValue,
payload: JsValue,
) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let nuri = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?;
let command = serde_wasm_bindgen::from_value::<AppRequestCommandV0>(command)
.map_err(|_| "Deserialization error of AppRequestCommandV0".to_string())?;
let payload = if !payload.is_undefined() && payload.is_object() {
Some(AppRequestPayload::V0(
serde_wasm_bindgen::from_value::<AppRequestPayloadV0>(payload)
.map_err(|_| "Deserialization error of AppRequestPayloadV0".to_string())?,
))
} else {
None
};
let request = AppRequest::V0(AppRequestV0 {
session_id,
command,
nuri,
payload,
});
let response = nextgraph::local_broker::app_request(request)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&response).unwrap())
}
#[wasm_bindgen] #[wasm_bindgen]
pub async fn file_get_from_private_store( pub async fn file_get_from_private_store(
session_id: JsValue, session_id: JsValue,
@ -931,54 +1050,49 @@ pub async fn file_get_from_private_store(
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id) let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?; .map_err(|_| "Deserialization error of session_id".to_string())?;
let nuri = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?; let nuri = NuriV0::new_from(&nuri).map_err(|e| format!("nuri: {}", e.to_string()))?;
let mut request = AppRequest::new(AppRequestCommandV0::FileGet, nuri.clone(), None); let branch_nuri = NuriV0::new_private_store_target();
request.set_session_id(session_id);
let (reader, cancel) = nextgraph::local_broker::app_request_stream(request) file_get_(session_id, nuri, branch_nuri, callback).await
}
#[wasm_bindgen]
pub async fn file_get(
session_id: JsValue,
reference: JsValue,
branch_nuri: String,
callback: &js_sys::Function,
) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let reference: BlockRef = serde_wasm_bindgen::from_value::<BlockRef>(reference)
.map_err(|_| "Deserialization error of file reference".to_string())?;
let branch_nuri =
NuriV0::new_from(&branch_nuri).map_err(|e| format!("branch_nuri: {}", e.to_string()))?;
file_get_(
session_id,
NuriV0::new_from_obj_ref(&reference),
branch_nuri,
callback,
)
.await .await
.map_err(|e: NgError| e.to_string())?; }
async fn inner_task( async fn file_get_(
mut reader: Receiver<AppResponse>, session_id: u64,
callback: js_sys::Function, mut nuri: NuriV0,
) -> ResultSend<()> { branch_nuri: NuriV0,
while let Some(app_response) = reader.next().await { callback: &js_sys::Function,
let response_js = serde_wasm_bindgen::to_value(&app_response).unwrap(); ) -> Result<JsValue, String> {
let this = JsValue::null(); nuri.copy_target_from(&branch_nuri);
match callback.call1(&this, &response_js) {
Ok(jsval) => {
let promise_res: Result<js_sys::Promise, JsValue> = jsval.dyn_into();
match promise_res {
Ok(promise) => {
let _ = JsFuture::from(promise).await;
}
Err(_) => {}
}
}
Err(e) => {
log_err!(
"JS callback for fetch_file_from_private_store failed with {:?}",
e
);
}
}
}
Ok(())
}
spawn_and_log_error(inner_task(reader, callback.clone())); let mut request = AppRequest::new(AppRequestCommandV0::FileGet, nuri, None);
request.set_session_id(session_id);
let cb = Closure::once(move || { app_request_stream_(request, callback).await
log_info!("cancelling");
//sender.close_channel()
cancel();
});
//Closure::wrap(Box::new(move |sender| sender.close_channel()) as Box<FnMut(Sender<Commit>)>);
let ret = cb.as_ref().clone();
cb.forget();
Ok(ret)
} }
async fn do_upload_done( async fn do_upload_done(
@ -1083,13 +1197,12 @@ async fn do_upload_start(session_id: u64, nuri: NuriV0, mimetype: String) -> Res
#[wasm_bindgen] #[wasm_bindgen]
pub async fn upload_start( pub async fn upload_start(
session_id: JsValue, session_id: JsValue,
nuri: JsValue, nuri: String,
mimetype: String, mimetype: String,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id) let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?; .map_err(|_| "Deserialization error of session_id".to_string())?;
let nuri: NuriV0 = serde_wasm_bindgen::from_value::<NuriV0>(nuri) let nuri: NuriV0 = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?;
.map_err(|_| "Deserialization error of nuri".to_string())?;
let upload_id = do_upload_start(session_id, nuri, mimetype).await?; let upload_id = do_upload_start(session_id, nuri, mimetype).await?;
@ -1180,7 +1293,7 @@ pub async fn upload_chunk(
session_id: JsValue, session_id: JsValue,
upload_id: JsValue, upload_id: JsValue,
chunk: JsValue, chunk: JsValue,
nuri: JsValue, nuri: String,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
//log_debug!("upload_chunk {:?}", js_nuri); //log_debug!("upload_chunk {:?}", js_nuri);
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id) let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
@ -1189,8 +1302,7 @@ pub async fn upload_chunk(
.map_err(|_| "Deserialization error of upload_id".to_string())?; .map_err(|_| "Deserialization error of upload_id".to_string())?;
let chunk: serde_bytes::ByteBuf = serde_wasm_bindgen::from_value::<serde_bytes::ByteBuf>(chunk) let chunk: serde_bytes::ByteBuf = serde_wasm_bindgen::from_value::<serde_bytes::ByteBuf>(chunk)
.map_err(|_| "Deserialization error of chunk".to_string())?; .map_err(|_| "Deserialization error of chunk".to_string())?;
let nuri: NuriV0 = serde_wasm_bindgen::from_value::<NuriV0>(nuri) let nuri: NuriV0 = NuriV0::new_from(&nuri).map_err(|e| e.to_string())?;
.map_err(|_| "Deserialization error of nuri".to_string())?;
let response = do_upload_chunk(session_id, upload_id, chunk, nuri).await?; let response = do_upload_chunk(session_id, upload_id, chunk, nuri).await?;

@ -109,6 +109,7 @@ impl Verifier {
spawn_and_log_error(sending_loop(Arc::new(file), tx.clone())); spawn_and_log_error(sending_loop(Arc::new(file), tx.clone()));
let fnonce = Box::new(move || { let fnonce = Box::new(move || {
log_debug!("FileGet cancelled");
tx.close_channel(); tx.close_channel();
}); });
Ok((rx, fnonce)) Ok((rx, fnonce))

@ -56,9 +56,38 @@
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NextGraph</title> <title>NextGraph</title>
<style>
.splashing {
height: 95vh;display: grid;
}
</style>
</head> </head>
<body> <body>
<div id="splash" class="splashing">
<div style="margin: auto; display: grid;">
<svg
style="width:100px;height:100px;margin: 0 auto 20px ;"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 225 225"
>
<g>
<circle
r="106.98013"
cy="112.90476"
cx="109.88096"
style="fill:#ffffff;stroke:none;stroke-width:0.268375" />
<path
d="M 98.343352,190.26108 C 80.403778,187.53354 65.011938,179.57839 52.608228,166.62327 38.602093,151.99448 31.178059,133.41381 31.178059,112.98841 c 0,-10.21889 1.700058,-19.44396 5.221234,-28.332119 4.28678,-10.820699 10.037295,-19.39063 18.535095,-27.62263 4.72982,-4.58187 6.60687,-6.10643 11.28099,-9.16256 11.89869,-7.779841 24.173884,-11.879991 38.095802,-12.724761 19.80437,-1.2017 39.11165,5.11306 54.60284,17.858751 1.50718,1.24006 2.72951,2.35934 2.71628,2.48729 -0.0132,0.12795 -3.85821,3.63443 -8.54442,7.79217 -4.6862,4.157729 -10.04724,8.96276 -11.91342,10.677819 -1.86617,1.715071 -3.54094,3.11831 -3.7217,3.11831 -0.18075,0 -1.39985,-0.745188 -2.70911,-1.655969 -7.53011,-5.23834 -15.99428,-7.82188 -25.62597,-7.82188 -12.731628,0 -23.249192,4.3379 -32.143882,13.257541 -6.39594,6.413868 -10.70387,14.555268 -12.50018,23.623578 -0.69099,3.48832 -0.68968,13.53072 0.002,17.00893 3.70508,18.62577 18.31886,33.10194 36.642322,36.29729 4.16439,0.72621 11.98099,0.71223 15.98975,-0.0286 14.03187,-2.59311 25.86047,-11.36806 32.26533,-23.93578 0.77379,-1.51834 1.26018,-2.88461 1.08086,-3.03616 -0.17934,-0.15156 -6.87448,-1.1779 -14.87813,-2.28078 -9.7795,-1.34758 -14.92353,-2.21379 -15.68471,-2.64117 -1.52067,-0.85379 -2.83611,-2.88806 -2.83611,-4.3859 0,-1.1732 2.02687,-15.86876 2.49085,-18.05962 0.29676,-1.40127 2.42559,-3.4934 3.84317,-3.77691 0.62227,-0.12445 8.82712,0.85555 18.28065,2.18348 9.43343,1.32511 17.26269,2.29453 17.39833,2.15427 0.13566,-0.14026 1.11808,-6.54833 2.18313,-14.24014 1.10778,-8.000208 2.20407,-14.60184 2.56177,-15.426229 0.34392,-0.792599 1.11019,-1.849131 1.70287,-2.34782 2.06321,-1.736079 3.1433,-1.785011 12.20439,-0.55291 9.63637,1.310309 10.70873,1.56224 12.28077,2.88503 1.64359,1.382979 2.2732,2.810909 2.25906,5.123309 -0.007,1.10173 -0.92172,8.29645 -2.03332,15.98826 -1.11158,7.69182 -1.97159,14.04091 -1.91113,14.1091 0.0605,0.0682 7.16644,1.11143 15.79109,2.31832 11.10566,1.55407 16.00827,2.38757 16.80223,2.85657 1.53015,0.90389 2.48023,2.64785 2.45017,4.49756 -0.0462,2.84349 -2.41252,18.12279 -2.97521,19.21089 -0.66164,1.27949 -2.60244,2.54696 -3.92109,2.56074 -0.51973,0.005 -7.87449,-0.95937 -16.34391,-2.144 -8.46944,-1.18464 -15.47588,-2.077 -15.56986,-1.98301 -0.094,0.094 -1.18792,7.34163 -2.43097,16.10589 -1.44004,10.15311 -2.49792,16.43621 -2.91556,17.31631 -0.72531,1.52848 -2.76261,3.06291 -4.53817,3.41802 -0.95688,0.19138 -10.90014,-0.92798 -13.59859,-1.53084 -0.5471,-0.12223 -1.89146,0.67252 -4.50941,2.66588 -11.2627,8.57562 -24.34195,13.90917 -38.35741,15.64164 -4.40038,0.54395 -15.72658,0.43298 -19.853658,-0.19451 z"
style="fill:#4972a5;fill-opacity:1;stroke:#4972a5;stroke-width:0.377976;stroke-opacity:1" />
</g>
</svg>
<br/>
<noscript style="text-align: center;">
NextGraph cannot load as Javascript is deactivated
</noscript>
</div>
</div>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

Loading…
Cancel
Save