Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: enable better sharing of TDs with lz-string #89

Merged
merged 3 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 52 additions & 19 deletions src/components/App/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,67 @@
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/
import React, { useEffect } from 'react';
import './App.css';
import TDViewer from '../TDViewer/TDViewer'
import React, { useContext, useEffect } from 'react';
import ediTDorContext from "../../context/ediTDorContext";
import GlobalState from '../../context/GlobalState';
import JSONEditorComponent from "../Editor/Editor";
import AppHeader from './AppHeader/AppHeader';
import TDViewer from '../TDViewer/TDViewer';
import './App.css';
import AppFooter from './AppFooter';
import GlobalState from '../../context/GlobalState';
import AppHeader from './AppHeader/AppHeader';

import '../../assets/main.css'
import '../../assets/main.css';
import { decompressSharedTd as decompressAndParseSharedTd } from '../../share';

const App = (props) => {
useEffect(() => { dragElement(document.getElementById("separator"), "H"); }, [props])
const GlobalStateWrapper = (props) => {
return (
<GlobalState>
<main className="h-full w-screen flex flex-col">
<AppHeader></AppHeader>
<div className="flex-grow splitter flex flex-row w-full height-adjust">
<div className="w-7/12" id="second"><TDViewer /></div>
<div id="separator"></div>
<div className="w-5/12" id="first"><JSONEditorComponent /></div>
</div>
<AppFooter></AppFooter>
<div id="modal-root"></div>
</main>
<App />
</GlobalState>
);
}

// The useEffect hook for checking the URI was called twice somehow.
// This variable prevents the callback from being executed twice.
let checkedUrl = false;
const App = (props) => {
const context = useContext(ediTDorContext);

useEffect(() => { dragElement(document.getElementById("separator"), "H"); }, [props])

useEffect(() => {
if (checkedUrl || window.location.search.indexOf("td") <= -1) {
return;
}
checkedUrl = true;

const url = new URL(window.location.href);
const compressedTd = url.searchParams.get("td");
if (compressedTd == null) return;

const td = decompressAndParseSharedTd(compressedTd);
if (td === undefined) {
alert("The TD found in the URLs path couldn't be reconstructed.");
return;
};

context.updateOfflineTD(JSON.stringify(td, null, 2));
}, [context]);

return (
<main className="h-full w-screen flex flex-col">
<AppHeader></AppHeader>
<div className="flex-grow splitter flex flex-row w-full height-adjust">
<div className="w-7/12" id="second"><TDViewer /></div>
<div id="separator"></div>
<div className="w-5/12" id="first"><JSONEditorComponent /></div>
</div>
<AppFooter></AppFooter>
<div id="modal-root"></div>
</main>
);
}


/**
*
Expand Down Expand Up @@ -84,4 +117,4 @@ const dragElement = (element, direction) => {
element.onmousedown = onMouseDown;
}

export default App;
export default GlobalStateWrapper;
35 changes: 1 addition & 34 deletions src/components/App/AppHeader/AppHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import logo from "../../../assets/editdor.png";
import "../../../assets/main.css";
import wot from "../../../assets/WoT.png";
import ediTDorContext from "../../../context/ediTDorContext";
import { getFileHandle, getFileHTML5, _readFileHTML5 } from "../../../util.js";
import { getFileHandle, getFileHTML5, isThingModel, _readFileHTML5 } from "../../../util.js";
import { ConvertTmDialog } from "../../Dialogs/ConvertTmDialog";
import { CreateTdDialog } from "../../Dialogs/CreateTdDialog";
import { ShareDialog } from "../../Dialogs/ShareDialog";
Expand All @@ -25,24 +25,6 @@ import Button from "./Button";
export default function AppHeader() {
const context = useContext(ediTDorContext);

/**
* @param {Object} td
* @returns {boolean}
*/
function isThingModel(td) {
try {
td = JSON.parse(td);
} catch {
return false;
}

if (!td.hasOwnProperty("@type")) {
return false;
}

return td["@type"].indexOf("ThingModel") > -1;
}

/**
* Check if the Browser Supports the new Native File System Api (Chromium 86.0)
*/
Expand Down Expand Up @@ -250,21 +232,6 @@ export default function AppHeader() {
return await window.chooseFileSystemEntries(opts);
};

useEffect(() => {
if (window.location.search.indexOf("td") > -1) {
const url = new URL(window.location.href);
const td = url.searchParams.get("td");
try {
const parsedTD = JSON.parse(td);
context.updateOfflineTD(JSON.stringify(parsedTD, null, 2));
} catch (error) {
alert('Sorry, we were unable to parse the TD given in the URL');
}
}
//because the GET Param should be only loaded once, the next line was added
// eslint-disable-next-line
}, []);

useEffect(() => {
const shortcutHandler = (e) => {
if (
Expand Down
9 changes: 5 additions & 4 deletions src/components/Dialogs/ConvertTmDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, { forwardRef, useContext, useEffect, useImperativeHandle } from 'r
import ReactDOM from "react-dom";
import ediTDorContext from "../../context/ediTDorContext";
import { DialogTemplate } from "./DialogTemplate";
import { compress } from "../../external/TdPlayground"

export const ConvertTmDialog = forwardRef((props, ref) => {
const context = useContext(ediTDorContext);
Expand Down Expand Up @@ -99,7 +100,7 @@ const createHtmlInputs = (td) => {
const properties = Object.keys(parsed["properties"] ? parsed["properties"] : {});
const actions = Object.keys(parsed["actions"] ? parsed["actions"] : {});
const events = Object.keys(parsed["events"] ? parsed["events"] : {});
const requiredFields = {"properties": [], "actions": [], "events": []};
const requiredFields = { "properties": [], "actions": [], "events": [] };

// Parse the required interaction affordances
if (parsed["tm:required"]) {
Expand Down Expand Up @@ -138,7 +139,7 @@ const createHtmlInputs = (td) => {
htmlActions = createAffordanceHtml("actions", actions);
htmlEvents = createAffordanceHtml("events", events);

} catch (ignored) {}
} catch (ignored) { }

const divider = (
<h2 key="modalDividerText" className="text-gray-400 pb-2 pt-4">
Expand All @@ -162,7 +163,7 @@ const convertTmToTd = (td, htmlInputs) => {
// Process the ticked affordances and save them in respective arrays
for (const item of htmlInputs) {
if (item.props.className.indexOf("form-checkbox") > -1 &&
document.getElementById(item.props.children[0].props.id).checked) {
document.getElementById(item.props.children[0].props.id).checked) {
if (item.props.children[0].props["data-interaction"] === "properties")
properties.push(item["key"].split("/")[1]);
else if (item.props.children[0].props["data-interaction"] === "actions")
Expand Down Expand Up @@ -227,6 +228,6 @@ const convertTmToTd = (td, htmlInputs) => {
delete parse["@type"];
delete parse["tm:required"];

let permalink = `${window.location.origin+window.location.pathname}?td=${encodeURIComponent(JSON.stringify(parse))}`;
let permalink = `${window.location.origin + window.location.pathname}?td=${compress(JSON.stringify(parse))}`;
window.open(permalink, "_blank");
}
75 changes: 35 additions & 40 deletions src/components/Dialogs/ShareDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/
import React, { forwardRef, useContext, useEffect, useImperativeHandle } from 'react';
import React, { forwardRef, useContext, useImperativeHandle } from 'react';
import ReactDOM from "react-dom";
import ediTDorContext from "../../context/ediTDorContext";
import { prepareTdForSharing } from '../../share';
import { DialogTemplate } from "./DialogTemplate";

export const ShareDialog = forwardRef((props, ref) => {
const context = useContext(ediTDorContext);
const [display, setDisplay] = React.useState(() => { return false });

useEffect(() => {
if (display === true) {
copyLinkToClipboard(createPermalink(context.offlineTD));
focusPermalinkField()
}
}, [display, context]);
const [display, setDisplay] = React.useState(false);
const [compressedTdLink, setCompressedTdLink] = React.useState("");
const [compressedTd, setCompressedTd] = React.useState("");

useImperativeHandle(ref, () => {
return {
Expand All @@ -35,21 +31,41 @@ export const ShareDialog = forwardRef((props, ref) => {

const open = () => {
setDisplay(true)
};

const close = () => {
setDisplay(false);
const tmpCompressedTd = prepareTdForSharing(context.offlineTD);
setCompressedTd(tmpCompressedTd);

const tmpCompressedTdLink = `${window.location.origin + window.location.pathname}?td=${tmpCompressedTd}`;
setCompressedTdLink(tmpCompressedTdLink);

copyLinkToClipboard(tmpCompressedTdLink);
focusPermalinkField();
};

const urlField = createPermalinkField(context.offlineTD);
const close = () => { setDisplay(false); };

let child = <input
type="text"
name="share-td-field"
id="share-td-field"
className="border-gray-600 bg-gray-600 w-full p-2 sm:text-sm border-2 text-white rounded-md focus:outline-none focus:border-blue-500"
defaultValue={compressedTdLink}
/>

if (display) {
return ReactDOM.createPortal(
<DialogTemplate
onCancel={close}
cancelText={"Close"}
hasSubmit={false}
children={urlField}
hasSubmit={true}
onSubmit={close}
submitText={"Okay"}
cancelText={"Open in Playground"}
onCancel={
() => {
window.open(`http://plugfest.thingweb.io/playground/#${compressedTd}`);
close();
}
}
children={child}
title={"Share This TD"}
description={"A link to this TD was copied to your clipboard."}
/>,
Expand All @@ -59,30 +75,9 @@ export const ShareDialog = forwardRef((props, ref) => {
return null;
});

const createPermalink = (td) => {
let parsedTD = {};
try {
parsedTD = JSON.parse(td);
} catch (_) { }

return `${window.location.origin+window.location.pathname}?td=${encodeURIComponent(
JSON.stringify(parsedTD)
)}`;
}

const createPermalinkField = (td) => {
return (<input
type="text"
name="share-td-field"
id="share-td-field"
className="border-gray-600 bg-gray-600 w-full p-2 sm:text-sm border-2 text-white rounded-md focus:outline-none focus:border-blue-500"
defaultValue={createPermalink(td)}
/>);
};

const copyLinkToClipboard = (link) => {
const copyLinkToClipboard = (compressedTdLink) => {
if (document.hasFocus()) {
navigator.clipboard.writeText(link).then(
navigator.clipboard.writeText(compressedTdLink).then(
function () {
console.log("Async: Copied TD link to clipboard!");
},
Expand Down
59 changes: 59 additions & 0 deletions src/share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { compress, decompress } from "./external/TdPlayground"
import { isThingModel } from "./util"

const tdPrefix = "tdjson";
const tmPrefix = "tmjson";

/**
*
* @param {string} td
* @returns {string | undefined}
*
* @description
* prepareTdForSharing takes a TD/TM string and tries to compress
* it for sharing. If the string is no valid JSON, this function will
* return undefined.
*/
export const prepareTdForSharing = (td) => {
let tdJSON;
try {
tdJSON = JSON.parse(td);
} catch (e) {
console.debug(e);
return undefined
}

let prefix = tdPrefix;
if (isThingModel(tdJSON)) {
prefix = tmPrefix;
}

const compressedTD = compress(prefix.concat(td));
return compressedTD;
}

/**
*
* @param {string} lzString
* @returns {object | undefined}
*
* @description
* decompressSharedTd takes a lz string as input, then tries
* to decompress and parse it as a TD/TM, which it returns.
* If any of these operations fail, this function returns undefined.
*/
export const decompressSharedTd = (lzString) => {
let decompressedTd = decompress(lzString);
if (decompressedTd == null || decompressedTd === "") {
return undefined;
};

decompressedTd = decompressedTd.substring(6);
try {
return JSON.parse(decompressedTd);
} catch (e) {
console.debug(e);
}

return undefined;
}
Loading