diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..b34e8d6b
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1 @@
+{ "tabWidth": 4, "trailingComma": "es5", "arrowParens": "avoid" }
diff --git a/.travis.yml b/.travis.yml
index c3ea3ee6..579ac033 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: node_js
node_js:
- - 10.16.2
+ - lts/dubnium
os:
- linux
dist: bionic
@@ -11,7 +11,7 @@ before_install:
- curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
- echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- sudo apt-get update && sudo apt-get install --no-install-recommends yarn
- - nvm install 10.16.2 && nvm use 10.16.2 && npm install -g yarn
+ - nvm install lts/dubnium && nvm use lts/dubnium && npm install -g yarn
- yarn install
cache:
apt: true
diff --git a/README.md b/README.md
index 37dfdf1e..456959d5 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-## Private Share
+## pShare
### Software Objectives:
- Privately and securely share data with friends, family, and business associates.
@@ -91,6 +91,32 @@ New binaries can be created by following https://github.com/HiddenField/dynamic-
Information about the architecture of the application can be found in [this document](documentation/electron-redux-architecture.md)
+### Talking to an installed pShare's instance of dynamicd
+
+#### Linux *.deb*-based installation from `bash`
+
+In the terminal, it is possible to make an alias `dyncli` to point to the running dynamicd:
+
+```shell
+$ alias dyncli='/opt/pShare/resources/static/dynamicd/linux/dynamic-cli "-conf=$HOME/.pshare/.dynamic/dynamic.conf" "-datadir=$HOME/.pshare/.dynamic"'
+```
+
+then we can use it to issue RPC commands against the running `dynamicd` as follows:
+
+```shell
+$ dyncli syncstatus
+```
+
+#### Windows-based installation from `cmd`
+
+In windows, we can use the following:
+
+```shell
+> "%LOCALAPPDATA%\Programs\pshare\resources\static\dynamicd\win32\dynamic-cli.exe" "-conf=%USERPROFILE%\.pshare\.dynamic\dynamic.conf" "-datadir=%USERPROFILE%\.pshare\.dynamic" syncstatus
+```
+
+to do the same thing. No `alias` command to ease usage, unfortunately.
+
### License
See LICENSE.md file for copying and use information.
\ No newline at end of file
diff --git a/getVersion.ts b/getVersion.ts
new file mode 100644
index 00000000..fbea28b4
--- /dev/null
+++ b/getVersion.ts
@@ -0,0 +1 @@
+export const version: string = require('./package.json').version
\ No newline at end of file
diff --git a/package.json b/package.json
index 206a7f77..7711b92a 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,18 @@
{
"name": "pshare",
- "version": "0.5.8",
+ "version": "1.0.0",
"description": "Secure and private peer to peer file sharing",
"homepage": "https://duality.solutions/pshare",
- "license": "UNLICENSED",
+ "license": "SEE LICENSE IN LICENSE.md",
"author": {
"name": "HiddenField",
"email": "info@hiddenfield.com",
"url": "https://duality.solutions/pshare"
},
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/duality-solutions/pShare.git"
+ },
"build": {
"appId": "solutions.duality.pshare",
"productName": "pShare",
@@ -17,9 +21,7 @@
},
"linux": {
"target": [
- "AppImage",
- "deb",
- "tar.gz"
+ "deb"
]
}
},
@@ -47,7 +49,10 @@
"is-in-subnet": "^1.9.0",
"mime-types": "^2.1.22",
"pify": "^4.0.1",
+ "progress-stream": "^2.0.0",
+ "qrcode-generator": "^1.4.3",
"react": "^16.8.6",
+ "react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.8.6",
"react-dropzone": "^10.0.0",
"react-redux": "^5.1.1",
@@ -65,6 +70,7 @@
"seedrandom": "^2.4.4",
"sjcl-git": "https://github.com/bitwiseshiftleft/sjcl.git#1.0.8",
"source-map-support": "^0.5.9",
+ "stream-buffers": "^3.0.2",
"styled-components": "^4.2.0",
"symbol-observable": "^1.2.0",
"ts-deep-equal": "^1.1.1",
@@ -86,6 +92,7 @@
"@types/jest": "^23.3.10",
"@types/mime-types": "^2.1.0",
"@types/pify": "^3.0.2",
+ "@types/progress-stream": "^2.0.0",
"@types/react": "^16.7.9",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.10",
@@ -93,6 +100,7 @@
"@types/react-router-dom": "^4.3.1",
"@types/react-transition-group": "1.1.5",
"@types/seedrandom": "^2.4.27",
+ "@types/stream-buffers": "^3.0.3",
"@types/styled-components": "^4.1.6",
"@types/uuid": "^3.4.4",
"@types/webdriverio": "^4.13.0",
@@ -100,10 +108,10 @@
"electron-builder": "^20.36.2",
"electron-webpack": "^2.6.1",
"electron-webpack-ts": "^3.1.0",
- "jest": "^23.6.0",
+ "jest": "^24.9.0",
"redux-saga-test-plan": "^3.7.0",
"spectron": "^5.0.0",
- "ts-jest": "^23.10.5",
+ "ts-jest": "^24.0.2",
"tslint": "^5.11.0",
"webdriverio": "^4.14.1",
"webpack": "^4.26.0",
diff --git a/src/main/getRpcClient.ts b/src/main/getRpcClient.ts
index 88c1f53a..eec18c4c 100644
--- a/src/main/getRpcClient.ts
+++ b/src/main/getRpcClient.ts
@@ -40,7 +40,7 @@ async function createRpcClient(cancellationToken: CancellationToken): Promise<{
const client = await createJsonRpcClient({
host: "localhost",
- port: "33650",
+ port: "33350",
username: processInfo.rpcUser,
password: processInfo.rpcPassword,
timeout: 20000,
diff --git a/src/main/index.ts b/src/main/index.ts
index 0c6983ce..25adbc24 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -15,8 +15,8 @@ import { installExtensionsAsync } from './installExtensionsAsync';
import { configureStore } from './store';
import { install as installDevtron } from 'devtron'
import { AppActions } from '../shared/actions/app';
-
import { divertConsoleToLogger } from './system/divertConsoleToLogger';
+import { version } from '../../getVersion';
declare module 'electron' {
@@ -56,7 +56,7 @@ if (!hasLock) {
const persistencePaths = ['user.syncAgreed', 'user.userName', 'user.accountCreationTxId', 'user.accountCreated', 'rtcConfig'];
let mainWindow: BrowserWindow | null
let rtcWindow: BrowserWindow | null
-
+ let aboutPanelWindow: BrowserWindow | null
const store = configureStore(() => mainWindow, persistencePaths)
store.getState();
@@ -93,13 +93,102 @@ if (!hasLock) {
return window
}
- function createMainWindow() {
- const window = new BrowserWindow({ width: 1024, height: 768 })
-
-
+ function createAboutPanelWindow() {
+ const window = new BrowserWindow({ width: 420, height: 300, resizable:false});
+ window.setMenuBarVisibility(false);
+ window.on('closed', () => {
+ aboutPanelWindow = null
+ })
+ const loadView = ( props: { title: string, version: string, }) => {
+
+ return (`
+
+
+
+ ${props.title}
+
+
+
+
+ pShare
+
+
+
+
+
+ p-share-logo-svgs
+
+
+
+
+ Powered by Duality™
+ Think Inside The Block™
+
+ https://duality.solutions/pshare
+
+ Version: ${props.version}
+
+
+
+ `)
+ }
+ const content = 'data:text/html;charset=UTF-8,' + encodeURIComponent(loadView({
+ title: "About pShare",
+ version: version || "Not found",
+ }));
+ window.loadURL(content);
+ return window;
+ }
+ function createMainWindow() {
+ const window = new BrowserWindow({ width: 1024, height: 768 })
console.log(`loading templateUrl : ${templateUrl}`)
window.loadURL(`${templateUrl}?role=renderer`)
@@ -181,12 +270,28 @@ if (!hasLock) {
}
mainWindow = createMainWindow()
rtcWindow = createRtcWindow()
+
setAppMenu(mainWindow);
- if (isDevelopment) {
+ const contextMenu = [
+ {
+ label: 'Cut',
+ role: 'cut'
+ },
+ {
+ label: 'Copy',
+ role: 'copy'
+ },
+ {
+ label: 'Paste',
+ role: 'paste'
+ }
+ ]
+ if (isDevelopment) {
// add inspect element on right click menu
mainWindow.webContents.on('context-menu', (e, props) => {
mainWindow && Menu.buildFromTemplate([
+ ...contextMenu,
{
label: 'Inspect element',
click() {
@@ -208,14 +313,19 @@ if (!hasLock) {
]).popup(mainWindow);
});
}
+ else {
+ mainWindow.webContents.on('context-menu', () => {
+ mainWindow && Menu.buildFromTemplate(contextMenu).popup(mainWindow);
+ })
+ }
})
-
function setAppMenu(mainWindow: BrowserWindow) {
const template = [
{
label: 'Edit',
+ id: 'edit-menu',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
@@ -229,21 +339,39 @@ if (!hasLock) {
]
},
{
+ role: 'Help',
+ id: 'help-menu',
+ submenu: [
+ {
+ label: 'Support',
+ click() { shell.openExternal('https://discord.gg/87be63e')}
+ },
+ {
+ label: 'About pShare',
+ click() {
+ if(aboutPanelWindow) return;
+ aboutPanelWindow = createAboutPanelWindow()
+ }
+ }
+ ]
+ }
+ ];
+ if (isDevelopment) {
+ template.push({
label: 'View',
+ id: 'view-menu',
submenu: [
{ role: 'reload' },
{ role: 'forcereload' },
{
label: 'Reset redux store',
async click() {
-
mainWindow && await mainWindow.webContents.executeJavaScript("window.resetStore && window.resetStore()")
}
},
{
label: 'Toggle RTC window devtools',
click() {
-
rtcWindow && rtcWindow.webContents.openDevTools({ mode: "detach" });
}
},
@@ -255,29 +383,20 @@ if (!hasLock) {
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
- },
- {
- role: 'window',
- submenu: [
- { role: 'minimize' },
- { role: 'close' }
- ]
- },
- {
- role: 'help',
- submenu: [
- {
- label: 'Learn More',
- click() { shell.openExternal('https://electronjs.org'); }
- }
- ]
- }
- ];
+ })
+ }
+
if (process.platform === 'darwin') {
template.unshift({
- label: app.getName(),
+ label: 'pShare',
submenu: [
- { role: 'about' },
+ {
+ label: 'About pShare',
+ click() {
+ if(aboutPanelWindow) return;
+ aboutPanelWindow = createAboutPanelWindow()
+ }
+ },
{ type: 'separator' },
{ role: 'services', submenu: [] },
{ type: 'separator' },
@@ -300,13 +419,13 @@ if (!hasLock) {
// }
// )
// Window menu
- template[3].submenu = [
- { role: 'close' },
- { role: 'minimize' },
- { role: 'zoom' },
- { type: 'separator' },
- { role: 'front' }
- ];
+ // template[3].submenu = [
+ // { role: 'close' },
+ // { role: 'minimize' },
+ // { role: 'zoom' },
+ // { type: 'separator' },
+ // { role: 'front' }
+ // ];
}
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
diff --git a/src/main/sagas/addFileSaga.ts b/src/main/sagas/addFileSaga.ts
index ee4f673b..5e95a3d1 100644
--- a/src/main/sagas/addFileSaga.ts
+++ b/src/main/sagas/addFileSaga.ts
@@ -2,42 +2,122 @@ import { takeEvery, select, call, put } from "redux-saga/effects";
import { getType, ActionType } from "typesafe-actions";
import { AddFileActions } from "../../shared/actions/addFile";
import * as fsExtra from 'fs-extra'
+import * as fs from 'fs';
import * as path from 'path'
import { app } from "electron";
import { MainRootState } from "../reducers";
+import { FilePathInfo } from "../../shared/types/FilePathInfo";
+import { getAllFilePaths } from "../system/getAllFilePaths";
+import { blinq } from "blinq";
const pathToShareDirectory = path.join(app.getPath("home"), ".pshare", "share");
export function* addFileSaga() {
yield takeEvery(getType(AddFileActions.filesSelected), function* (action: ActionType) {
+
+
+ const { files, directories }: SortedFileInfoCollection = yield call(() => sortFileInfosToFileOrDirectories(action.payload))
+
+
+
+
const linkedUserName: string | undefined = yield select((s: MainRootState) => s.sharedFiles.linkedUserName)
if (!linkedUserName) {
return
}
- const targetDirectory = path.join(pathToShareDirectory, linkedUserName, "out")
+ const baseDirectory = path.join(pathToShareDirectory, linkedUserName, "out");
+ const currentPath = yield select((s: MainRootState) => s.fileNavigation.sharedFilesViewPath.join("/"))
+
+ const targetDirectory = path.join(baseDirectory, currentPath)
yield call(() => fsExtra.ensureDir(targetDirectory))
- const filePathInfos = action.payload;
-
- for (const fpi of filePathInfos) {
- console.log("filePathInfo : " + fpi.path)
- const fileName = path.basename(fpi.path)
- const [firstSeg, ...remainingSegs] = fileName.split(".");
- for (let i = 0; ; i++) {
- const dest = path.join(targetDirectory, i === 0 ? fileName : `${firstSeg}(${i})${["", ...remainingSegs].join(".")}`);
-
- try {
- yield call(() => fsExtra.copy(fpi.path, dest, { errorOnExist: true, overwrite: false }))
- } catch (err) {
- if (/ already exists$/.test(err.message)) {
- continue
- }
- throw err
- }
- break
- }
+
+ try {
+ yield* addDroppedFiles(files, targetDirectory)
+ yield* addDroppedDirectories(directories, targetDirectory)
+ } catch (err) {
+ yield put(AddFileActions.failed(err && typeof err.message === "string" ? err.message : "something went wrong"))
}
yield put(AddFileActions.close())
})
}
+interface CopyOperation {
+ src: string
+ dest: string
+}
+
+function* addDroppedDirectories(filePathInfos: FilePathInfo[], targetDirectory: string) {
+ const operations: CopyOperation[] = [];
+ for (const folderInfo of filePathInfos) {
+ const allFilePaths: string[] = yield call(() => getAllFilePaths([folderInfo.path]))
+ const dirName = path.basename(folderInfo.path)
+ // const newDirectoryPath = path.join(targetDirectory, dirName)
+ // const exists: boolean = yield call(() => pathExists(newDirectoryPath))
+ // if (exists) {
+ // throw Error("pathExists")
+ // }
+ const relativePaths = allFilePaths.map(p => ({ absolutePath: p, relativePath: path.join(dirName, path.relative(folderInfo.path, p)) }))
+ const ops = relativePaths.map(({ absolutePath, relativePath }) => ({ src: absolutePath, dest: path.join(targetDirectory, relativePath) }))
+ operations.push(...ops)
+ }
+ const directoriesToEnsure = blinq(operations).select(o => path.dirname(o.dest)).distinct().toArray();
+
+ for (const dp of directoriesToEnsure) {
+ yield call(() => fsExtra.ensureDir(dp))
+ }
+
+ for (const { src, dest } of operations) {
+ yield call(() => fsExtra.copy(src, dest, { errorOnExist: true, overwrite: false }))
+ }
+
+
+}
+// async function pathExists(path: string): Promise {
+// try {
+// await fs.promises.access(path)
+// } catch{
+// return false
+// }
+// return true
+// }
+function* addDroppedFiles(filePathInfos: FilePathInfo[], targetDirectory: string) {
+ for (const fpi of filePathInfos) {
+ console.log("filePathInfo : " + fpi.path)
+ const fileName = path.basename(fpi.path)
+ const [firstSeg, ...remainingSegs] = fileName.split(".");
+ for (let i = 0; ; i++) {
+ const dest = path.join(targetDirectory, i === 0 ? fileName : `${firstSeg}(${i})${["", ...remainingSegs].join(".")}`);
+
+ try {
+ yield call(() => fsExtra.copy(fpi.path, dest, { errorOnExist: true, overwrite: false }))
+ } catch (err) {
+ if (/ already exists$/.test(err.message)) {
+ continue
+ }
+ throw err
+ }
+ break
+ }
+ }
+}
+
+interface SortedFileInfoCollection {
+ files: FilePathInfo[]
+ directories: FilePathInfo[]
+}
+
+async function sortFileInfosToFileOrDirectories(fileInfos: FilePathInfo[]): Promise {
+ const files: FilePathInfo[] = []
+ const directories: FilePathInfo[] = []
+ for (const fileInfo of fileInfos) {
+ const normalizedPath = path.normalize(fileInfo.path);
+ const stat: fs.Stats = await fs.promises.stat(normalizedPath);
+ if (stat.isDirectory()) {
+ directories.push(fileInfo)
+ } else if (stat.isFile()) {
+ files.push(fileInfo)
+ }
+ }
+ return { files, directories }
+}
\ No newline at end of file
diff --git a/src/main/sagas/bdapSaga.ts b/src/main/sagas/bdapSaga.ts
index 4dd8cefd..b748d75e 100644
--- a/src/main/sagas/bdapSaga.ts
+++ b/src/main/sagas/bdapSaga.ts
@@ -1,4 +1,4 @@
-import { takeEvery, put, call, select, take, all, race } from "redux-saga/effects";
+import { takeEvery, put, call, select, take, race } from "redux-saga/effects";
import { getType, ActionType } from "typesafe-actions";
import { BdapActions } from "../../shared/actions/bdap";
import { RpcClient } from "../RpcClient";
@@ -12,165 +12,298 @@ import { blinq } from "blinq";
import { delay } from "redux-saga";
import { LinkDeniedResponse } from "../../dynamicdInterfaces/LinkDeniedResponse";
import { CompleteLink } from "../../dynamicdInterfaces/links/CompleteLink";
+import { tuple } from "../../shared/system/tuple";
+import { getFirstBdapAccount } from "./helpers/getFirstBdapAccount";
+import { BdapAccount } from "../../dynamicdInterfaces/BdapAccount";
+
+type Report = { success: any; failure: any };
+type ResultsReport = Record;
+type Frequency = "repeated" | "once";
+interface BdapOperation {
+ action: any;
+ successAction: any;
+ failureAction: any;
+ frequency: Frequency;
+}
+const operations: Record = {
+ users: {
+ action: () => BdapActions.getUsers(),
+ successAction: BdapActions.getUsersSuccess,
+ failureAction: BdapActions.getUsersFailed,
+ frequency: "repeated",
+ },
+ completeLinks: {
+ action: () => BdapActions.getCompleteLinks(),
+ successAction: BdapActions.getCompleteLinksSuccess,
+ failureAction: BdapActions.getCompleteLinksFailed,
+ frequency: "repeated",
+ },
+ pendingRequestLinks: {
+ action: () => BdapActions.getPendingRequestLinks(),
+ successAction: BdapActions.getPendingRequestLinksSuccess,
+ failureAction: BdapActions.getPendingRequestLinksFailed,
+ frequency: "repeated",
+ },
+ pendingAcceptLinks: {
+ action: () => BdapActions.getPendingAcceptLinks(),
+ successAction: BdapActions.getPendingAcceptLinksSuccess,
+ failureAction: BdapActions.getPendingAcceptLinksFailed,
+ frequency: "repeated",
+ },
+ deniedLinks: {
+ action: () => BdapActions.getDeniedLinks(),
+ successAction: BdapActions.getDeniedLinksSuccess,
+ failureAction: BdapActions.getDeniedLinksFailed,
+ frequency: "repeated",
+ },
+ balance: {
+ action: () => BdapActions.getBalance(),
+ successAction: BdapActions.getBalanceSuccess,
+ failureAction: BdapActions.getBalanceFailed,
+ frequency: "repeated",
+ },
+ topUpAddress: {
+ action: () => BdapActions.getTopUpAddress(),
+ successAction: BdapActions.getTopUpAddressSuccess,
+ failureAction: BdapActions.getTopUpAddressFailed,
+ frequency: "once",
+ },
+};
+
+type OperationCounts = Record;
export function* bdapSaga(rpcClient: RpcClient, mock: boolean = false) {
- yield takeEvery(getType(BdapActions.getUsers), function* () {
-
+ yield takeEvery(getType(BdapActions.getUsers), function*() {
let response: GetUserInfo[];
try {
- response = yield call(() => rpcClient.command("getusers"))
-
+ response = yield call(() => rpcClient.command("getusers"));
} catch (err) {
- yield put(BdapActions.getUsersFailed(err.message))
- return
+ yield put(BdapActions.getUsersFailed(err.message));
+ return;
}
- const currentUserName: string | undefined = yield select((state: MainRootState) => state.user.userName)
- if (typeof currentUserName !== 'undefined') {
- const currentUser = blinq(response).singleOrDefault(r => r.object_id === currentUserName)
- if (typeof currentUser !== 'undefined') {
- yield put(BdapActions.currentUserReceived(currentUser))
+ const currentUserName: string | undefined = yield select(
+ (state: MainRootState) => state.user.userName
+ );
+ if (typeof currentUserName !== "undefined") {
+ const currentUser = blinq(response).singleOrDefault(
+ r => r.object_id === currentUserName
+ );
+ if (typeof currentUser !== "undefined") {
+ yield put(BdapActions.currentUserReceived(currentUser));
}
}
- yield put(BdapActions.getUsersSuccess(response))
- })
- yield takeEvery(getType(BdapActions.getPendingAcceptLinks), function* () {
-
- yield* rpcLinkCommand(rpcClient, (client) => client.command("link", "pending", "accept"), BdapActions.getPendingAcceptLinksSuccess, BdapActions.getPendingAcceptLinksFailed)
- })
- yield takeEvery(getType(BdapActions.getPendingRequestLinks), function* () {
-
- yield* rpcLinkCommand(rpcClient, (client) => client.command("link", "pending", "request"), BdapActions.getPendingRequestLinksSuccess, BdapActions.getPendingRequestLinksFailed)
- })
- yield takeEvery(getType(BdapActions.getCompleteLinks), function* () {
-
+ yield put(BdapActions.getUsersSuccess(response));
+ });
+ yield takeEvery(getType(BdapActions.getBalance), function*() {
+ let balance: number;
+ try {
+ balance = yield unlockedCommandEffect(rpcClient, async client => {
+ const b: number = await client.command("getbalance");
+ const cr: { total_credits: number } = await client.command(
+ "getcredits"
+ );
+ const tc = cr.total_credits;
+ const bc = Math.trunc(b / 0.00100001);
+ return tc + bc;
+ });
+ } catch (err) {
+ yield put(BdapActions.getBalanceFailed(err.message));
+ return;
+ }
+ yield put(BdapActions.getBalanceSuccess(balance));
+ });
+ yield takeEvery(getType(BdapActions.getTopUpAddress), function*() {
+ let topUpAddress: string | undefined;
+ try {
+ const bdapAcct: BdapAccount = yield getFirstBdapAccount(rpcClient);
+ topUpAddress = bdapAcct.link_address;
+ } catch (err) {
+ yield put(BdapActions.getTopUpAddressFailed(err.message));
+ return;
+ }
+ yield put(BdapActions.getTopUpAddressSuccess(topUpAddress!));
+ });
+ yield takeEvery(getType(BdapActions.getPendingAcceptLinks), function*() {
+ yield* rpcLinkCommand(
+ rpcClient,
+ client => client.command("link", "pending", "accept"),
+ BdapActions.getPendingAcceptLinksSuccess,
+ BdapActions.getPendingAcceptLinksFailed
+ );
+ });
+ yield takeEvery(getType(BdapActions.getPendingRequestLinks), function*() {
+ yield* rpcLinkCommand(
+ rpcClient,
+ client => client.command("link", "pending", "request"),
+ BdapActions.getPendingRequestLinksSuccess,
+ BdapActions.getPendingRequestLinksFailed
+ );
+ });
+ yield takeEvery(getType(BdapActions.getCompleteLinks), function*() {
//const rpcClient: RpcClient = yield call(() => getRpcClient())
let response: LinkResponse;
try {
- response = yield call(() => rpcClient.command("link", "complete"))
-
+ response = yield call(() => rpcClient.command("link", "complete"));
} catch (err) {
- yield put(BdapActions.getCompleteLinksFailed(err.message))
- return
+ yield put(BdapActions.getCompleteLinksFailed(err.message));
+ return;
}
const links = extractLinks(response);
+ yield put(BdapActions.getCompleteLinksSuccess(links));
+ });
- yield put(BdapActions.getCompleteLinksSuccess(links))
-
- })
-
-
- yield takeEvery(getType(BdapActions.getDeniedLinks), function* () {
-
- const currentUser: GetUserInfo = yield getCurrentUser()
- const userName = currentUser.object_id
+ yield takeEvery(getType(BdapActions.getDeniedLinks), function*() {
+ const currentUser: GetUserInfo = yield getCurrentUser();
+ const userName = currentUser.object_id;
let response: LinkDeniedResponse | {};
try {
- response = yield unlockedCommandEffect(rpcClient, client => client.command("link", "denied", userName))
-
+ response = yield unlockedCommandEffect(rpcClient, client =>
+ client.command("link", "denied", userName)
+ );
} catch (err) {
//if (!/^DeniedLinkList: ERRCODE: 5604/.test(err.message)) {
// yield put(BdapActions.getDeniedLinksFailed(err.message))
// return
//}
- response = {}
+ response = {};
}
if (isLinkDeniedResponse(response)) {
- const deniedList = entries(response.denied_list).select(([, v]) => v).toArray()
- yield put(BdapActions.getDeniedLinksSuccess(deniedList))
+ const deniedList = entries(response.denied_list)
+ .select(([, v]) => v)
+ .toArray();
+ yield put(BdapActions.getDeniedLinksSuccess(deniedList));
+ } else {
+ yield put(BdapActions.getDeniedLinksSuccess([]));
}
- else {
- yield put(BdapActions.getDeniedLinksSuccess([]))
- }
-
- })
-
- yield takeEvery(getType(BdapActions.initialize), function* () {
- let allSourcesRetrievedAtLeastOnce = false
- for (; ;) {
-
-
- yield put(BdapActions.getUsers())
-
- yield put(BdapActions.getCompleteLinks())
- yield put(BdapActions.getPendingAcceptLinks())
- yield put(BdapActions.getPendingRequestLinks())
- yield put(BdapActions.getDeniedLinks())
-
- const getResults = yield all({
- users: race({
- success: take(getType(BdapActions.getUsersSuccess)),
- failure: take(getType(BdapActions.getUsersFailed))
- }),
- completeLinks: race({
- success: take(getType(BdapActions.getCompleteLinksSuccess)),
- failure: take(getType(BdapActions.getCompleteLinksFailed))
- }),
- pendingRequest: race({
- success: take(getType(BdapActions.getPendingRequestLinksSuccess)),
- failure: take(getType(BdapActions.getPendingRequestLinksFailed))
- }),
- pendingAccept: race({
- success: take(getType(BdapActions.getPendingAcceptLinksSuccess)),
- failure: take(getType(BdapActions.getPendingAcceptLinksFailed))
- }),
- denied: race({
- success: take(getType(BdapActions.getDeniedLinksSuccess)),
- failure: take(getType(BdapActions.getDeniedLinksFailed))
-
- })
- })
-
- if (getResults.users.success
- && getResults.completeLinks.success
- && getResults.pendingRequest.success
- && getResults.pendingAccept.success
- && getResults.denied.success
- ) {
- console.log("all user/link data successful retrieved")
-
- allSourcesRetrievedAtLeastOnce = true
- yield put(BdapActions.bdapDataFetchSuccess())
- yield delay(60000)
+ });
+
+ yield takeEvery(getType(BdapActions.initialize), function*() {
+ let operationCounts: OperationCounts = entries(operations)
+ .select(([k, v]) => tuple(k, 0))
+ .aggregate({}, (p, [k, v]) => ({ ...p, [k]: v }));
+
+ //let allSourcesRetrievedAtLeastOnce = false
+
+ for (;;) {
+ let remainingEntries = entries(operations)
+ .join(
+ entries(operationCounts),
+ ([k]) => k,
+ ([k]) => k,
+ ([k, vo], [, vc]) => tuple(k, vo, vc)
+ )
+ .where(
+ ([, vo, vc]) =>
+ vo.frequency === "repeated" ||
+ (vo.frequency === "once" && vc === 0)
+ )
+ .select(([k, vo]) => tuple(k, vo));
+
+ console.log(
+ "remaining entries",
+ remainingEntries.select(([k]) => k).toArray()
+ );
+
+ for (let attempts = 0; remainingEntries.any(); attempts++) {
+ if (attempts >= 3) {
+ throw Error(
+ `Could not retrieve ${[
+ ...remainingEntries.select(([k]) => k),
+ ].join(", ")} lists from dynamicd`
+ );
+ }
+ const remainingActions = remainingEntries.select(([k, v]) => ({
+ key: k,
+ effect: put(v.action()),
+ successAction: v.successAction,
+ failureAction: v.failureAction,
+ }));
+ let resultsReport: ResultsReport = {};
+ for (const v of remainingActions) {
+ yield v.effect;
+ const r = yield race({
+ success: take(getType(v.successAction)),
+ failure: take(getType(v.failureAction)),
+ });
+ resultsReport = { ...resultsReport, [v.key]: r };
+ }
+ const resultEntries = entries(resultsReport);
+
+ const entriesWithResults = remainingEntries.join(
+ resultEntries,
+ ([k]) => k,
+ ([k]) => k,
+ ([k, vo], [, vr]) => ({ key: k, operation: vo, result: vr })
+ );
+
+ const failedEntries = entriesWithResults.where(
+ ({ result: { failure } }) => failure
+ );
+ const successEntries = entriesWithResults.where(
+ ({ result: { success } }) => success
+ );
+
+ operationCounts = entries(operationCounts)
+ .leftOuterJoin(
+ successEntries,
+ ([k]) => k,
+ e => e.key,
+ (oe, se) => (se ? tuple(oe[0], oe[1] + 1) : oe)
+ )
+ .aggregate({}, (p, [k, v]) => ({ ...p, [k]: v }));
+
+ remainingEntries = failedEntries.select(e =>
+ tuple(e.key, e.operation)
+ );
}
- else {
- console.warn("some user/link data was not successfully retrieved")
- //todo: report this, somehow
- yield put(BdapActions.bdapDataFetchFailed("some user/link data was not successfully retrieved"))
- if (allSourcesRetrievedAtLeastOnce) {
- yield delay(60000)
- }
+ //allSourcesRetrievedAtLeastOnce = true;
+ if (!remainingEntries.any()) {
+ yield put(BdapActions.bdapDataFetchSuccess());
+ } else {
+ const msg = `some user/link data was not successfully retrieved [${[
+ ...remainingEntries.select(([k]) => k),
+ ].join(", ")}]`;
+ yield put(BdapActions.bdapDataFetchFailed(msg));
}
+ console.log("operation counts", operationCounts);
+ yield delay(60000);
}
-
- })
+ });
}
-const reservedKeyNames = ["locked_links"]
+const reservedKeyNames = ["locked_links"];
const extractLinks = (response: LinkResponse): T[] =>
entries(response)
- .leftOuterJoin(reservedKeyNames, ([k,]) => k, rkn => rkn, (entry, rkn) => ({ entry, rkn }))
+ .leftOuterJoin(
+ reservedKeyNames,
+ ([k]) => k,
+ rkn => rkn,
+ (entry, rkn) => ({ entry, rkn })
+ )
.where(({ rkn }) => typeof rkn === "undefined")
.select(({ entry: [, v] }) => v)
- .toArray()
-
-
+ .toArray();
const getCurrentUser = () =>
- call(function* () {
- let currentUser: GetUserInfo = yield select((state: MainRootState) => state.bdap.currentUser);
- if (typeof currentUser === 'undefined') {
- const a: ActionType = yield take(getType(BdapActions.currentUserReceived));
+ call(function*() {
+ let currentUser: GetUserInfo = yield select(
+ (state: MainRootState) => state.bdap.currentUser
+ );
+ if (typeof currentUser === "undefined") {
+ const a: ActionType<
+ typeof BdapActions.currentUserReceived
+ > = yield take(getType(BdapActions.currentUserReceived));
currentUser = a.payload;
}
return currentUser;
- })
+ });
function* rpcLinkCommand(
rpcClient: RpcClient,
@@ -178,19 +311,19 @@ function* rpcLinkCommand(
successActionCreator: (entries: T[]) => any,
failActionCreator: (message: string) => any
) {
-
let response: LinkResponse;
try {
- response = yield unlockedCommandEffect(rpcClient, cmd)
+ response = yield unlockedCommandEffect(rpcClient, cmd);
} catch (err) {
- yield put(failActionCreator(err.message))
+ yield put(failActionCreator(err.message));
return;
}
- const links = extractLinks(response)
- yield put(successActionCreator(links))
+ const links = extractLinks(response);
+ yield put(successActionCreator(links));
}
-function isLinkDeniedResponse(obj: LinkDeniedResponse | {}): obj is LinkDeniedResponse {
+function isLinkDeniedResponse(
+ obj: LinkDeniedResponse | {}
+): obj is LinkDeniedResponse {
return (obj).list_updated !== undefined;
}
-
diff --git a/src/main/sagas/bulkImportSaga.ts b/src/main/sagas/bulkImportSaga.ts
new file mode 100644
index 00000000..f11104fe
--- /dev/null
+++ b/src/main/sagas/bulkImportSaga.ts
@@ -0,0 +1,227 @@
+import { takeEvery, call, select, put } from "redux-saga/effects";
+import { getType, ActionType } from "typesafe-actions";
+
+import { RpcClient } from "../RpcClient";
+import { BdapActions } from "../../shared/actions/bdap";
+import { BrowserWindowProvider } from "../../shared/system/BrowserWindowProvider";
+// import { app, BrowserWindow, dialog } from "electron";
+import * as fs from 'fs';
+import { blinq } from "blinq";
+import { MainRootState } from "../reducers";
+import { GetUserInfo } from "../../dynamicdInterfaces/GetUserInfo";
+import { Link } from "../../dynamicdInterfaces/links/Link";
+import { PendingLink } from "../../dynamicdInterfaces/links/PendingLink";
+import { DeniedLink } from "../../dynamicdInterfaces/DeniedLink";
+import { unlockedCommandEffect } from "./effects/unlockedCommandEffect";
+import { getUserNameFromFqdn } from "../../shared/system/getUserNameFromFqdn";
+import { BulkImportActions } from "../../shared/actions/bulkImport";
+
+export function* previewBulkImportSaga() {
+ yield takeEvery(getType(BulkImportActions.previewBulkImport), function*(action: ActionType) {
+ const filePath = action.payload;
+ // console.log(filePath)
+ if (filePath == null) {
+ yield put(BulkImportActions.bulkImportAborted());
+ return;
+ }
+ const data = yield call (() => readFile(filePath.path))
+ yield put(BulkImportActions.previewData(data))
+ })
+}
+
+export interface RequestStatus {
+ link: string,
+ status: string,
+}
+
+export function* bulkImportSaga(rpcClient: RpcClient, browserWindowProvider: BrowserWindowProvider) {
+ yield takeEvery(getType(BulkImportActions.beginBulkImport), function* (action: ActionType) {
+
+ const data = action.payload;
+
+ const userFqdnsFromFile = [...blinq(splitLines(data))];
+
+ const allUsers: GetUserInfo[] = yield select((s: MainRootState) => s.bdap.users);
+ const completeLinks: Link[] = yield select((s: MainRootState) => s.bdap.completeLinks);
+ const pendingRequestLinks: PendingLink[] = yield select((s: MainRootState) => s.bdap.pendingRequestLinks);
+ const deniedRequestLinks: DeniedLink[] = yield select((s: MainRootState) => s.bdap.deniedLinks);
+ const currentUserFqdn: string = yield select((s: MainRootState) => typeof s.bdap.currentUser !== 'undefined' ? s.bdap.currentUser.object_full_path : undefined)
+ const pendingAcceptLinks: PendingLink[] = yield select((s: MainRootState) => s.bdap.pendingAcceptLinks);
+
+ const fqdnRequestStatus: RequestStatus[] = [];
+
+ const completeFqdns =
+ blinq(completeLinks)
+ .select(l => blinq([l.recipient_fqdn, l.requestor_fqdn]).first(n => n !== currentUserFqdn));
+
+ // for( const fqdn in completeFqdns ) {
+ // fqdnRequestStatus.push({ link: fqdn, status: 'completed'})
+ // }
+
+ const deniedFqdns =
+ blinq(deniedRequestLinks)
+ .select(l => l.requestor_fqdn);
+
+ // for ( const fqdn in deniedFqdns ) {
+ // fqdnRequestStatus.push({ link: fqdn, status: 'denied' })
+ // }
+
+ const pendingLinkFqdns =
+ blinq(pendingAcceptLinks)
+ .concat(pendingRequestLinks)
+ .select(l => blinq([l.recipient_fqdn, l.requestor_fqdn]).first(n => n !== currentUserFqdn));
+
+ // for ( const fqdn in pendingLinkFqdns ) {
+ // fqdnRequestStatus.push({ link: fqdn, status: 'pending' })
+ // }
+
+ const exclusions =
+ completeFqdns
+ .concat(deniedFqdns)
+ .concat(pendingLinkFqdns)
+ .concat([currentUserFqdn])
+ .distinct();
+
+ const totalListItems = userFqdnsFromFile.length;
+
+ const fqdnListToUsers = blinq(userFqdnsFromFile)
+ .fullOuterJoin(allUsers, x => x, u => u.object_full_path, (listUser, user) => ({ listUser, user }));
+
+ const usersThatExist = fqdnListToUsers.where(x => x.user != null && x.listUser != null).select(x => x.user!);
+ const listFqdnsThatDontExist = fqdnListToUsers.where(x => x.listUser != null && x.user == null).select(x => x.listUser!);
+
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const userFqdn of listFqdnsThatDontExist) {
+ failCount++;
+ fqdnRequestStatus.push({ status: 'User does not exist', link: userFqdn })
+ yield put(BulkImportActions.bulkImportProgress({
+ totalItems: totalListItems,
+ failed: failCount,
+ successful: successCount,
+ currentItem: {
+ linkFqdn: userFqdn,
+ err: "User does not exist",
+ success: false
+ }
+ }))
+ }
+
+ const usersToExclusions = usersThatExist
+ .leftOuterJoin(exclusions, u => u.object_full_path, e => e, (user, excludedUserFqdn) => ({ user, excludedUserFqdn }));
+ const excludedUserFqdns = usersToExclusions.where(x => x.excludedUserFqdn != null).select(x => x.user.object_full_path);
+ for (const userFqdn of excludedUserFqdns) {
+ failCount++;
+ fqdnRequestStatus.push({ status: 'Link already requested/complete/denied', link: userFqdn })
+ yield put(BulkImportActions.bulkImportProgress({
+ totalItems: totalListItems,
+ failed: failCount,
+ successful: successCount,
+ currentItem: {
+ linkFqdn: userFqdn,
+ err: "Link already requested/complete/denied",
+ success: false
+ }
+ }))
+ }
+
+ const usersToRequestLink =
+ usersToExclusions
+ .where(x => x.excludedUserFqdn == null)
+ .select(x => x.user);
+
+
+ // const requestedLinks: = [];
+ const userName = getUserNameFromFqdn(currentUserFqdn);
+ const inviteMessage = `${userName} wants to link with you`;
+ //const failedUsers: GetUserInfo[] = [];
+ for (const user of usersToRequestLink) {
+ console.log("inviting " + user.object_id)
+ //let response: LinkRequestResponse;
+ try {
+ yield unlockedCommandEffect(rpcClient, client => client.command("link", "request", userName, user.object_id, inviteMessage))
+ successCount++;
+ fqdnRequestStatus.push({ status: 'success', link: user.object_full_path})
+
+ } catch (err) {
+ failCount++;
+ if (/^Insufficient funds/.test(err.message)) {
+ fqdnRequestStatus.push({ status: 'Insufficient funds', link: user.object_full_path })
+ yield put(BulkImportActions.bulkImportFailed(fqdnRequestStatus))
+ yield put(BdapActions.insufficientFunds("request a link to " + user.object_id + " or any more users in the bulk import list"))
+ yield put(BdapActions.getPendingRequestLinks());
+ return;
+ } else {
+ fqdnRequestStatus.push({ status: 'Failed', link: user.object_full_path })
+ yield put(BulkImportActions.bulkImportProgress({
+ totalItems: totalListItems,
+ failed: failCount,
+ successful: successCount,
+ currentItem: {
+ linkFqdn: user.object_full_path,
+ success: false,
+ err: err.message
+ }
+ }))
+ }
+
+ continue;
+ }
+ yield put(BdapActions.getBalance())
+ yield put(BulkImportActions.bulkImportProgress({
+ totalItems: totalListItems,
+ failed: failCount,
+ successful: successCount,
+ currentItem: {
+ linkFqdn: user.object_full_path,
+ success: true
+ }
+ }))
+
+
+ }
+ yield put(BulkImportActions.bulkImportSuccess(fqdnRequestStatus));
+ //console.log("failed users", failedUsers);
+ yield put(BdapActions.getPendingRequestLinks());
+
+ });
+}
+
+function* splitLines(data: string) {
+ const split = data.match(/[^\r\n]+/g);
+ if (split) {
+ for (const v of split) {
+ yield v;
+ }
+ }
+}
+
+// function getFilePathSync(window: BrowserWindow) {
+// const homeDir = app.getPath("documents");
+// const path = dialog.showOpenDialog(window, {
+// // filters: [
+// // {
+// // name: "p-share wallet key backup",
+// // extensions: ["psh.json"]
+// // }
+// // ],
+// defaultPath: homeDir,
+// title: "Bulk import file",
+// properties: ["multiSelections", "openFile"]
+
+// });
+// if (path == null) {
+// return undefined;
+// }
+// if (Array.isArray(path) && path.length > 0) {
+// return path[0];
+// }
+// return undefined;
+// }
+
+function* readFile(path: string) {
+ const buf: Buffer = yield call(() => fs.promises.readFile(path))
+ const data = buf.toString();
+ return data;
+}
\ No newline at end of file
diff --git a/src/main/sagas/exportUserLinksSaga.ts b/src/main/sagas/exportUserLinksSaga.ts
new file mode 100644
index 00000000..e7d0b393
--- /dev/null
+++ b/src/main/sagas/exportUserLinksSaga.ts
@@ -0,0 +1,66 @@
+import { takeEvery, call, select } from "redux-saga/effects";
+import { getType } from "typesafe-actions";
+import { dialog, app, } from "electron";
+import { BrowserWindowProvider } from "../../shared/system/BrowserWindowProvider";
+import * as path from "path"
+import fs from 'fs'
+import { DashboardActions } from "../../shared/actions/dashboard";
+import { MainRootState } from "../reducers";
+import { CompleteLink } from "../../dynamicdInterfaces/links/CompleteLink";
+import { PendingLink } from "../../dynamicdInterfaces/links/PendingLink";
+
+export function* exportUserLinksSaga(browserWindowProvider: BrowserWindowProvider) {
+ yield takeEvery(getType(DashboardActions.exportMyLinks), function* () {
+ const browserWindow = browserWindowProvider();
+ if (browserWindow == null) {
+ return
+ }
+
+ const currentUserFqdn: string = yield select((s: MainRootState) => typeof s.bdap.currentUser !== 'undefined' ? s.bdap.currentUser.object_full_path : undefined)
+ const links: string[] = []
+ const completeLinks: CompleteLink[] = yield select((s: MainRootState) => s.bdap.completeLinks)
+ const pendingAcceptLinks: PendingLink[] = yield select((s: MainRootState) => s.bdap.pendingAcceptLinks)
+ const pendingRequestLinks: PendingLink[] = yield select((s: MainRootState) => s.bdap.pendingRequestLinks)
+
+ completeLinks.map((link: CompleteLink) => {
+ link.recipient_fqdn === currentUserFqdn ?
+ links.push(link.requestor_fqdn) : links.push(link.recipient_fqdn)
+ })
+
+ pendingAcceptLinks.map((link: PendingLink) => {
+ links.push(link.requestor_fqdn)
+ })
+
+ pendingRequestLinks.map((link: PendingLink) => {
+ links.push(link.recipient_fqdn)
+ })
+
+ const fileName = 'User Links export';
+ const downloadsDir = app.getPath("downloads");
+ const defaultSavePath = path.join(downloadsDir, fileName);
+
+ const showDialog = () => new Promise((resolve, reject) => {
+ dialog
+ .showSaveDialog(browserWindow, {
+ buttonLabel: "Save",
+ defaultPath: defaultSavePath,
+ title: `Save ${fileName} as...`
+ }, (filename) => {
+ if (filename) {
+ fs.writeFileSync(filename, links.join('\n'), 'utf-8');
+ resolve('Success')
+ }
+ else {
+ reject("cancelled");
+ }
+ });
+ });
+ // simple call without try/catch kill the saga monitor in error state (reject)
+ try {
+ yield call(() => showDialog())
+ }
+ catch {
+ console.log('export-cancelled')
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/main/sagas/fileRequestSaveDialogSaga.ts b/src/main/sagas/fileRequestSaveDialogSaga.ts
index b5de39bd..5fc7802c 100644
--- a/src/main/sagas/fileRequestSaveDialogSaga.ts
+++ b/src/main/sagas/fileRequestSaveDialogSaga.ts
@@ -3,48 +3,61 @@ import { getType, ActionType } from "typesafe-actions";
import { FileSharingActions } from "../../shared/actions/fileSharing";
import { dialog, app, BrowserWindow } from "electron";
import { BrowserWindowProvider } from "../../shared/system/BrowserWindowProvider";
-import * as path from "path"
+import * as path from "path";
import { FileRequest } from "../../shared/actions/payloadTypes/FileRequest";
+import { FileRequestWithSavePath } from "../../shared/actions/payloadTypes/FileRequestWithSavePath";
-export function* requestFileSaveDialogSaga(browserWindowProvider: BrowserWindowProvider) {
- yield takeEvery(getType(FileSharingActions.requestFile), function* (action: ActionType) {
+export function* requestFileSaveDialogSaga(
+ browserWindowProvider: BrowserWindowProvider
+) {
+ yield takeEvery(getType(FileSharingActions.requestFile), function*(
+ action: ActionType
+ ) {
const browserWindow = browserWindowProvider();
if (browserWindow == null) {
- return
+ return;
}
- const fileRequest = action.payload
- let savePath: string
+ const fileRequest = action.payload;
+ let savePath: string;
try {
savePath = yield* getSavePath(fileRequest, browserWindow);
} catch (err) {
if (err.message === "cancelled") {
return;
}
- throw err
+ throw err;
}
- yield put(FileSharingActions.requestFileWithSavePath({ ...fileRequest, savePath }))
- })
+ const fr: FileRequestWithSavePath = {
+ ...fileRequest,
+ savePath,
+ type: "file",
+ };
+ yield put(FileSharingActions.startRequestFile(fr));
+ });
}
function* getSavePath(fileRequest: FileRequest, browserWindow: BrowserWindow) {
const fileName = path.basename(path.normalize(fileRequest.fileName));
const downloadsDir = app.getPath("downloads");
const defaultSavePath = path.join(downloadsDir, fileName);
- const showDialog = () => new Promise((resolve, reject) => {
- dialog
- .showSaveDialog(browserWindow, {
- buttonLabel: "Save",
- defaultPath: defaultSavePath,
- title: `Save ${fileName} as...`
- }, (filename) => {
- if (filename) {
- resolve(filename);
- }
- else {
- reject(Error("cancelled"));
+ const showDialog = () =>
+ new Promise((resolve, reject) => {
+ dialog.showSaveDialog(
+ browserWindow,
+ {
+ buttonLabel: "Save",
+ defaultPath: defaultSavePath,
+ title: `Save ${fileName} as...`,
+ },
+ filename => {
+ if (filename) {
+ resolve(filename);
+ } else {
+ reject(Error("cancelled"));
+ }
}
- });
- });
+ );
+ });
const savePath: string = yield call(() => showDialog());
return savePath;
}
diff --git a/src/main/sagas/fileShareSaga.ts b/src/main/sagas/fileShareSaga.ts
deleted file mode 100644
index b9715138..00000000
--- a/src/main/sagas/fileShareSaga.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { RpcClient } from "../RpcClient";
-import { select, take, actionChannel, all, put } from "redux-saga/effects";
-import { MainRootState } from "../reducers";
-import { InOutSharedFiles } from "../../shared/reducers/fileWatch";
-import { Link, isLink } from "../../dynamicdInterfaces/links/Link";
-import { blinq } from "blinq";
-import { getUserNameFromFqdn } from "../../shared/system/getUserNameFromFqdn";
-import { entries } from "../../shared/system/entries";
-import { SharedFile } from "../../shared/types/SharedFile";
-import { BdapActions } from "../../shared/actions/bdap";
-import { getType, ActionType } from "typesafe-actions";
-import { FileWatchActions } from "../../shared/actions/fileWatch";
-import { unlockedCommandEffect } from "./effects/unlockedCommandEffect";
-import { PublicSharedFile } from "../../shared/types/PublicSharedFile";
-import { RootActions } from "../../shared/actions";
-import { Channel, buffers, delay } from "redux-saga";
-import { getChannelActionsUntilTimeOut } from "./helpers/getChannelActionsUntilTimeOut";
-import { FileListActions } from "../../shared/actions/fileList";
-
-type AddUnlinkAndNewLinkActionTypes =
- ActionType
- | ActionType
- | ActionType
-
-const putErrorRegexes = [
- /Put failed\. Record is locked\. You need to wait at least (\d+) seconds before updating the same record in the DHT\./,
- /DHT data entry is locked for another (\d+) seconds/
-];
-
-export function* fileShareSaga(rpcClient: RpcClient) {
- const channel: Channel =
- yield actionChannel((action: RootActions) => {
- switch (action.type) {
- case getType(FileWatchActions.fileAdded):
- case getType(FileWatchActions.fileUnlinked):
- case getType(BdapActions.newCompleteLink):
-
- console.log("found AddAndUnlinkActionTypes")
- return true
- default:
- return false
- }
- }, buffers.expanding())
- yield all({
- initialScan: take(getType(FileWatchActions.initialScanComplete)),
- bdapFetchData: take(getType(BdapActions.bdapDataFetchSuccess))
- })
- const userName: string = yield select((s: MainRootState) => s.user.userName)
-
-
- let hasAnnouncedFirstPublish = false
- for (; ;) {
- const allActions: AddUnlinkAndNewLinkActionTypes[] = yield getChannelActionsUntilTimeOut(channel, 5000)
-
- const usersThatNeedUpdating = blinq(allActions)
- .select(a =>
- isLink(a.payload)
- ? getRemoteLinkName(a.payload, userName)
- : a.payload.sharedWith)
- .selectMany(x => x == null ? [] : [x])
- .distinct()
- const existingFileWatchUsers: Record = yield select((s: MainRootState) => s.fileWatch.users)
- const completeLinks: Link[] = yield select((s: MainRootState) => s.bdap.completeLinks)
- const remoteUsers =
- blinq(completeLinks)
- .selectMany(l => [l.recipient_fqdn, l.requestor_fqdn])
- .select(fqdn => getUserNameFromFqdn(fqdn)!)
- .intersect(usersThatNeedUpdating)
- const entriesWithMatchingCompleteLink =
- remoteUsers
- .leftOuterJoin | undefined]>(
- entries(existingFileWatchUsers),
- remoteUserName => remoteUserName,
- ([fileShareUserName]) => fileShareUserName,
- (remUn, x) => [remUn, x ? x[1].out || {} : {}])
- .where(([, sharedFiles]) => typeof sharedFiles !== 'undefined')
- const dataForLinks =
- entriesWithMatchingCompleteLink
- .select<[string, Record | undefined], [string, Iterable]>(
- ([userName, sharedFiles]) => [
- userName,
- sharedFiles
- ? entries(sharedFiles)
- .select(([fileName, sharedFile]) => ({
- fileName,
- hash: sharedFile.hash!,
- size: sharedFile.size!,
- contentType: sharedFile.contentType!
- }))
- .orderBy(x => x.hash)
- : []
- ])
- const dataToPublish = [...dataForLinks]
- for (const [remoteUserName, publicSharedFiles] of dataToPublish) {
- const serialized = JSON.stringify([...publicSharedFiles])
- console.log(`shared with ${remoteUserName} : ${serialized}`)
- //dht putlinkrecord hfchrissperry100 hfchrissperry101 pshare-filelist ""
- for (; ;) {
- try {
- const result =
- yield unlockedCommandEffect(
- rpcClient,
- client =>
- client.command("dht", "putlinkrecord", userName, remoteUserName, "pshare-filelist", serialized, true))
- console.log(`putbdaplinkdata returned ${JSON.stringify(result, null, 2)}`)
- } catch (err) {
- const r = blinq(putErrorRegexes).select(regex => regex.exec(err.message)).firstOrDefault(result => result != null)
- //const r = /DHT data entry is locked for another (\d+) seconds/.exec(err.message)
- if (r) {
- console.warn(err.message)
- const unlockTimeSecsStr = r[1]
- const unlockTimeSecs = parseInt(unlockTimeSecsStr, 10)
- if (!isNaN(unlockTimeSecs)) {
- console.warn(`waiting for ${unlockTimeSecs}s before trying again`)
- yield delay(1000 * (unlockTimeSecs + 1))
- continue
- }
- console.warn(`could not parse wait time from error message`)
- }
- throw err
-
- }
- break;
- }
- }
- if (dataToPublish.length > 0 || !hasAnnouncedFirstPublish) {
- hasAnnouncedFirstPublish = true
- yield put(FileListActions.fileListPublished())
- }
- }
-
-}
-
-const getRemoteLinkName = (link: Link, localUserName: string) =>
- blinq([link.recipient_fqdn, link.requestor_fqdn])
- .select(getUserNameFromFqdn)
- .single(n => n !== localUserName)
-
diff --git a/src/main/sagas/fileWatchSaga.ts b/src/main/sagas/fileWatchSaga.ts
index 90335a77..1f9068a8 100644
--- a/src/main/sagas/fileWatchSaga.ts
+++ b/src/main/sagas/fileWatchSaga.ts
@@ -1,160 +1,174 @@
-import { watch } from 'chokidar'
-import { eventChannel, END } from 'redux-saga';
-import { app } from 'electron';
-import * as path from 'path'
-import * as fs from 'fs'
-import * as util from 'util'
-import * as fsExtra from 'fs-extra'
-import { call, take, cancelled, fork, put } from 'redux-saga/effects';
-import { createAsyncQueue } from '../../shared/system/createAsyncQueue';
-import { BdapActions } from '../../shared/actions/bdap';
-import mime from 'mime-types'
-import { blinq } from 'blinq';
-import { FileWatchActions } from "../../shared/actions/fileWatch";
-import { SharedFile } from '../../shared/types/SharedFile';
-import { maximumFileSize } from '../../shared/system/maximumFileSize';
-import { hashFile } from '../../shared/system/hashing/hashFile';
-import { getType } from 'typesafe-actions';
+import { watch } from "chokidar";
+import { eventChannel, END, buffers } from "redux-saga";
+import { app } from "electron";
+import os from "os";
+import * as path from "path";
+import * as fs from "fs";
+import * as util from "util";
+import * as fsExtra from "fs-extra";
+import { call, take, put } from "redux-saga/effects";
+import { BdapActions } from "../../shared/actions/bdap";
+import mime from "mime-types";
+import { blinq } from "blinq";
+import { FileWatchActions, FileChange } from "../../shared/actions/fileWatch";
+import { SharedFile } from "../../shared/types/SharedFile";
+//import { maximumFileSize } from '../../shared/system/maximumFileSize';
+//import { hashFile } from '../../shared/system/hashing/hashFile';
+import { getType } from "typesafe-actions";
+import { resourceScope } from "../../shared/system/redux-saga/resourceScope";
+import { takeBatch } from "../../shared/system/redux-saga/takeBatch";
interface SimpleFileWatchEvent {
- type: "add" | "change" | "unlink" | "ready"
+ type: "add" | "change" | "unlink" | "ready";
}
interface FileWatchEvent extends SimpleFileWatchEvent {
- path: string
+ path: string;
}
-const fsStatAsync = util.promisify(fs.stat)
+const fsStatAsync = util.promisify(fs.stat);
//const isDirectory = (obj: DirectoryEntry): obj is Directory => obj.type === "directory"
-const isFileWatchEvent = (obj: SimpleFileWatchEvent): obj is FileWatchEvent => (obj).path !== undefined
+const isFileWatchEvent = (obj: SimpleFileWatchEvent): obj is FileWatchEvent =>
+ (obj).path !== undefined;
const pathToShareDirectory = path.join(app.getPath("home"), ".pshare", "share");
-const getRelativePath = (fqPath: string) => path.relative(pathToShareDirectory, fqPath)
+const getRelativePath = (fqPath: string) =>
+ path.relative(pathToShareDirectory, fqPath);
export function* fileWatchSaga() {
- yield take(getType(BdapActions.bdapDataFetchSuccess))
- yield call(() => fsExtra.ensureDir(pathToShareDirectory))
- console.log("starting file watcher")
- const watcher = watch(pathToShareDirectory, { awaitWriteFinish: { stabilityThreshold: 500 } })
- const channel = eventChannel((emitter: (v: SimpleFileWatchEvent | FileWatchEvent | END) => void) => {
- const addHandler: (...args: any[]) => void = path => emitter({ type: "add", path });
- const changeHandler: (...args: any[]) => void = path => emitter({ type: "change", path });
- const unlinkHandler: (...args: any[]) => void = path => emitter({ type: "unlink", path });
- const readyHandler: (...args: any[]) => void = () => emitter({ type: "ready" });
- watcher.on("add", addHandler)
- watcher.on("change", changeHandler)
- watcher.on("unlink", unlinkHandler)
- watcher.on("ready", readyHandler)
- return () => {
- console.log("killing watcher")
- watcher.off("add", addHandler)
- watcher.off("change", changeHandler)
- watcher.off("unlink", unlinkHandler)
- watcher.off("ready", readyHandler)
- }
- })
- const q = createAsyncQueue()
- yield fork(function* () {
- try {
- for (; ;) {
- const ev: SimpleFileWatchEvent = yield take(channel)
- q.post(ev)
- }
- } finally {
- if (yield cancelled()) {
- channel.close()
- }
- }
- })
- //const addedFiles: string[] = []
- //const unlinkedFiles: string[] = []
- for (; ;) {
- const ev: SimpleFileWatchEvent = yield call(() => q.receive())
- if (ev.type === "ready") {
- yield put(FileWatchActions.initialScanComplete())
- continue
- }
+ yield take(getType(BdapActions.bdapDataFetchSuccess));
+ yield call(() => fsExtra.ensureDir(pathToShareDirectory));
+ console.log("starting file watcher");
+ const watcher = watch(pathToShareDirectory, {
+ awaitWriteFinish: { stabilityThreshold: 500 },
+ });
+ const channel = eventChannel(
+ (emitter: (v: SimpleFileWatchEvent | FileWatchEvent | END) => void) => {
+ const addHandler: (...args: any[]) => void = path =>
+ emitter({ type: "add", path });
+ const changeHandler: (...args: any[]) => void = path =>
+ emitter({ type: "change", path });
+ const unlinkHandler: (...args: any[]) => void = path =>
+ emitter({ type: "unlink", path });
+ const readyHandler: (...args: any[]) => void = () =>
+ emitter({ type: "ready" });
+ watcher.on("add", addHandler);
+ watcher.on("change", changeHandler);
+ watcher.on("unlink", unlinkHandler);
+ watcher.on("ready", readyHandler);
+ return () => {
+ console.log("killing watcher");
+ watcher.off("add", addHandler);
+ watcher.off("change", changeHandler);
+ watcher.off("unlink", unlinkHandler);
+ watcher.off("ready", readyHandler);
+ };
+ },
+ buffers.expanding()
+ );
+ const scope = resourceScope(channel, ch => ch.close());
- if (isFileWatchEvent(ev)) {
- switch (ev.type) {
- case "add":
- {
- const files: SharedFile[] = yield* getSharedFileInfo([ev.path], true);
- for (const f of files) {
- if (f.size === undefined || f.size > maximumFileSize) {
- continue
- }
+ yield* scope.use(function*(channel) {
+ for (;;) {
+ const receivedEvts = yield takeBatch(channel, {
+ maxDurationMs: 10000,
+ maxSize: 4096,
+ minDurationMs: 1000,
+ });
+ console.log(`got ${receivedEvts.length} filewatch events`);
+ const fileChanges: FileChange[] = [];
- yield put(FileWatchActions.fileAdded(f))
+ for (const ev of receivedEvts) {
+ if (ev.type === "ready") {
+ yield put(FileWatchActions.initialScanComplete());
+ continue;
+ }
+
+ if (isFileWatchEvent(ev)) {
+ switch (ev.type) {
+ case "add": {
+ const files: SharedFile[] = yield* getSharedFileInfo(
+ [ev.path],
+ true
+ );
+
+ for (const f of files) {
+ // if (f.size === undefined || f.size > maximumFileSize) {
+ // continue
+ // }
+
+ fileChanges.push({ type: "added", file: f });
+ }
+ break;
}
- break
- }
- case "unlink":
- {
- const files: SharedFile[] = yield* getSharedFileInfo([ev.path], false);
- for (const f of files) {
- yield put(FileWatchActions.fileUnlinked(f))
+ case "unlink": {
+ const files: SharedFile[] = yield* getSharedFileInfo(
+ [ev.path],
+ false
+ );
+ for (const f of files) {
+ fileChanges.push({ type: "unlinked", file: f });
+ }
+ break;
}
- break
}
+ }
}
+ yield put(FileWatchActions.filesChanged(fileChanges));
}
- }
- // const allFiles = blinq(addedFiles)
- // .except(unlinkedFiles);
- // const files: SharedFile[] = yield* getSharedFileInfo(allFiles);
-
- // for (const f of files) {
- // yield put(FileWatchActions.fileAdded(f))
- // }
- // //console.log(initialFiles)
- // console.log(files)
-
+ });
}
const getTopDirectoryFromPath = (filePath: string) => {
const pathSegments = filePath.split(path.sep);
return pathSegments.length <= 1 ? undefined : pathSegments[0];
-}
-function* getSharedFileInfo(allFiles: Iterable, gatherMetaData: boolean) {
- const filePromises: Iterable> =
- blinq(allFiles)
- .selectMany(filePath => {
- const relPath = getRelativePath(filePath);
- const userDirName = getTopDirectoryFromPath(relPath); //represents linked user
- if (!userDirName) {
- return [];
- }
- const userDirRelativePath = path.relative(userDirName, relPath);
- const inOrOut = getTopDirectoryFromPath(userDirRelativePath)
- if (inOrOut !== "in" && inOrOut !== "out") {
- return []
- }
- const dir: "in" | "out" = inOrOut
- const inOutRelPath = path.relative(inOrOut, userDirRelativePath)
- return [{
+};
+function* getSharedFileInfo(
+ allFiles: Iterable,
+ gatherMetaData: boolean
+) {
+ const filePromises: Iterable> = blinq(allFiles)
+ .selectMany(filePath => {
+ const relPath = getRelativePath(filePath);
+ const userDirName = getTopDirectoryFromPath(relPath); //represents linked user
+ if (!userDirName) {
+ return [];
+ }
+ const userDirRelativePath = path.relative(userDirName, relPath);
+ const inOrOut = getTopDirectoryFromPath(userDirRelativePath);
+ if (inOrOut !== "in" && inOrOut !== "out") {
+ return [];
+ }
+ const dir: "in" | "out" = inOrOut;
+ const inOutRelPath = path.relative(inOrOut, userDirRelativePath);
+ return [
+ {
direction: dir,
filePath,
userDirName,
- inOutRelPath
- }];
- })
- .select(async (fi): Promise => {
+ inOutRelPath,
+ },
+ ];
+ })
+ .select(
+ async (fi): Promise => {
let stats: fs.Stats | undefined;
- let contentType: string | undefined
- let hash: string | undefined
+ let contentType: string | undefined;
+ //let hash: string | undefined
if (gatherMetaData) {
stats = await fsStatAsync(fi.filePath);
- contentType = mime.lookup(fi.filePath) || 'application/octet-stream'
- hash = await hashFile(fi.filePath)
- };
- return ({
+ contentType =
+ mime.lookup(fi.filePath) || "application/octet-stream";
+ //hash = await hashFile(fi.filePath)
+ }
+ return {
path: fi.filePath,
contentType,
- relativePath: fi.inOutRelPath,
+ relativePath: normalizeSlashes(fi.inOutRelPath),
direction: fi.direction,
size: stats ? stats.size : undefined,
sharedWith: fi.userDirName,
- hash
- });
- });
+ //hash
+ };
+ }
+ );
const files: SharedFile[] = [];
for (let filePromise of filePromises) {
const f: SharedFile = yield call(() => filePromise);
@@ -163,3 +177,5 @@ function* getSharedFileInfo(allFiles: Iterable, gatherMetaData: boolean)
return files;
}
+const normalizeSlashes = (p: string) =>
+ os.platform() === "win32" ? p.replace(/\\/g, "/") : p;
diff --git a/src/main/sagas/index.ts b/src/main/sagas/index.ts
index 9d462beb..55651cec 100644
--- a/src/main/sagas/index.ts
+++ b/src/main/sagas/index.ts
@@ -7,6 +7,7 @@ import { saveMnemonicSaga } from "./saveMnemonicSaga";
import { BrowserWindowProvider } from "../../shared/system/BrowserWindowProvider";
import { translateMnemonicFileSaveFailedActionsToValidationMessagesSaga } from "./translateMnemonicFileSaveFailedActionsToValidationMessagesSaga";
import { bdapSaga } from "./bdapSaga";
+import { bulkImportSaga, previewBulkImportSaga } from "./bulkImportSaga";
import { linkRequestSaga } from "./linkRequestSaga";
import { linkAcceptSaga } from "./linkAcceptSaga";
import { fileWatchSaga } from "./fileWatchSaga";
@@ -16,19 +17,17 @@ import { addFileSaga } from "./addFileSaga";
import { startViewSharedFilesSaga } from "./startViewSharedFilesSaga";
import { scanForLinkMessagesSaga } from "./scanForLinkMessagesSaga";
import { sendLinkMessageSaga } from "./sendLinkMessageSaga";
-//import { fileShareSaga } from "./fileShareSaga";
import { requestFileSaveDialogSaga } from "./fileRequestSaveDialogSaga";
import { newLinkSaga } from "./newLinkSaga";
import { restoreFromMnemonicSaga } from "./restoreFromMnemonicSaga";
import { removeFileSaga } from "./removeFileSaga";
-
+import { exportUserLinksSaga } from './exportUserLinksSaga';
export const getRootSaga = (rpcClient: RpcClientWrapper, browserWindowProvider: BrowserWindowProvider) => [
() => startViewSharedFilesSaga(rpcClient),
() => addFileSaga(),
() => removeFileSaga(),
() => fileWatchSaga(),
- //() => fileShareSaga(rpcClient),
() => linkRequestSaga(rpcClient),
() => linkAcceptSaga(rpcClient),
() => linkDeclineSaga(rpcClient),
@@ -40,10 +39,13 @@ export const getRootSaga = (rpcClient: RpcClientWrapper, browserWindowProvider:
() => saveMnemonicSaga(browserWindowProvider),
() => translateMnemonicFileSaveFailedActionsToValidationMessagesSaga(),
() => bdapSaga(rpcClient),
+ () => bulkImportSaga(rpcClient, browserWindowProvider),
+ () => previewBulkImportSaga(),
() => scanForLinkMessagesSaga(rpcClient),
() => sendLinkMessageSaga(rpcClient),
() => requestFileSaveDialogSaga(browserWindowProvider),
() => newLinkSaga(),
- () => restoreFromMnemonicSaga(rpcClient)
+ () => restoreFromMnemonicSaga(rpcClient),
+ () => exportUserLinksSaga(browserWindowProvider),
]
diff --git a/src/main/sagas/linkAcceptSaga.ts b/src/main/sagas/linkAcceptSaga.ts
index 4bb990ca..90ee2068 100644
--- a/src/main/sagas/linkAcceptSaga.ts
+++ b/src/main/sagas/linkAcceptSaga.ts
@@ -49,15 +49,22 @@ export function* linkAcceptSaga(rpcClient: RpcClient) {
typeof registrationDays === 'undefined'
? client.command("link", "accept", recipient, requestor)
: client.command("link", "accept", recipient, requestor, registrationDays.toString()));
+ console.log(response)
} catch (err) {
- //debugger
- throw err
+ if (/^Insufficient funds/.test(err.message)) {
+ yield put(BdapActions.insufficientFunds("accept a link from " + requestor))
+
+ } else {
+ throw err
+ }
}
- console.log(response)
+
}
yield put(BdapActions.getCompleteLinks())
yield put(BdapActions.getPendingAcceptLinks())
+ yield put(BdapActions.getBalance())
+
});
}
diff --git a/src/main/sagas/linkDeclineSaga.ts b/src/main/sagas/linkDeclineSaga.ts
index 774a3491..365b2f7d 100644
--- a/src/main/sagas/linkDeclineSaga.ts
+++ b/src/main/sagas/linkDeclineSaga.ts
@@ -45,15 +45,22 @@ export function* linkDeclineSaga(rpcClient: RpcClient) {
try {
response =
yield unlockedCommandEffect(rpcClient, client => client.command("link", "deny", recipient, requestor));
+ console.log(response)
} catch (err) {
- //debugger
+ if (/^Insufficient funds/.test(err.message)) {
+ yield put(BdapActions.insufficientFunds("deny a link from " + requestor))
+ return
+
+ }
throw err
}
- console.log(response)
+
}
yield put(BdapActions.getCompleteLinks())
yield put(BdapActions.getPendingAcceptLinks())
+ yield put(BdapActions.getBalance())
+
});
}
diff --git a/src/main/sagas/linkRequestSaga.ts b/src/main/sagas/linkRequestSaga.ts
index cb6b4883..6e50274c 100644
--- a/src/main/sagas/linkRequestSaga.ts
+++ b/src/main/sagas/linkRequestSaga.ts
@@ -47,6 +47,7 @@ export function* linkRequestSaga(rpcClient: RpcClient) {
try {
response =
yield unlockedCommandEffect(rpcClient, client => client.command("link", "request", requestor, recipient, inviteMessage))
+ console.log(response)
} catch (err) {
if (/^BDAP_SEND_LINK_RPC_ERROR\: ERRCODE\: 4001/.test(err.message)) {
@@ -54,12 +55,18 @@ export function* linkRequestSaga(rpcClient: RpcClient) {
yield put(BdapActions.createLinkRequestFailed("Link request or accept already exists for these accounts"))
return
}
- //debugger
+
+ if (/^Insufficient funds/.test(err.message)) {
+ yield put(BdapActions.insufficientFunds("request a link to " + recipient))
+ return
+
+ }
throw err
}
- console.log(response)
+
yield put(BdapActions.getPendingRequestLinks())
+ yield put(BdapActions.getBalance())
})
diff --git a/src/main/sagas/remoteLoggingSaga.ts b/src/main/sagas/remoteLoggingSaga.ts
index 685e2dd7..3ff42862 100644
--- a/src/main/sagas/remoteLoggingSaga.ts
+++ b/src/main/sagas/remoteLoggingSaga.ts
@@ -6,6 +6,11 @@ import winston from 'winston'
const isDevelopment = process.env.NODE_ENV === 'development'
+const passwordRegex = new RegExp('password','i')
+const passphraseRegex = new RegExp('passphrase','i')
+const mnemonicRegex = new RegExp('mnemonic','i')
+
+const tokenTester = (item: string) => (passwordRegex.test(item) || passphraseRegex.test(item) || mnemonicRegex.test(item))
export function* remoteLoggingSaga() {
if (isDevelopment) {
@@ -16,6 +21,12 @@ export function* remoteLoggingSaga() {
if (typeof logger === 'undefined') {
return;
}
+ let flag = false
+ action.payload.args.map(item => {
+ if (item && typeof item === 'string' && tokenTester(item) ) flag = true
+ if (item && typeof item === 'object' && Object.keys(item).some(ele => tokenTester(ele))) flag = true
+ })
+ if (flag) return;
const timestamp = new Date().toUTCString()
switch (action.payload.level) {
case "error":
diff --git a/src/main/sagas/removeFileSaga.ts b/src/main/sagas/removeFileSaga.ts
index b04271a5..6b5b3232 100644
--- a/src/main/sagas/removeFileSaga.ts
+++ b/src/main/sagas/removeFileSaga.ts
@@ -1,4 +1,4 @@
-import { takeEvery, put } from "redux-saga/effects";
+import { takeEvery, put, call } from "redux-saga/effects";
import { getType, ActionType } from "typesafe-actions";
import * as fsExtra from 'fs-extra'
import { RemoveFileActions } from "../../shared/actions/removeFile";
@@ -6,8 +6,9 @@ import { RemoveFileActions } from "../../shared/actions/removeFile";
export function* removeFileSaga() {
yield takeEvery(getType(RemoveFileActions.removeSharedFile), function* (action: ActionType) {
- fsExtra.removeSync(action.payload)
- yield put (RemoveFileActions.fileRemoved())
+ yield call(() => fsExtra.remove(action.payload))
+
+ yield put(RemoveFileActions.fileRemoved())
})
}
diff --git a/src/main/sagas/startViewSharedFilesSaga.ts b/src/main/sagas/startViewSharedFilesSaga.ts
index 1f051aca..a7a83f8f 100644
--- a/src/main/sagas/startViewSharedFilesSaga.ts
+++ b/src/main/sagas/startViewSharedFilesSaga.ts
@@ -1,5 +1,15 @@
-import { v4 as uuid } from 'uuid';
-import { takeEvery, call, put, select, actionChannel, flush, fork, take, race } from "redux-saga/effects";
+import { v4 as uuid } from "uuid";
+import {
+ takeEvery,
+ call,
+ put,
+ select,
+ actionChannel,
+ flush,
+ fork,
+ take,
+ race,
+} from "redux-saga/effects";
import { getType, ActionType } from "typesafe-actions";
import { DashboardActions } from "../../shared/actions/dashboard";
import { RpcClient } from "../RpcClient";
@@ -7,124 +17,193 @@ import { GetUserInfo } from "../../dynamicdInterfaces/GetUserInfo";
import { MainRootState } from "../reducers";
import { FileListActions } from "../../shared/actions/fileList";
import { SharedFilesActions } from "../../shared/actions/sharedFiles";
-import { FileListMessage } from '../../shared/types/FileListMessage';
-import { getUserNameFromFqdn } from '../../shared/system/getUserNameFromFqdn';
-import { SharedFile } from '../../shared/types/SharedFile';
-import { entries } from '../../shared/system/entries';
-import { PublicSharedFile } from '../../shared/types/PublicSharedFile';
-import { BdapActions } from '../../shared/actions/bdap';
-import { LinkMessageEnvelope } from '../../shared/actions/payloadTypes/LinkMessageEnvelope';
-import { delay } from 'redux-saga';
+import { FileListMessage } from "../../shared/types/FileListMessage";
+import { getUserNameFromFqdn } from "../../shared/system/getUserNameFromFqdn";
+import { SharedFile } from "../../shared/types/SharedFile";
+import { entries } from "../../shared/system/entries";
+import { PublicSharedFile } from "../../shared/types/PublicSharedFile";
+import { BdapActions } from "../../shared/actions/bdap";
+import { delay } from "redux-saga";
+import { FileNavigationActions } from "../../shared/actions/fileNavigation";
+import { FileSharingActions } from "../../shared/actions/fileSharing";
+import {
+ FileListRequest,
+ isFileListRequest,
+} from "../../shared/actions/payloadTypes/FileListRequest";
+import { RtcActions } from "../../shared/actions/rtc";
export function* startViewSharedFilesSaga(rpcClient: RpcClient) {
- yield fork(function* () {
- yield take(getType(BdapActions.initialize))
+ yield fork(function*() {
+ yield take(getType(BdapActions.initialize));
const pred = (action: BdapActions) => {
switch (action.type) {
case getType(BdapActions.linkMessageReceived):
- return action.payload.message.type === "pshare-filelist-request";
+ return (
+ action.payload.message.type ===
+ "pshare-filelist-request"
+ );
default:
return false;
}
- }
-
- yield takeEvery(pred, function* (action: ActionType) {
- const msg = action.payload
- const { id } = msg.message
- const senderFqdn = msg.rawMessage.sender_fqdn
- const sender = getUserNameFromFqdn(senderFqdn)
+ };
+
+ yield takeEvery(pred, function*(
+ action: ActionType
+ ) {
+ const msg = action.payload;
+ const { id } = msg.message;
+ const senderFqdn = msg.rawMessage.sender_fqdn;
+ const sender = getUserNameFromFqdn(senderFqdn);
if (sender) {
- const filesRecord: Record = yield select((s: MainRootState) => {
- if (s.fileWatch.users[sender]) {
- return s.fileWatch.users[sender].out;
+ const filesRecord: Record = yield select(
+ (s: MainRootState) => {
+ if (s.fileWatch.users[sender]) {
+ return s.fileWatch.users[sender].out;
+ } else {
+ return {};
+ }
}
- else {
- return {}
- }
- })
- const sharedFiles: PublicSharedFile[] =
- entries(filesRecord)
- .select(([fileName, v]) => ({
- fileName,
- hash: v.hash!,
- size: v.size!,
- contentType: v.contentType!
- }))
- .toArray()
+ );
+ const sharedFiles: PublicSharedFile[] = entries(filesRecord)
+ .select(([fileName, v]) => ({
+ fileName,
+ //hash: v.hash!,
+ size: v.size!,
+ contentType: v.contentType!,
+ }))
+ .toArray();
const fileListMessage: FileListMessage = {
files: sharedFiles,
- id
- }
- yield put(BdapActions.sendLinkMessage({ recipient: sender, payload: { id: uuid(), timestamp: Math.trunc((new Date()).getTime()), type: "pshare-filelist", payload: fileListMessage } }))
+ id,
+ };
+ yield put(
+ BdapActions.sendLinkMessage({
+ recipient: sender,
+ payload: {
+ id: uuid(),
+ timestamp: Math.trunc(new Date().getTime()),
+ type: "pshare-filelist",
+ payload: fileListMessage,
+ },
+ })
+ );
}
- })
-
-
- })
- yield takeEvery(getType(DashboardActions.startViewSharedFiles), function* (action: ActionType) {
-
- const linkedUserName = action.payload
- const linkedUserInfo: GetUserInfo = yield call(() => rpcClient.command("getuserinfo", linkedUserName))
- yield put(DashboardActions.viewSharedFiles(linkedUserInfo))
- const chan = yield actionChannel(getType(SharedFilesActions.close))
- const checkClosed = function* () {
- const closeActions: any[] = yield flush(chan)
- return closeActions.some(x => true)
- }
- let fileListMessage: FileListMessage;
+ });
+ });
+ yield takeEvery(getType(DashboardActions.startViewSharedFiles), function*(
+ action: ActionType
+ ) {
+ yield put(FileNavigationActions.goRoot({ type: "downloadableFiles" }));
+ yield put(FileNavigationActions.goRoot({ type: "sharedFiles" }));
+ const linkedUserName = action.payload;
+ const linkedUserInfo: GetUserInfo = yield call(() =>
+ rpcClient.command("getuserinfo", linkedUserName)
+ );
+ yield put(DashboardActions.viewSharedFiles(linkedUserInfo));
+ const chan = yield actionChannel(getType(SharedFilesActions.close));
+ const checkClosed = function*() {
+ const closeActions: any[] = yield flush(chan);
+ return closeActions.some(x => true);
+ };
+ let fileListMessage: PublicSharedFile[];
try {
- fileListMessage = yield call(() => getSharedFileListForLink(linkedUserName))
+ fileListMessage = yield call(() =>
+ getSharedFileListForLink(linkedUserName)
+ );
} catch (err) {
- yield put(FileListActions.fileListFetchFailed())
-
- return
+ if (yield* checkClosed()) {
+ return;
+ }
+ yield put(FileListActions.fileListFetchFailed());
+
+ return;
}
if (yield* checkClosed()) {
- return
+ return;
}
- const fileList = fileListMessage.files
- yield put(FileListActions.fileListFetchSuccess(fileList))
+ yield put(FileListActions.fileListFetchSuccess(fileListMessage));
//yield put(DashboardActions.viewSharedFiles(linkedUserInfo))
-
- })
+ });
}
-
-
-
function* getSharedFileListForLink(linkedUserName: string) {
- const msgId = uuid()
- yield put(BdapActions.sendLinkMessage({ recipient: linkedUserName, payload: { id: msgId, timestamp: Math.trunc((new Date()).getTime()), type: "pshare-filelist-request", payload: { id: msgId } } }))
-
-
- const pred = (action: BdapActions) => {
+ const msgId = uuid();
+ const myUserName: string = yield select(
+ (s: MainRootState) => s.user.userName!
+ );
+
+ const req: FileListRequest = {
+ fileName: "file-list",
+ ownerUserName: linkedUserName,
+ requestorUserName: myUserName,
+ requestId: msgId,
+ type: "file-list",
+ };
+ yield put(FileSharingActions.startRequestFile(req));
+
+ // yield put(
+ // BdapActions.sendLinkMessage({
+ // recipient: linkedUserName,
+ // payload: {
+ // id: msgId,
+ // timestamp: Math.trunc(new Date().getTime()),
+ // type: "pshare-filelist-request",
+ // payload: { id: msgId },
+ // },
+ // })
+ // );
+
+ const successPredicate = (action: FileSharingActions) => {
switch (action.type) {
- case getType(BdapActions.linkMessageReceived):
- return action.payload.message.payload.id === msgId && action.payload.message.type === "pshare-filelist";
+ case getType(FileSharingActions.fileListResponse):
+ return action.payload.requestId === msgId;
default:
return false;
}
- }
- const { action, timeout }: { action: ActionType, timeout: unknown } = yield race({
- timeout: delay(20000),
- action: take(pred)
- })
+ };
+ const failurePredicate = (action: RtcActions) => {
+ switch (action.type) {
+ case getType(RtcActions.fileReceiveFailed):
+ return (
+ isFileListRequest(action.payload.fileRequest) &&
+ action.payload.fileRequest.requestId === msgId
+ );
+ default:
+ return false;
+ }
+ };
+ const {
+ successAction,
+ failureAction,
+ timeout,
+ abort,
+ }: {
+ successAction: ActionType;
+ failureAction: ActionType;
+ timeout: unknown;
+ abort: unknown;
+ } = yield race({
+ timeout: delay(1200000),
+ abort: take(getType(DashboardActions.startViewSharedFiles)),
+ successAction: take(successPredicate as any),
+ failureAction: take(failurePredicate as any),
+ });
if (timeout) {
- throw Error("timeout")
+ throw Error("timeout");
+ }
+ if (abort) {
+ console.log("file-list-fetch aborted");
+ return;
+ }
+ if (failureAction) {
+ throw failureAction.payload.error;
}
-
-
//const action: ActionType = yield take(pred)
-
- const { payload: fileListMessage }: LinkMessageEnvelope = action.payload.message
-
-
- return fileListMessage
-
-
-}
\ No newline at end of file
+ console.log("got shared files", successAction);
+ return successAction.payload.sharedFiles;
+}
diff --git a/src/main/store/hot-reload/runRootSagaWithHotReload.ts b/src/main/store/hot-reload/runRootSagaWithHotReload.ts
index 3a86946a..465939a8 100644
--- a/src/main/store/hot-reload/runRootSagaWithHotReload.ts
+++ b/src/main/store/hot-reload/runRootSagaWithHotReload.ts
@@ -20,6 +20,8 @@ import { actionLoggingSaga } from "../../sagas/actionLoggingSaga";
import { remoteLoggingSaga } from "../../sagas/remoteLoggingSaga";
import { EventDispatcher } from "../../../shared/system/events/EventDispatcher";
import { createStoreWithHotReload } from "./createStoreWithHotReload";
+import * as path from 'path'
+
export function runRootSagaWithHotReload(sagaMw: SagaMiddleware<{}>, browserWindowProvider: BrowserWindowProvider, sagaMonitor: EventDispatcher, store: ReturnType) {
let rpcClient: RpcClientWrapper | undefined;
@@ -39,13 +41,15 @@ export function runRootSagaWithHotReload(sagaMw: SagaMiddleware<{}>, browserWind
const cancellationTokenSource = createCancellationTokenSource()
sagaMonitor.addEventListener("error", e => {
console.log("sagamonitor error")
+
cancellationTokenSource.cancel().then(() => {
store.dispatch(AppActions.terminated());
+ const logPath = path.join(app.getPath("home"), ".pshare", "logs", "pshare.log");
const messageBoxOptions: MessageBoxOptions = {
type: "error",
title: "Error",
- message: `Oops, it looks like something's gone wrong`,
- detail: `Sorry, the application will now close.\nMaybe restarting will help.${e.message ? `\nThe error was : ${e.message}` : ""}`,
+ message: `An unrecoverable error has occurred`,
+ detail: `Sorry, the application will now close.${e.message ? `\n\nThe error was\n\n"${e.message}"` : ""}\n\nA log file can be found at\n\n${logPath}\n\nIf the problem persists, please get in touch with us at\n\nhttps://discord.gg/87be63e`,
normalizeAccessKeys: true,
buttons: ["&Ok"],
noLink: true,
diff --git a/src/main/system/divertConsoleToLogger.ts b/src/main/system/divertConsoleToLogger.ts
index 9ff77bff..580611cf 100644
--- a/src/main/system/divertConsoleToLogger.ts
+++ b/src/main/system/divertConsoleToLogger.ts
@@ -1,5 +1,12 @@
import { prepareErrorForSerialization } from '../../shared/proxy/prepareErrorForSerialization';
import { getLogger } from './getLogger';
+
+const passwordRegex = new RegExp('password','i')
+const passphraseRegex = new RegExp('passphrase','i')
+const mnemonicRegex = new RegExp('mnemonic','i')
+
+const tokenTester = (item: string) => (passwordRegex.test(item) || passphraseRegex.test(item) || mnemonicRegex.test(item))
+
export async function divertConsoleToLogger() {
const logger = await getLogger()
if (typeof logger === 'undefined') {
@@ -11,23 +18,47 @@ export async function divertConsoleToLogger() {
const originalConsoleError = console.error.bind(console);
console.log = (...args: any) => {
const timestamp = new Date().toUTCString()
- logger.info(["Main console", timestamp, ...args]);
+ let flag = false
+ args.map((item:any) => {
+ if (item && typeof item === 'string' && tokenTester(item))
+ flag = true;
+ })
originalConsoleLog(...args);
+ if(flag) return;
+ logger.info(["Main console", timestamp, ...args]);
};
console.warn = (...args: any) => {
const timestamp = new Date().toUTCString()
- logger.warn(["Main console", timestamp, ...args]);
+ let flag = false
+ args.map((item:any) => {
+ if (item && typeof item === 'string' && tokenTester(item))
+ flag = true;
+ })
originalConsoleWarn(...args);
+ if(flag) return;
+ logger.warn(["Main console", timestamp, ...args]);
};
console.info = (...args: any) => {
const timestamp = new Date().toUTCString()
- logger.info(["Main console", timestamp, ...args]);
+ let flag = false
+ args.map((item:any) => {
+ if (item && typeof item === 'string' && tokenTester(item))
+ flag = true;
+ })
originalConsoleInfo(...args);
+ if(flag) return;
+ logger.info(["Main console", timestamp, ...args]);
};
console.error = (...args: any) => {
const timestamp = new Date().toUTCString()
- logger.error(["Main console", timestamp, ...args]);
+ let flag = false
+ args.map((item:any) => {
+ if (item && typeof item === 'string' && tokenTester(item))
+ flag = true;
+ })
originalConsoleError(...args);
+ if(flag) return;
+ logger.error(["Main console", timestamp, ...args]);
};
process.on('uncaughtException', (err) => {
const e = prepareErrorForSerialization(err);
diff --git a/src/main/system/getAllFilePaths.ts b/src/main/system/getAllFilePaths.ts
new file mode 100644
index 00000000..145f545c
--- /dev/null
+++ b/src/main/system/getAllFilePaths.ts
@@ -0,0 +1,34 @@
+import { promisify } from 'util';
+import * as fs from 'fs';
+import * as path from 'path'
+
+export async function getAllFilePaths(filePaths: string[], visited: Set | undefined = undefined): Promise {
+ if (filePaths.length === 0) {
+ return [];
+ }
+ const visitedSet = visited || new Set();
+ const files: string[] = [];
+ for (const pth of filePaths) {
+ const normalizedPath = path.normalize(pth)
+ if (visitedSet.has(normalizedPath)) {
+ continue;
+ }
+ visitedSet.add(normalizedPath);
+ const pathExists = await exists(normalizedPath);
+ if (!pathExists) {
+ continue;
+ }
+ const stat: fs.Stats = await fs.promises.stat(normalizedPath);
+ if (stat.isFile()) {
+ files.push(normalizedPath);
+ }
+ else if (stat.isDirectory()) {
+ const dirContents = await fs.promises.readdir(normalizedPath);
+ const dirContentsPaths = dirContents.map(p => path.join(normalizedPath, p));
+ const dPaths = await getAllFilePaths(dirContentsPaths, visitedSet);
+ files.push(...dPaths);
+ }
+ }
+ return files;
+}
+const exists = promisify(fs.exists);
diff --git a/src/main/validation/userNameValidationRules.ts b/src/main/validation/userNameValidationRules.ts
index a4e5fc20..97109d99 100644
--- a/src/main/validation/userNameValidationRules.ts
+++ b/src/main/validation/userNameValidationRules.ts
@@ -3,7 +3,7 @@ import { GetUserInfo } from "../../dynamicdInterfaces/GetUserInfo";
import { RpcClient } from "../RpcClient";
const isLongEnough = (client: RpcClient, value: string) => value.length >= 3;
-const isValidCharacters = (client: RpcClient, value: string) => /^[A-Za-z0-9]+$/.test(value);
+const isValidCharacters = (client: RpcClient, value: string) => /^[a-z0-9]+$/.test(value);
const userNameDoesNotExist = async (client: RpcClient, value: string) => {
let userInfo: GetUserInfo;
@@ -34,7 +34,7 @@ export const userNameValidationRules: ValidationTest[] = [
testsOnSuccess: [
{
test: isValidCharacters,
- message: "Value may only contain letters and numbers",
+ message: "User name may only contain smaller case alphanumeric",
testsOnSuccess: [{
test: userNameDoesNotExist,
message: "User name is already taken"
diff --git a/src/renderer/assets/svgs/copy-32.svg b/src/renderer/assets/svgs/copy-32.svg
new file mode 100644
index 00000000..94bb70a7
--- /dev/null
+++ b/src/renderer/assets/svgs/copy-32.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/assets/svgs/export.svg b/src/renderer/assets/svgs/export.svg
new file mode 100644
index 00000000..3181c3b3
--- /dev/null
+++ b/src/renderer/assets/svgs/export.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/assets/svgs/folder-blue.svg b/src/renderer/assets/svgs/folder-blue.svg
new file mode 100644
index 00000000..72f50ffb
--- /dev/null
+++ b/src/renderer/assets/svgs/folder-blue.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/assets/svgs/goback.svg b/src/renderer/assets/svgs/goback.svg
new file mode 100644
index 00000000..ad0ce7f8
--- /dev/null
+++ b/src/renderer/assets/svgs/goback.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/assets/svgs/right-chevron.svg b/src/renderer/assets/svgs/right-chevron.svg
new file mode 100644
index 00000000..5933ee50
--- /dev/null
+++ b/src/renderer/assets/svgs/right-chevron.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/components/dashboard/AddFile.tsx b/src/renderer/components/dashboard/AddFile.tsx
index 5c9cc8bc..98445491 100644
--- a/src/renderer/components/dashboard/AddFile.tsx
+++ b/src/renderer/components/dashboard/AddFile.tsx
@@ -6,7 +6,8 @@ import { Text } from "../ui-elements/Text";
import Container from "../ui-elements/Container";
import { FilePathInfo } from "../../../shared/types/FilePathInfo";
import { Dropzone, DropzoneError } from "../ui-elements/Dropzone";
-import { maximumFileSize } from "../../../shared/system/maximumFileSize";
+import BalanceIndicator from "../../../renderer/containers/dashboard/BalanceIndicator";
+//import { maximumFileSize } from "../../../shared/system/maximumFileSize";
@@ -17,27 +18,31 @@ export interface AddFileStateProps {
export interface AddFilesDispatchProps {
close: () => void
filesSelected: (files: FilePathInfo[]) => void
+ //directoriesSelected: (directories: string[]) => void
}
export type AddFileProps = AddFileStateProps & AddFilesDispatchProps
-export const AddFile: FunctionComponent = ({ close, filesSelected, linkedUserCommonName }) => {
+export const AddFile: FunctionComponent = ({ close, filesSelected, linkedUserCommonName }) => {
// react hooks FTW!!!!
- const [error, setError] = useState(undefined)
+ const [
+ error,
+ //setError
+ ] = useState(undefined)
const userNameParts = linkedUserCommonName.split(' ')
const lastName = (userNameParts.length > 1) ? userNameParts[userNameParts.length - 1] : ""
const firstName = userNameParts.length > 1 ? userNameParts.slice(0, -1).join(' ') : userNameParts[0]
const filesSelectedHandler = (files: FilePathInfo[]) => {
- if (files.some(f => f.size > maximumFileSize)) {
- setError({ title: "File too large!", message: "please select or drag a file that is no larger than 3gb" })
- return
- }
filesSelected(files)
}
- return <>
-
+ const directoriesSelectedHander = (directories: FilePathInfo[]) => {
+ filesSelected(directories)
+ }
+ return
+
+
@@ -54,13 +59,13 @@ export const AddFile: FunctionComponent
= ({ close, filesSelected,
Add file
-
+
File size limit: 3gb
- >;
+ ;
}
diff --git a/src/renderer/components/dashboard/AddLinks.tsx b/src/renderer/components/dashboard/AddLinks.tsx
index 4e4b9b58..6c899b5a 100644
--- a/src/renderer/components/dashboard/AddLinks.tsx
+++ b/src/renderer/components/dashboard/AddLinks.tsx
@@ -1,7 +1,7 @@
import { Component } from "react";
import React from "react";
import { H1, Text } from "../ui-elements/Text";
-import { AddLinksIcon, UserListAvatar, CloseIcon, BtnAddLinksIcon, RequestSentIcon } from "../ui-elements/Image";
+import { UserListAvatar, CloseIcon, BtnAddLinksIcon, RequestSentIcon } from "../ui-elements/Image";
import { UserList, UserListItem } from "../ui-elements/Dashboard";
import man from "../../assets/man.svg";
import Container from "../ui-elements/Container";
@@ -14,6 +14,8 @@ import Modal from "../ui-elements/Modal";
import Button from "../ui-elements/Button";
import Input from "../ui-elements/Input";
import { SearchActions } from "../../../shared/actions/search";
+// import { BulkImportActions } from "../../../shared/actions/bulkImport";
+import BalanceIndicator from "../../containers/dashboard/BalanceIndicator";
type SearchStatus = "NO_SEARCH" | "SEARCH_RESULT"
export interface AddLinksStateProps {
@@ -23,7 +25,11 @@ export interface AddLinksStateProps {
status: SearchStatus
}
-export type AddLinksDispatchProps = PickedDispatchProps & PickedDispatchProps & { push: (pathname: string) => void }
+export type AddLinksDispatchProps =
+ PickedDispatchProps
+ // & PickedDispatchProps
+ & PickedDispatchProps
+ & { push: (pathname: string) => void }
export type AddLinksProps = AddLinksStateProps & AddLinksDispatchProps
interface AddLinksComponentStateProps {
@@ -33,7 +39,6 @@ interface AddLinksComponentStateProps {
interface CustomRequestMessageProps {
close: () => void,
send: (msg: string) => void,
-
}
interface CustomRequestMessageComponentState {
msg: string
@@ -94,27 +99,32 @@ export class AddLinks extends Component }
-
+
+
push('/Dashboard/MyLinks')} />
finish
- Add Links
-
-
addLinksQueryTextChanged(e.target.value)}
- margin="20px 0 20px 0"
- padding="0 20px"
- />
-
0 ? "visible" : "hidden",
- margin:'30px 0 0 0'
- }}
+
+ {/* */}
+ Add Links
+
+ addLinksQueryTextChanged(e.target.value)}
+ margin="20px 0 20px 0"
+ padding="0 20px"
+ autoFocus={true}
+ />
+ 0 ? "visible" : "hidden",
+ margin: '30px 0 0 0'
+ }}
onClick={() => {
addLinksQueryTextChanged("");
document.getElementById("addLinksInput")!.focus()
- }} />
+ }} />
{/*
@@ -129,7 +139,7 @@ export class AddLinks extends Component this.setState(x))
+ renderResults(queryText, status, users, x => this.setState(x), push)
}
@@ -140,24 +150,34 @@ export class AddLinks extends Component void) => {
+const renderResults = (queryText: string, status: string, users: BdapUser[], setState: (x: AddLinksComponentStateProps) => void, push: (pathname: string) => void) => {
switch (status) {
case "NO_SEARCH":
return queryText.length === 0
- ? Type some characters to find other users...
+ ? <>
+ Type some characters to find other users...
+
+ {
+ event.preventDefault();
+ push("/Dashboard/BulkImport")
+ }}
+ style={{ cursor: 'pointer', color: '#2e77d0' }}
+ >Bulk invite from file...
+ >
: Type some more characters to find other users...
case "SEARCH_RESULT":
return users.length > 0
- ?
- {users.map(u =>
-
+ ?
+ {users.map(u => setState({ requestModal: true, recipent: u.userName }) }>
+
{u.state === 'pending' ?
Request sent
- : setState({ requestModal: true, recipent: u.userName })}>
+ :
Request
}
diff --git a/src/renderer/components/dashboard/BalanceIndicator.tsx b/src/renderer/components/dashboard/BalanceIndicator.tsx
new file mode 100644
index 00000000..1029abca
--- /dev/null
+++ b/src/renderer/components/dashboard/BalanceIndicator.tsx
@@ -0,0 +1,97 @@
+import { FunctionComponent, useState, useRef, CSSProperties, useCallback } from "react";
+import React from "react";
+import { QRCode } from "../ui-elements/QRCode"
+import { Text } from "../ui-elements/Text";
+import { useOutsideAlerter } from "../../system/useOutsideAlerter";
+import copyIcon from "../../assets/svgs/copy-32.svg"
+
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { delay } from "../../../shared/system/delay";
+import { PickedDispatchProps } from "../../../renderer/system/PickedDispatchProps";
+import { BdapActions } from "../../../shared/actions/bdap";
+
+export interface BalanceIndicatorStateProps {
+ balance: number
+ walletAddress: string
+ errorMessage?: string
+ hideLinkWhenMinimized?: boolean
+}
+export type BalanceIndicatorDispatchProps = PickedDispatchProps
+export type BalanceIndicatorProps = BalanceIndicatorStateProps & BalanceIndicatorDispatchProps
+export const BalanceIndicator: FunctionComponent = ({ balance, walletAddress, errorMessage, fundsDialogDismissed, hideLinkWhenMinimized }) => {
+ const [visible, setVisible] = useState(false)
+ const [copied, setCopied] = useState(false);
+ const elemRef = useRef(null);
+
+ const isVisible = visible || errorMessage;
+
+ const cb = useCallback((ev: MouseEvent) => {
+ if (isVisible) {
+ setVisible(false)
+ ev.preventDefault()
+ ev.stopPropagation()
+ fundsDialogDismissed()
+ }
+ }, [isVisible])
+ useOutsideAlerter(elemRef, cb);
+ const borderStyle = isVisible ? "solid 2px #ccc" : "none";
+
+ const outerStyle: CSSProperties = {
+ padding: '6px',
+ display: "block",
+ position: "absolute",
+ zIndex: 10000,
+ top: 0,
+ left: 0,
+ borderBottom: borderStyle,
+ borderRight: borderStyle,
+ backgroundColor: isVisible ? "#ffffff" : "none",
+ borderBottomRightRadius: "8px"
+ }
+
+ return <>
+ {((!hideLinkWhenMinimized) || isVisible) &&
+
+ {
+ e.preventDefault();
+ console.log("balanceIndicator clicked");
+ const show = !isVisible;
+ setVisible(show)
+ if (!show) {
+ fundsDialogDismissed()
+ }
+ }}
+ style={{ cursor: 'pointer', color: (balance > 60) ? '#2e77d0' : 'red' }}
+ >Balance : {balance} credits
+
+ {isVisible && <>
+ {errorMessage &&
{errorMessage}
}
+
+
+
+
+ {copied &&
COPIED TO CLIPBOARD
}
+
{walletAddress}
+
{
+ setCopied(true)
+ await delay(2000)
+ setCopied(false)
+ }}>
+
+
+
+
+ >}
+
}
+
+ >
+}
\ No newline at end of file
diff --git a/src/renderer/components/dashboard/BulkImport.tsx b/src/renderer/components/dashboard/BulkImport.tsx
new file mode 100644
index 00000000..2bbad05a
--- /dev/null
+++ b/src/renderer/components/dashboard/BulkImport.tsx
@@ -0,0 +1,145 @@
+import { FunctionComponent, useState } from "react";
+import React from "react";
+import { Box } from "../ui-elements/Box";
+import { CloseIcon } from "../ui-elements/Image";
+import { Text } from "../ui-elements/Text";
+import Container from "../ui-elements/Container";
+import { FilePathInfo } from "../../../shared/types/FilePathInfo";
+import { Dropzone, DropzoneError } from "../ui-elements/Dropzone";
+import Button from "../ui-elements/Button";
+import { UserList, UserListItem } from "../ui-elements/Dashboard";
+import { RequestStatus } from "../../../main/sagas/bulkImportSaga";
+
+
+
+export interface BulkImportStateProps {
+ data: string,
+ fqdnData: RequestStatus[],
+ err: boolean
+}
+
+export interface BulkImportsDispatchProps {
+ push: (pathname:string) => void,
+ previewBulkImport: (filepath: FilePathInfo) => void
+ beginBulkImport: (data: string) => void
+}
+
+export type BulkImportProps = BulkImportStateProps & BulkImportsDispatchProps
+
+export const BulkImport: FunctionComponent = ({ err, data, push, previewBulkImport, beginBulkImport, fqdnData }) => {
+ // react hooks FTW!!!!
+ const [
+ error,
+ // setError
+ ] = useState(undefined)
+
+ const [
+ status, setStatus
+ ] = useState('dropzone') // default dropzone;
+
+
+ // currently select only one file
+ const filesSelectedHandler = (files: FilePathInfo[]) => {
+ if(error) {
+ return;
+ }
+ setStatus('preview');
+ const file = files[0];
+ previewBulkImport(file)
+ }
+
+ const renderBody = (status: string, fqdnData: RequestStatus[]) => {
+ switch(status) {
+ case "dropzone":
+ return (<>
+
+
+ close push("/Dashboard/AddLinks")} />
+
+
+
+
+
+ Bulk Import File
+
+
+
+
+
+ >);
+ case "preview":
+ return (<>
+
+
+
+
+ Bulk Import Links Preview
+
+
+ setStatus('dropzone')} width="100px" margin="0 1em 0 0">
+ Cancel
+
+ {
+ beginBulkImport(data);
+ setStatus('result')
+ }} primary width="100px">
+ Send Link Requests
+
+
+
+ {data.split('\n').map((item, idx) => item.length > 0 ?
+
+ {item}
+ : ''
+ )}
+
+
+
+
+ >);
+ case "result":
+ return (<>
+
+
+
+
+ Bulk Import Result
+
+ push('/Dashboard/MyLinks')} margin="1em 0 0 0">
+ Go Back to MyLinks
+
+
+ {err ?
+
+ You have run out of funds. All further link requests are aborted.
+
+ : ''}
+
+ Link
+ Status
+
+
+ {fqdnData.map((item,idx) =>
+
+ {item.link}
+ {item.status}
+
+ )}
+
+
+
+
+ >);
+ default: return;
+ }
+ }
+
+ return
+
+ {renderBody(status, fqdnData)}
+
+
;
+}
+
+
diff --git a/src/renderer/components/dashboard/ClientDownloads.tsx b/src/renderer/components/dashboard/ClientDownloads.tsx
index cbaa8407..3181cd43 100644
--- a/src/renderer/components/dashboard/ClientDownloads.tsx
+++ b/src/renderer/components/dashboard/ClientDownloads.tsx
@@ -10,6 +10,8 @@ import man from "../../assets/man.svg";
import { LinkDisplayName } from "./LinkDisplayName";
import { prettySize } from "../../../shared/system/prettySize";
import CircularProgress from "../ui-elements/CircularProgress";
+import BalanceIndicator from "../../containers/dashboard/BalanceIndicator"
+import { prettyTime } from "../../../shared/system/prettyTime";
export interface ClientDownloadsDispatchProps {
@@ -19,7 +21,8 @@ export interface ClientDownloadsStateProps {
}
export type ClientDownloadsProps = ClientDownloadsDispatchProps & ClientDownloadsStateProps
export const ClientDownloads: FunctionComponent = ({ currentSessions }) => <>
-
+
+
@@ -37,14 +40,14 @@ export const ClientDownloads: FunctionComponent = ({ curre
const sessionEntries = entries(currentSessions);
if (!sessionEntries.any()) {
return (<>Nobody is downloading data from you
- >)
+ >)
}
else {
return sessionEntries
.select(([key, downloadState]) => {
return (
-
+
@@ -65,11 +68,11 @@ export const ClientDownloads: FunctionComponent
= ({ curre
margin: '0',
width: '0.4px'
}} />
-
-
- {`${downloadState.progressPct}%`}
-
-
+
+
+ {`${downloadState.progressPct}%`}{downloadState.eta!=null&&<> {prettySize(downloadState.speed)}/s ({prettyTime(downloadState.eta!)})>}
+
+
= ({ invites, beginAcceptLink, beginDeclineLink }: InvitesProps) =>
<>
-
+
+
Invites
-
- {invites.length>0 ?
-
- {invites.map(({ user, link, link_message }, idx) =>
-
-
-
-
-
-
-
-
{
- e.stopPropagation();
- beginAcceptLink(link);
- }}>
- Accept
+ }} />
+ {invites.length > 0 ?
+
+ {invites.map(({ user, link, link_message }, idx) =>
+
+
+
+
+
+
+
+ {
+ e.stopPropagation();
+ beginAcceptLink(link);
+ }}>
+ Accept
- Decline
-
-
-
- )}
- :
-
+ Decline
+
+
+
+ )}
+ :
+
You have no invites yet 😕
-
+
}
diff --git a/src/renderer/components/dashboard/MyLinks.tsx b/src/renderer/components/dashboard/MyLinks.tsx
index 6204f382..db5baeaa 100644
--- a/src/renderer/components/dashboard/MyLinks.tsx
+++ b/src/renderer/components/dashboard/MyLinks.tsx
@@ -1,83 +1,108 @@
import { FunctionComponent } from "react";
import React from "react";
import { H1, Text } from "../ui-elements/Text";
-import { MyLinksIcon, UserListAvatar, PendingIcon, BtnAddLinksIcon, ViewBtnIcon, CloseIcon } from "../ui-elements/Image";
+import { MyLinksIcon, UserListAvatar, PendingIcon, BtnAddLinksIcon, ViewBtnIcon, CloseIcon, ExportIcon } from "../ui-elements/Image";
import { UserList, UserListItem } from "../ui-elements/Dashboard";
import man from "../../assets/man.svg";
import Container from "../ui-elements/Container";
import { BdapUser } from "../../system/BdapUser";
import { LinkDisplayName } from "./LinkDisplayName";
import Input from "../ui-elements/Input";
-
+import BalanceIndicator from "../../containers/dashboard/BalanceIndicator"
export interface MyLinksStateProps {
users: BdapUser[],
+ allUsers: BdapUser[],
userName: string,
- queryText: string
+ queryText: string,
+ balance: number
}
export interface MyLinksDispatchProps {
push: (pathname: string) => void,
startViewSharedFiles: (userName: string) => void
myLinksQueryTextChanged: (value: string) => void
+ exportMyLinks: () => void
}
export type MyLinksProps = MyLinksStateProps & MyLinksDispatchProps
-export const MyLinks: FunctionComponent
= ({ users, push, startViewSharedFiles, userName, myLinksQueryTextChanged, queryText }: MyLinksProps) =>
- <>
-
-
Add Links
-
push('/Dashboard/AddLinks')} />
+export const MyLinks: FunctionComponent = ({ users, push, startViewSharedFiles, userName, myLinksQueryTextChanged, queryText, allUsers, balance, exportMyLinks }: MyLinksProps) => {
+
+
+ return <>
+
+
+
+ Add Links
+
push('/Dashboard/AddLinks')} />
+
+ Export Links
+ exportMyLinks()}/>
+
My Links ({userName})
- {users.length>0 ?
- <>
-
-
myLinksQueryTextChanged(e.target.value)}
- margin="20px 0 20px 0"
- padding="0 20px"
- />
- {/*
-
- NOTE
-
- adding and removing the element below with a ternary statement
- causes measurable performance issues
+ {allUsers.length > 0 ?
+ <>
+
+ myLinksQueryTextChanged(e.target.value)}
+ margin="20px 0 20px 0"
+ padding="0 20px"
+ />
+ {/*
- toggling css visibility below is an optimization that doesn't
- cause document reflow
-
-
- */}
- 0 ? "visible" : "hidden",
- margin:'30px 0 0 0'
- }}
- onClick={() => {
- myLinksQueryTextChanged("");
- document.getElementById("myLinksInput")!.focus()
- }} />
-
-
- {users.map(u =>
-
-
-
-
-
- {u.state === 'pending'
- ?
- : View startViewSharedFiles(u.userName)} width="30px" height="30px" margin="0 0 0 1em" />
}
- {/* requestFile({ fileId: "foo", ownerUserName: u.userName, requestorUserName: userName })} primary width="102px" minHeight="30px" fontSize="0.8em" > Request Test > */}
-
- )}
-
+ NOTE
+
+ adding and removing the element below with a ternary statement
+ causes measurable performance issues
+
+ toggling css visibility below is an optimization that doesn't
+ cause document reflow
+
+
+ */}
+
0 ? "visible" : "hidden",
+ margin: '30px 0 0 0'
+ }}
+ onClick={() => {
+ myLinksQueryTextChanged("");
+ document.getElementById("myLinksInput")!.focus()
+ }} />
+
+ {
+ users.length > 0
+ ?
+
+ {users.map(u =>
+ {} : u.state === 'pending-invite'
+ ? () => push('/Dashboard/Invites')
+ : () => startViewSharedFiles(u.userName)} >
+
+
+
+
+ {u.state === 'pending'
+ ?
+ : u.state === 'pending-invite'
+ ?
+ : View
}
+ {/* requestFile({ fileId: "foo", ownerUserName: u.userName, requestorUserName: userName })} primary width="102px" minHeight="30px" fontSize="0.8em" > Request Test > */}
+
+ )}
+
+
+ : No results.
+ }
> :
<>
- You don't have any links yet .
- Go ahead, add someone to your list by clicking Add Links at the top-right.
+ You don't have any links yet .
+ Go ahead, add someone to your list by clicking Add Links at the top-right.
>
}
>
+}
diff --git a/src/renderer/components/dashboard/SharedFiles.tsx b/src/renderer/components/dashboard/SharedFiles.tsx
index 0eacd455..6a4d47f7 100644
--- a/src/renderer/components/dashboard/SharedFiles.tsx
+++ b/src/renderer/components/dashboard/SharedFiles.tsx
@@ -3,22 +3,32 @@ import React from "react";
import man from "../../assets/man.svg";
import { Box } from "../ui-elements/Box";
import { LinkDisplayName } from "./LinkDisplayName";
-import { UserListAvatar, CloseIcon, BtnAddLinksIcon, DocumentSvg, DeleteIcon, DownloadIcon, DoneIcon, ErrorIcon } from "../ui-elements/Image";
+import { UserListAvatar, CloseIcon, BtnAddLinksIcon, DocumentSvg, DeleteIcon, DownloadIcon, DoneIcon, ErrorIcon, FolderIcon, GoBackIcon, RightChevronIcon } from "../ui-elements/Image";
import { Text } from "../ui-elements/Text";
-import Button, { SharedButton, DownloadButton, CustomButton } from "../ui-elements/Button";
+import { SharedButton, DownloadButton, CustomButton, BreadcrumbButton } from "../ui-elements/Button";
import { Divider } from "../ui-elements/Divider";
import { FilesList, FilesListItem, FilesListFile, Hovered } from "../ui-elements/Dashboard";
import { SharedFile } from "../../../shared/types/SharedFile";
import { blinq } from "blinq";
import { FileRequest } from "../../../shared/actions/payloadTypes/FileRequest";
-import { DownloadableFile, SharedFilesFetchState } from "../../../shared/reducers/sharedFiles";
+import { SharedFilesFetchState } from "../../../shared/types/SharedFilesFetchState";
+import { DownloadableFile } from "../../../shared/types/DownloadableFile";
import { InlineSpinner } from "../ui-elements/LoadingSpinner";
import { prettySize } from "../../../shared/system/prettySize";
import CircularProgress from "../ui-elements/CircularProgress";
+import BalanceIndicator from "../../containers/dashboard/BalanceIndicator";
+import { DirectoryEntry } from "../../../shared/system/file/DirectoryEntry";
+import { FileEntry } from "../../../shared/system/file/FileEntry";
+import { NavigationCommand, BaseNavigationCommand } from "../../../shared/actions/fileNavigation";
+import * as path from "path"
+import { prettyTime } from "../../../shared/system/prettyTime";
export interface SharedFilesStateProps {
- outFiles: SharedFile[],
+ outFilesView: (DirectoryEntry | FileEntry)[]
+ downloadableFilesView: (DirectoryEntry | FileEntry)[]
+ currentSharedFilesPath: string
+ currentDownloadableFilesPath: string
linkedUserCommonName?: string
linkedUserName?: string
downloadableFiles: DownloadableFile[]
@@ -30,16 +40,20 @@ export interface SharedFilesDispatchProps {
shareNewFile: () => void
requestFile: (req: FileRequest) => void
removeSharedFile: (filePath: string) => void
+ openDirectory: (navCommand: NavigationCommand) => void
+ upDirectory: (navCommand: BaseNavigationCommand) => void
+ goRoot: (navCommand: NavigationCommand) => void
}
export type SharedFilesProps = SharedFilesStateProps & SharedFilesDispatchProps
-export const SharedFiles: FunctionComponent = ({ close, requestFile, removeSharedFile, shareNewFile, outFiles, linkedUserName, userName, linkedUserCommonName, downloadableFiles, sharedFilesFetchState }) => {
+export const SharedFiles: FunctionComponent = ({ downloadableFilesView, currentSharedFilesPath, currentDownloadableFilesPath, openDirectory, upDirectory, goRoot, close, requestFile, removeSharedFile, shareNewFile, outFilesView, linkedUserName, userName, linkedUserCommonName, sharedFilesFetchState }) => {
const [currentView, setCurrentView] = useState<"shared" | "downloads">("shared")
- const [promptModal, setPromptModal] = useState(false)
+ const [promptModal, setPromptModal] = useState(false)
const [filePath, setFilePath] = useState(undefined)
return <>
- {promptModal && setPromptModal(false) } />}
-
+ {promptModal && setPromptModal(false)} />}
+
@@ -53,11 +67,28 @@ export const SharedFiles: FunctionComponent
= ({ close, reques
{
currentView === "downloads"
- ?
- : setPromptModal(!promptModal)}
- setFilePath={setFilePath}
- />
+ ?
+ : setPromptModal(!promptModal)}
+ setFilePath={setFilePath}
+ openDirectory={openDirectory}
+ upDirectory={upDirectory}
+ goRoot={goRoot}
+ currentSharedFilesPath={currentSharedFilesPath}
+ />
}
>;
@@ -65,66 +96,149 @@ export const SharedFiles: FunctionComponent = ({ close, reques
interface DownloadViewState {
- downloadableFiles: DownloadableFile[]
+ downloadableFilesView: (DirectoryEntry | FileEntry)[]
+ currentDownloadableFilesPath: string
requestFile: (req: FileRequest) => void
userName: string
ownerUserName: string
sharedFilesFetchState: SharedFilesFetchState
+ openDirectory: (navCommand: NavigationCommand) => void
+ upDirectory: (navCommand: BaseNavigationCommand) => void
+ goRoot: (navCommand: NavigationCommand) => void
+
}
-const DownloadView: FunctionComponent = ({ downloadableFiles, requestFile, userName, ownerUserName, sharedFilesFetchState }) => {
+const DownloadView: FunctionComponent = ({ openDirectory, upDirectory, downloadableFilesView, currentDownloadableFilesPath, requestFile, userName, ownerUserName, sharedFilesFetchState }) => {
+
+ const callUpDirectoryMultiple = (number: number) => {
+ Array.from(Array(number)).forEach((x, i) => {
+ upDirectory({ type: 'downloadableFiles'});
+ })
+ }
+
switch (sharedFilesFetchState) {
case "success":
- return
+ return
+
Files shared with you
+
+ { currentDownloadableFilesPath.length > 0 ?
+ upDirectory({ type: "downloadableFiles" })} style={{cursor: 'pointer'}} width="20%">
+
+ Go Back
+
+ :
+
+
+ Go Back
+
+ }
+
+
+
+ callUpDirectoryMultiple(currentDownloadableFilesPath.split('/').length)}>
+
+ pShare
+
+
+ {currentDownloadableFilesPath.length>0 ? currentDownloadableFilesPath.split('/').map((item,idx) =>
+ <>
+ callUpDirectoryMultiple(currentDownloadableFilesPath.split('/').length - (idx+1)) }>
+
+ {item}
+
+
+ >
+ ) : ''}
+
+
+
- {blinq(downloadableFiles).select(f =>
-
-
-
- {f.file.fileName}
-
-
- {
- (() => {
- switch (f.state) {
- case "downloading": //download progress bars
- return
-
- downloading
+ {
+ downloadableFilesView
+ ?
+ blinq(downloadableFilesView)
+ .select(entry => {
+ if (entry.type === "file") {
+ const fileEntry = (entry as FileEntry)
+ const f = fileEntry.fileInfo;
+ return
+
+
+ {path.basename(f.file.fileName)}
+
+
+
+ {(() => {
+ switch (f.state) {
+ case "downloading": //download progress bars
+ return
+
+ downloading{f.eta!=null&&<> {prettySize(f.speed)}/s ({prettyTime(f.eta!)})>}
-
-
- case "ready":
- return (
-
- {prettySize(f.file.size)}
- requestFile({ fileId: f.file.hash, ownerUserName, requestorUserName: userName, fileName: f.file.fileName })} />
-
- )
- case "failed": // try again cancel buttons
- return <>
-
- >
- case "downloaded":
- return
- case "starting":
- return (
- Starting
- )
- default:
- return <>>
- }
- })()
- }
-
- )}
+
+ ;
+ case "ready":
+ return (
+
+ {prettySize(f.file.size)}
+ requestFile({ ownerUserName, requestorUserName: userName, fileName: f.file.fileName, type: "file" })} />
+
+ );
+ case "failed": // try again cancel buttons
+ return <>
+
+ >;
+ case "downloaded":
+ return
;
+ case "starting":
+ return (
+ Starting
+ );
+ default:
+ return <>>;
+ }
+ })()}
+
+ ;
+ }
+ if (entry.type === "directory") {
+ const directoryEntry = (entry as DirectoryEntry);
+ return openDirectory({ type: "downloadableFiles", location: directoryEntry.name! })}>
+
+
+ {path.basename(directoryEntry.name!)}
+
+ {/*
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ toggleDeleteModal(directoryEntry.fullPath!);
+ setFilePath(directoryEntry.fullPath!);
+ }} width="35px" height="20px" margin="5px 10px" />
+ */}
+
+ }
+ throw Error("unexpected entry type")
+
+ })
+
+ : []
+ }
+
- ;
+
+
;
case "failed":
return
@@ -149,66 +263,148 @@ const DeletePrompt: FunctionComponent<{
return (
-
-
- Are you sure you wanna delete {filePath ? filePath.split('/').pop(): ''} ?
+
+
+ Are you sure you want to remove {filePath ? path.basename(filePath) : ''} ?
-
- cancel()}>Cancel
- {
- if(filePath) {
- removeSharedFile(filePath);
- cancel()}}}> Proceed
-
-
+
+ cancel()}>
+ Cancel
+ {
+ if (filePath) {
+ removeSharedFile(filePath);
+ cancel()
+ }
+ }}> Delete
+
+
- )
+ )
}
interface ShareViewProps {
shareNewFile: () => void
- outFiles: SharedFile[]
- toggleDeleteModal: (filePath:string) => void,
- setFilePath: (filePath:string) => void
+ outFilesView: (DirectoryEntry | FileEntry)[]
+ toggleDeleteModal: (filePath: string) => void,
+ setFilePath: (filePath: string) => void
+ openDirectory: (navCommand: NavigationCommand) => void
+ upDirectory: (navCommand: BaseNavigationCommand) => void
+ goRoot: (navCommand: NavigationCommand) => void
+ currentSharedFilesPath: string
+
}
-const ShareView: FunctionComponent = ({ outFiles, shareNewFile, toggleDeleteModal, setFilePath }) => {
- return (
- <>
-
-
- Your shared files
-
- share new file
- shareNewFile()} />
-
-
-
-
- {outFiles
- ? blinq(outFiles).select(f =>
-
-
- {f.relativePath}
-
-
- {
+const ShareView: FunctionComponent = ({ currentSharedFilesPath, outFilesView, shareNewFile, toggleDeleteModal, setFilePath, openDirectory, upDirectory }) => {
+ const callUpDirectoryMultiple = (number: number) => {
+ Array.from(Array(number)).forEach((x, i) => {
+ upDirectory({ type: 'sharedFiles'});
+ })
+ }
+
+ return (
+
+
+
+
+ Your shared files
+
+ share new file
+ shareNewFile()} />
+
+
+
+
+ { currentSharedFilesPath.length > 0 ?
+ upDirectory({ type: "sharedFiles" })} style={{cursor: 'pointer'}} width="20%">
+
+ Go Back
+
+ :
+
+
+ Go Back
+
+ }
+
+
+
+
+ callUpDirectoryMultiple(currentSharedFilesPath.split('/').length)}>
+
+ pShare
+
+
+ {currentSharedFilesPath.length>0 ? currentSharedFilesPath.split('/').map((item,idx) =>
+ <>
+ callUpDirectoryMultiple(currentSharedFilesPath.split('/').length - (idx+1)) }>
+
+ {item}
+
+
+ >
+ ) : ''}
+
+
+
+ {outFilesView
+ ? blinq(outFilesView)
+ .select(entry => {
+ if (entry.type === "file") {
+ const fileEntry = (entry as FileEntry);
+ const f = fileEntry.fileInfo;
+ return
+
+
+ {path.basename(f.relativePath)}
+
+
+ {
toggleDeleteModal(f.path);
- setFilePath(f.path)
- }}
- width="35px" height="20px" margin="5px 10px" />
-
- )
- : []}
-
-
-
- >
-)
+ setFilePath(f.path);
+ }} width="35px" height="20px" margin="5px 10px" />
+
+ ;
+ }
+ if (entry.type === "directory") {
+ const directoryEntry = (entry as DirectoryEntry
);
+ return (
+ openDirectory({ type: "sharedFiles", location: directoryEntry.name! })}
+ >
+
+
+ {path.basename(directoryEntry.name!)}
+
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ toggleDeleteModal(directoryEntry.fullPath!);
+ setFilePath(directoryEntry.fullPath!);
+ }} width="35px" height="20px" margin="5px 10px" />
+
+ )
+ }
+ throw Error("unexpected entry type")
+
+ })
+ : []}
+
+
+
+
+ )
}
diff --git a/src/renderer/components/dashboard/Sidebar.tsx b/src/renderer/components/dashboard/Sidebar.tsx
index 3d276e4b..c3a54cee 100644
--- a/src/renderer/components/dashboard/Sidebar.tsx
+++ b/src/renderer/components/dashboard/Sidebar.tsx
@@ -69,7 +69,7 @@ const tabs: TabInfo[] = [
margin="0 0 0 1em" />,
text: "My Links",
isSelected: (pathname: string) => pathname === '/Dashboard/MyLinks' || pathname === '/Dashboard/AddLinks'
- || pathname === '/Dashboard/SharedFiles' || pathname === '/Dashboard/AddFile'
+ || pathname === '/Dashboard/SharedFiles' || pathname === '/Dashboard/AddFile' || pathname === '/Dashboard/BulkImport'
},
{
location: '/Dashboard/Inbox',
diff --git a/src/renderer/components/ui-elements/Button.tsx b/src/renderer/components/ui-elements/Button.tsx
index 8c069db6..89905b9c 100755
--- a/src/renderer/components/ui-elements/Button.tsx
+++ b/src/renderer/components/ui-elements/Button.tsx
@@ -94,6 +94,19 @@ const ArrowButton:React.FunctionComponent = ({ label, onClick,
)
+const BreadcrumbButton = styled('div')<{ type?: string, active?: boolean }>`
+ display: inline-block;
+ min-width: 50px;
+ padding: 4px;
+ margin: 10px 0 10px 0;
+ // border: solid 1px ${(props) => props.active ? '#4a4a4a': '#4a4a4a' };
+ background: ${(props) => props.active ? '#4a4a4a': 'white' };
+ border-radius: 4px;
+ box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
+ // clip-path: ${(props) => props.active ? 'polygon(75% 0%, 88% 50%, 75% 100%, 0% 100%, 0% 54%, 0% 0%) ': ''};
+ cursor: pointer;
+`;
+
const StyledSharedButton = styled('div')<{ white?: boolean , margin?: string}>`
width: 132px;
height: 42px;
@@ -113,7 +126,7 @@ const SharedButton:React.FunctionComponent<{ onClick: () => void, white?: boolea
-
+
YOUR FILES
@@ -130,6 +143,6 @@ const DownloadButton:React.FunctionComponent<{ onClick: () => void, white?: bool
)
export default StyledButton
-export { ArrowButton, BackArrowButton, LightButton, SharedButton, DownloadButton, BackButton, CustomButton };
+export { ArrowButton, BackArrowButton, LightButton, SharedButton, DownloadButton, BackButton, CustomButton, BreadcrumbButton };
diff --git a/src/renderer/components/ui-elements/CircularProgress.tsx b/src/renderer/components/ui-elements/CircularProgress.tsx
index 08c4095f..0808b24a 100644
--- a/src/renderer/components/ui-elements/CircularProgress.tsx
+++ b/src/renderer/components/ui-elements/CircularProgress.tsx
@@ -9,9 +9,9 @@ const Svg = styled('svg')`
const CircularProgress:React.FunctionComponent<{ progress: number, size: number }> = ({ progress, size }) =>
-
-
+
+
diff --git a/src/renderer/components/ui-elements/Dashboard.tsx b/src/renderer/components/ui-elements/Dashboard.tsx
index aeff4003..cd5b25f6 100644
--- a/src/renderer/components/ui-elements/Dashboard.tsx
+++ b/src/renderer/components/ui-elements/Dashboard.tsx
@@ -87,7 +87,7 @@ const FilesList = styled('ul')`
`;
const Hovered = styled('div')`
- visibility:hidden;
+ opacity:0;
`;
const Unhovered = styled('div')`
visibility: visible;
@@ -98,12 +98,17 @@ const FilesListItem = styled('li')`
direction: row;
justify-content: space-between;
padding: 0.5em 0.25em;
+ border: 2px solid #e7e7e700;
+ transition : border-color 200ms ease-out;
+ ${Hovered} {
+ transition : opacity 200ms ease-out
+ }
&:hover {
border-radius: 7px;
- border: solid 2px #e7e7e7;
+ border-color: #e7e7e7ff;
};
&:hover ${Hovered} {
- visibility: visible;
+ opacity:1;
}
&:hover ${Unhovered} {
visibility: hidden;
@@ -118,8 +123,11 @@ const FilesListFile = styled('span')`
justify-content: flex-start;
padding: 0;
margin: 0;
+ ${Hovered} {
+ transition : opacity 200ms ease-out
+ }
&:hover ${Hovered} {
- visibility: visible;
+ opacity:1;
}
`;
diff --git a/src/renderer/components/ui-elements/Dropzone.tsx b/src/renderer/components/ui-elements/Dropzone.tsx
index 89216f62..1303ed4f 100644
--- a/src/renderer/components/ui-elements/Dropzone.tsx
+++ b/src/renderer/components/ui-elements/Dropzone.tsx
@@ -1,10 +1,17 @@
-import React from "react";
+import React, { useRef, useEffect, MutableRefObject } from "react";
import { FunctionComponent } from "react";
import { Text } from "./Text";
import { Card } from "./Card";
import Button from "./Button";
import { FilePathInfo } from "../../../shared/types/FilePathInfo";
-export const Dropzone: FunctionComponent = ({ error, filesSelected, multiple, accept }) => {
+export const Dropzone: FunctionComponent = ({ error, filesSelected, directoriesSelected, multiple, accept }) => {
+ const dirFileInputRef: MutableRefObject = useRef(null);
+ useEffect(() => {
+ const elem = dirFileInputRef.current;
+ if (elem) {
+ elem.setAttribute("webkitdirectory", "") //annoyingly, we don't seem to be able to set this as an attr with JSX :(
+ }
+ })
return {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
@@ -36,10 +43,29 @@ export const Dropzone: FunctionComponent = ({ error, filesSelecte
Select file
+ {
+ directoriesSelected
+ &&
+ <>
+ {
+ e.preventDefault();
+ if (!e.currentTarget.files) {
+ return;
+ }
+ const files = [...e.currentTarget.files];
+ directoriesSelected(files.map(f => ({ path: f.path, type: "", size: 0 })))
+ }} style={({ display: "none" })} />
+
+
+ Select directory
+
+
+ >}
;
};
interface DropzoneDispatchProps {
filesSelected: (files: FilePathInfo[]) => void;
+ directoriesSelected?: (directories: FilePathInfo[]) => void;
}
interface DropzoneStateProps {
error?: DropzoneError;
diff --git a/src/renderer/components/ui-elements/Image.tsx b/src/renderer/components/ui-elements/Image.tsx
index cc5eecb2..020e4b4d 100755
--- a/src/renderer/components/ui-elements/Image.tsx
+++ b/src/renderer/components/ui-elements/Image.tsx
@@ -23,6 +23,10 @@ import cancelicon from "../../assets/cancel.svg";
import downloadicon from "../../assets/svgs/download.svg";
import doneicon from "../../assets/svgs/done.svg";
import erroricon from "../../assets/svgs/error.svg";
+import foldericon from "../../assets/svgs/folder-blue.svg";
+import goback from "../../assets/svgs/goback.svg";
+import rightchevron from "../../assets/svgs/right-chevron.svg";
+import exporticon from "../../assets/svgs/export.svg";
interface ImageProps {
src?: string,
@@ -30,7 +34,7 @@ interface ImageProps {
height?: string,
margin?: string,
white?: boolean,
- onClick?: () => void,
+ onClick?: (e:React.MouseEvent) => void,
cursor?: string,
float?: string,
@@ -107,9 +111,22 @@ const DoneIcon: React.FunctionComponent =
const ErrorIcon: React.FunctionComponent =
({ width, height, margin, onClick, style }) =>
+const FolderIcon: React.FunctionComponent =
+ ({ width, height, margin, onClick, style }) =>
+
+const GoBackIcon: React.FunctionComponent =
+ ({ width, height, margin, onClick, style }) =>
+
+const RightChevronIcon: React.FunctionComponent =
+ ({ width, height, margin, onClick, style }) =>
+
+const ExportIcon: React.FunctionComponent =
+ ({ width, height, margin, onClick, style }) =>
+
export {
SvgIcon as AppLogo, PlainAppLogo, MyLinksIcon, InboxIcon, OutboxIcon, RequestSentIcon, ViewBtnIcon,
InvitesIcon, UserListAvatar, PendingIcon, BtnAddLinksIcon, AddLinksIcon, CloseIcon, DocumentSvg,
- ProgressSpinner, DeleteIcon, CheckIcon, CancelIcon, DownloadIcon, DoneIcon, ErrorIcon
+ ProgressSpinner, DeleteIcon, CheckIcon, CancelIcon, DownloadIcon, DoneIcon, ErrorIcon, FolderIcon,
+ GoBackIcon, RightChevronIcon, ExportIcon
}
\ No newline at end of file
diff --git a/src/renderer/components/ui-elements/QRCode.tsx b/src/renderer/components/ui-elements/QRCode.tsx
new file mode 100644
index 00000000..4ace4e28
--- /dev/null
+++ b/src/renderer/components/ui-elements/QRCode.tsx
@@ -0,0 +1,122 @@
+import * as qrGenerator from 'qrcode-generator';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+
+export interface IProps {
+ value?: string;
+ ecLevel?: 'L' | 'M' | 'Q' | 'H';
+ enableCORS?: boolean;
+ size?: number;
+ minimumCellSize?: number;
+ minPadding?: number;
+ bgColor?: string;
+ fgColor?: string;
+ qrStyle?: 'squares' | 'dots';
+ style?: object;
+}
+
+
+
+function utf16to8(str: string): string {
+ let out: string = '', i: number, c: number;
+ const len: number = str.length;
+ for (i = 0; i < len; i++) {
+ c = str.charCodeAt(i);
+ if ((c >= 0x0001) && (c <= 0x007F)) {
+ out += str.charAt(i);
+ } else if (c > 0x07FF) {
+ out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+ out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+ out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+ } else {
+ out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+ out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+ }
+ }
+ return out;
+}
+
+function drawPositioningPattern(row: number, col: number, length: number, props: IProps, ctx: CanvasRenderingContext2D) {
+
+ const cellSize = Math.max(Math.trunc(props.size! / length), props.minimumCellSize!);
+
+ for (let r = -1; r <= 7; r++) {
+ if (!(row + r <= -1 || length <= row + r)) {
+ for (let c = -1; c <= 7; c++) {
+ if (!(col + c <= -1 || length <= col + c) &&
+ (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
+ (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
+ (2 <= r && r <= 4 && 2 <= c && c <= 4)) {
+
+ const w = (Math.ceil(((row + r) + 1) * cellSize) - Math.floor((row + r) * cellSize));
+ const h = (Math.ceil(((col + c) + 1) * cellSize) - Math.floor((col + c) * cellSize));
+
+ ctx.fillStyle = props.fgColor!;
+ ctx.fillRect(Math.round((row + r) * cellSize), Math.round((col + c) * cellSize), w, h);
+ }
+ }
+ }
+ }
+}
+const defaultProps: IProps = {
+ value: '',
+ ecLevel: 'M',
+ size: 150,
+ minPadding: 10,
+ bgColor: '#FFFFFF',
+ fgColor: '#000000',
+ qrStyle: 'squares',
+ minimumCellSize: 1
+}
+export const QRCode: React.FunctionComponent = (props) => {
+ const { value, ecLevel, size, bgColor, fgColor, qrStyle, minimumCellSize } = { ...defaultProps, ...props };
+ const canvasRef = React.useRef(null);
+
+ const qrCode = qrGenerator(0, ecLevel!);
+ qrCode.addData(utf16to8(value!));
+ qrCode.make();
+ const length = qrCode.getModuleCount();
+ const cellSize = Math.max(Math.trunc(size! / length), minimumCellSize!);
+ const actualSize = cellSize * length;
+
+ React.useEffect(() => {
+ const canvas: HTMLCanvasElement = ReactDOM.findDOMNode(canvasRef.current!) as HTMLCanvasElement;
+ const ctx: CanvasRenderingContext2D = canvas.getContext('2d')!;
+
+ const scale = (window.devicePixelRatio || 1);
+ canvas.height = canvas.width = actualSize * scale;
+ ctx.scale(scale, scale);
+ ctx.fillStyle = bgColor!;
+ ctx.fillRect(0, 0, actualSize, actualSize);
+ const s = qrStyle === "dots" ? 1 : 0
+ for (let row = 0; row < length; row++) {
+ for (let col = 0; col < length; col++) {
+ if (qrCode.isDark(row, col)) {
+ ctx.fillStyle = fgColor!;
+ const w = (Math.ceil((col + 1) * cellSize) - Math.floor(col * cellSize)) - s;
+ const h = (Math.ceil((row + 1) * cellSize) - Math.floor(row * cellSize)) - s;
+ ctx.fillRect(Math.round(col * cellSize), Math.round(row * cellSize), w, h);
+ }
+ }
+ }
+ drawPositioningPattern(0, 0, length, props, ctx);
+ drawPositioningPattern(length - 7, 0, length, props, ctx);
+ drawPositioningPattern(0, length - 7, length, props, ctx);
+
+
+ }, [value, ecLevel, size, bgColor, fgColor, qrStyle])
+
+ return
+
+
+}
\ No newline at end of file
diff --git a/src/renderer/containers/dashboard/AddLinks.ts b/src/renderer/containers/dashboard/AddLinks.ts
index b6c70799..c45be26f 100644
--- a/src/renderer/containers/dashboard/AddLinks.ts
+++ b/src/renderer/containers/dashboard/AddLinks.ts
@@ -9,6 +9,7 @@ import { blinq } from "blinq";
import { push } from "connected-react-router";
import { filterDeniedUsers } from "./helpers/filterDeniedUsers";
import { SearchActions } from "../../../shared/actions/search";
+import { BulkImportActions } from "../../../shared/actions/bulkImport";
@@ -71,6 +72,6 @@ const mapStateToProps = (state: RendererRootState /*, ownProps*/): AddLinksState
};
};
-const mapDispatchToProps: MapPropsToDispatchObj = { ...SearchActions, ...BdapActions, push };
+const mapDispatchToProps: MapPropsToDispatchObj = { ...SearchActions, ...BdapActions, ...BulkImportActions, push };
export default connect(mapStateToProps, mapDispatchToProps)(AddLinks)
diff --git a/src/renderer/containers/dashboard/BalanceIndicator.ts b/src/renderer/containers/dashboard/BalanceIndicator.ts
new file mode 100644
index 00000000..79e5dc9b
--- /dev/null
+++ b/src/renderer/containers/dashboard/BalanceIndicator.ts
@@ -0,0 +1,17 @@
+import { RendererRootState } from "../../reducers";
+import { BalanceIndicator, BalanceIndicatorDispatchProps, BalanceIndicatorStateProps } from "../../components/dashboard/BalanceIndicator"
+import { MapPropsToDispatchObj } from "../../system/MapPropsToDispatchObj";
+import { connect } from "react-redux";
+import { BdapActions } from "../../../shared/actions/bdap";
+
+const mapStateToProps = (state: RendererRootState, ownProps: { hideLinkWhenMinimized?: boolean }): BalanceIndicatorStateProps => ({
+ balance: state.bdap.balance,
+ walletAddress: state.bdap.topUpAddress || "",
+ errorMessage: state.bdap.insufficientFundsErrorMessage,
+ ...ownProps
+})
+
+const mapDispatchToProps: MapPropsToDispatchObj = { ...BdapActions };
+
+export default connect(mapStateToProps, mapDispatchToProps)(BalanceIndicator)
+
diff --git a/src/renderer/containers/dashboard/BulkImport.ts b/src/renderer/containers/dashboard/BulkImport.ts
new file mode 100644
index 00000000..f4d07747
--- /dev/null
+++ b/src/renderer/containers/dashboard/BulkImport.ts
@@ -0,0 +1,21 @@
+import { RendererRootState } from "../../reducers";
+import { MapPropsToDispatchObj } from "../../system/MapPropsToDispatchObj";
+import { connect } from "react-redux";
+import { BulkImportStateProps, BulkImportsDispatchProps, BulkImport } from "../../components/dashboard/BulkImport";
+import { BulkImportActions } from "../../../shared/actions/bulkImport";
+import { push } from "connected-react-router";
+
+
+
+
+const mapStateToProps = (state: RendererRootState /*, ownProps*/): BulkImportStateProps => {
+ return {
+ data: state.bulkImport.previewData,
+ fqdnData: state.bulkImport.fqdnData,
+ err: state.bulkImport.err
+ };
+};
+
+const mapDispatchToProps: MapPropsToDispatchObj = { ...BulkImportActions, push };
+
+export default connect(mapStateToProps, mapDispatchToProps)(BulkImport)
diff --git a/src/renderer/containers/dashboard/MyLinks.ts b/src/renderer/containers/dashboard/MyLinks.ts
index 104dd70c..5c61e650 100644
--- a/src/renderer/containers/dashboard/MyLinks.ts
+++ b/src/renderer/containers/dashboard/MyLinks.ts
@@ -12,9 +12,9 @@ import { FileSharingActions } from "../../../shared/actions/fileSharing";
import { SearchActions } from "../../../shared/actions/search";
const getUserName = createSelector([(state: RendererRootState) => typeof state.bdap.currentUser !== 'undefined' ? state.bdap.currentUser.object_id : undefined], (user) => user)
-const getUserList = createSelector(
+const getBalance = createSelector([(state: RendererRootState) => state.bdap.balance], (b) => b)
+const getUserListBase = createSelector(
[
- (state: RendererRootState) => state.myLinksSearch.query,
(state: RendererRootState) => state.bdap.users,
(state: RendererRootState) => state.bdap.completeLinks,
(state: RendererRootState) => state.bdap.pendingAcceptLinks,
@@ -22,7 +22,7 @@ const getUserList = createSelector(
(state: RendererRootState) => state.bdap.deniedLinks,
(state: RendererRootState) => typeof state.bdap.currentUser !== 'undefined' ? state.bdap.currentUser.object_full_path : undefined
],
- (query, users, completeLinks, pendingAcceptLinks, pendingRequestLinks, deniedLinks, currentUserFqdn) => {
+ (users, completeLinks, pendingAcceptLinks, pendingRequestLinks, deniedLinks, currentUserFqdn) => {
const linkedUsers = blinq(users)
.join(
completeLinks,
@@ -41,7 +41,7 @@ const getUserList = createSelector(
(u) => ({
userName: u.object_id,
commonName: u.common_name,
- state: "pending"
+ state: "pending-invite"
} as BdapUser))
const pendingRequestUsers = blinq(users)
.join(
@@ -56,6 +56,15 @@ const getUserList = createSelector(
const baseQuery = linkedUsers
.concat(pendingAcceptUsers)
.concat(pendingRequestUsers);
+ return baseQuery
+ });
+const getUserList = createSelector(
+ [
+ (state: RendererRootState) => state.myLinksSearch.query,
+ getUserListBase
+ ],
+ (query, baseQuery) => {
+
return query.length > 0
? baseQuery
.select(bdapUser => ({ bdapUser, commonNameQueryPosition: bdapUser.commonName.indexOf(query), userNameQueryPosition: bdapUser.userName.indexOf(query) }))
@@ -76,7 +85,9 @@ const mapStateToProps = (state: RendererRootState /*, ownProps*/): MyLinksStateP
return {
users: getUserList(state),
userName: getUserName(state)!,
- queryText: state.myLinksSearch.queryText
+ queryText: state.myLinksSearch.queryText,
+ allUsers: getUserListBase(state).toArray(),
+ balance: getBalance(state)
};
};
diff --git a/src/renderer/containers/dashboard/SharedFiles.ts b/src/renderer/containers/dashboard/SharedFiles.ts
index 7568c647..b42392c6 100644
--- a/src/renderer/containers/dashboard/SharedFiles.ts
+++ b/src/renderer/containers/dashboard/SharedFiles.ts
@@ -5,26 +5,86 @@ import { SharedFiles, SharedFilesStateProps, SharedFilesDispatchProps } from "..
import { SharedFilesActions } from "../../../shared/actions/sharedFiles";
import { FileSharingActions } from "../../../shared/actions/fileSharing";
import { RemoveFileActions } from "../../../shared/actions/removeFile";
+import * as path from "path"
+import { createSelector } from 'reselect'
+import { fileListToTree } from "../../../shared/system/file/fileListToTree";
+import { getDirectoryListing } from "../../../shared/system/file/getDirectoryListing";
+import { FileNavigationActions } from "../../../shared/actions/fileNavigation";
+import { blinq } from "blinq";
+const userNameSelector = (state: RendererRootState) => state.sharedFiles.linkedUserName
+const fileWatchUsersSelector = (state: RendererRootState) => state.fileWatch.users;
+const fileWatchUserSelector = createSelector(
+ [
+ userNameSelector,
+ fileWatchUsersSelector
+ ],
+ (linkedUserName, users) => linkedUserName != null ? users[linkedUserName] : undefined
+)
+const outSelector = createSelector(
+ [
+ fileWatchUserSelector,
+ ],
+ fileWatchUser => fileWatchUser ? fileWatchUser.out : undefined)
+const outFilesSelector = createSelector(
+ [
+ outSelector
+ ],
+ out => out ? Object.values(out) : [])
+const sharedFilesDownloadableFilesSelector = (state: RendererRootState) => state.sharedFiles.downloadableFiles;
+const downloadableFilesSelector = createSelector(
+ [
+ sharedFilesDownloadableFilesSelector
+ ],
+ downloadableFiles => downloadableFiles || []
+)
+const outFilesTreeSelector = createSelector([
+ outFilesSelector
+], files => fileListToTree(files))
+
+const downloadableFilesTreeSelector = createSelector([
+ downloadableFilesSelector
+], files => fileListToTree(files))
+
+const sharedFilesPathSelector = (state: RendererRootState) => state.fileNavigation.sharedFilesViewPath.join("/")
+const downloadableFilesPathSelector = (state: RendererRootState) => state.fileNavigation.downloadableFilesViewPath.join("/")
+
+const outFilesCurrentDirectorySelector = createSelector(
+ [
+ outFilesTreeSelector,
+ sharedFilesPathSelector
+ ],
+ (tree, filePath) => blinq(getDirectoryListing(filePath, tree)).orderBy(x => x.type === "directory" ? 0 : 1).thenBy(x => path.basename(x.name!)).toArray())
+
+const downloadableFilesCurrentDirectorySelector = createSelector(
+ [
+ downloadableFilesTreeSelector,
+ downloadableFilesPathSelector
+ ],
+ (tree, filePath) => blinq(getDirectoryListing(filePath, tree)).orderBy(x => x.type === "directory" ? 0 : 1).thenBy(x => path.basename(x.name!)).toArray())
const mapStateToProps = (state: RendererRootState /*, ownProps*/): SharedFilesStateProps => {
+ //const outFiles = outFilesSelector(state);
+ const downloadableFiles = downloadableFilesSelector(state);
+ const outFilesView = outFilesCurrentDirectorySelector(state)
+ const downloadableFilesView = downloadableFilesCurrentDirectorySelector(state);
+ const currentSharedFilesPath = sharedFilesPathSelector(state);
+ const currentDownloadableFilesPath = downloadableFilesPathSelector(state);
return {
- outFiles:
- state.sharedFiles.linkedUserName
- && state.fileWatch.users[state.sharedFiles.linkedUserName]
- && state.fileWatch.users[state.sharedFiles.linkedUserName].out
- ? Object.values(state.fileWatch.users[state.sharedFiles.linkedUserName].out)
- : [],
+ outFilesView,
+ downloadableFilesView,
+ currentSharedFilesPath,
+ currentDownloadableFilesPath,
linkedUserCommonName: state.sharedFiles.linkedCommonName,
linkedUserName: state.sharedFiles.linkedUserName,
userName: state.user.userName!,
- downloadableFiles: state.sharedFiles.downloadableFiles || [],
+ downloadableFiles,
sharedFilesFetchState: state.sharedFiles.state
}
};
-const mapDispatchToProps: MapPropsToDispatchObj = { ...SharedFilesActions, ...FileSharingActions, ...RemoveFileActions };
+const mapDispatchToProps: MapPropsToDispatchObj = { ...FileNavigationActions, ...SharedFilesActions, ...FileSharingActions, ...RemoveFileActions };
export default connect(mapStateToProps, mapDispatchToProps)(SharedFiles)
diff --git a/src/renderer/routes/appRoutes.ts b/src/renderer/routes/appRoutes.ts
index 953fb7bf..b9f404b3 100644
--- a/src/renderer/routes/appRoutes.ts
+++ b/src/renderer/routes/appRoutes.ts
@@ -24,6 +24,7 @@ import RestoreSyncProgress from '../containers/onboarding_restore/RestoreSyncPro
import SecureFilePassword from '../containers/onboarding_restore/SecureFilePassword';
import SharedFiles from '../containers/dashboard/SharedFiles';
import AddFile from '../containers/dashboard/AddFile';
+import BulkImport from '../containers/dashboard/BulkImport';
import { CreatingLinkProgress } from '../components/dashboard/CreatingLinkProgress';
import { ClientDownloads } from '../containers/dashboard/ClientDownloads';
export interface RouteInfo {
@@ -62,7 +63,8 @@ const dashboardRoutingTable = {
sharedFiles: route("/Dashboard/SharedFiles", SharedFiles),
addFile: route("/Dashboard/AddFile", AddFile),
creatingLinkProgress: route("/Dashboard/CreatingLinkProgress", CreatingLinkProgress),
- clientDownloads: route("/Dashboard/Outbox", ClientDownloads)
+ clientDownloads: route("/Dashboard/Outbox", ClientDownloads),
+ bulkImport: route("/Dashboard/BulkImport", BulkImport)
}
export const pushRoute = (route: RouteInfo) => push(route.path)
diff --git a/src/renderer/sagas/navSaga/navSaga.ts b/src/renderer/sagas/navSaga/navSaga.ts
index 5b15ea8a..7cd34004 100644
--- a/src/renderer/sagas/navSaga/navSaga.ts
+++ b/src/renderer/sagas/navSaga/navSaga.ts
@@ -15,11 +15,19 @@ import { AppActions } from "../../../shared/actions/app";
export function* navSaga() {
console.log("nav saga started")
yield* waitForInitialized();
+ const syncIsComplete: boolean = yield select((s: RendererRootState) => s.sync.isComplete)
+ if (!syncIsComplete) {
+ yield* waitForSync();
- yield* waitForSync();
-
+ }
+
const currentState: RendererRootState = yield select()
+ if(currentState.router.location.pathname.startsWith("/Dashboard/")){
+ yield* dashboardNav()
+
+ return;
+ }
if (currentState.user.isOnboarded) {
console.log("nav saga: user is onboarded, navigating to /Main")
yield put(pushRoute(appRoutes.dashboard))
@@ -48,7 +56,7 @@ export function* navSaga() {
yield bdapAccountConfigNavMap.runNav();
yield* waitForWalletCredentials();
}
- else{
+ else {
console.log("nav saga: navigating to Onboarding -- /CreateAccount")
yield put(pushRoute(appRoutes.createAccount))
console.log("nav saga navigating to /CreateAccount")
@@ -57,9 +65,9 @@ export function* navSaga() {
createAccount: take(getType(RootActions.createAccount)),
restoreAccount: take(getType(RootActions.restoreAccount))
})
-
+
let returnedToCreateAccount = false
-
+
if (createAccount) {
yield put(pushRoute(appRoutes.enterUserName))
const bdapAccountConfigNavMap = getNavMap();
@@ -77,8 +85,8 @@ export function* navSaga() {
yield* waitForWalletCredentials();
} else {
yield put(pushRoute(appRoutes.restoreAccount))
-
-
+
+
for (; ;) {
let returnedToRestoreAccount = false
const { passphrase, mnemonicFile, cancelled } = yield race({
@@ -86,7 +94,7 @@ export function* navSaga() {
mnemonicFile: take(getType(RootActions.restoreWithMnemonicFile)),
cancelled: take(getType(RootActions.restoreCancelled))
})
-
+
if (cancelled) {
yield put(pushRoute(appRoutes.createAccount))
returnedToCreateAccount = true
@@ -99,7 +107,7 @@ export function* navSaga() {
restoreNavMap.registerNavAction(RootActions.restoreSync, appRoutes.restoreSyncProgress)
restoreNavMap.registerNavAction(RootActions.restoreFailed, appRoutes.restoreWithPassphrase)
restoreNavMap.registerNavAction(RootActions.restoreSuccess, appRoutes.passwordCreateOrLogin, true) //true parameter indicates stopping condition
-
+
yield restoreNavMap.runNav(); //note: this hangs until we hit a navAction with "stopOnThisAction" parameter `true`
}
else if (mnemonicFile) {
@@ -111,26 +119,26 @@ export function* navSaga() {
restoreNavMap.registerNavAction(RootActions.restoreSync, appRoutes.restoreSyncProgress)
restoreNavMap.registerNavAction(RootActions.restoreFailed, appRoutes.restoreWithMnemonicFile)
restoreNavMap.registerNavAction(RootActions.restoreSuccess, appRoutes.passwordCreateOrLogin, true) //true parameter indicates stopping condition
-
+
yield restoreNavMap.runNav(); //note: this hangs until we hit a navAction with "stopOnThisAction" parameter `true`
}
-
-
+
+
if (returnedToRestoreAccount) continue;
if (returnedToCreateAccount) break
yield* waitForWalletCredentials();
break;
-
+
}
}
if (!returnedToCreateAccount) {
break;
}
}
-
- }
+
}
-
+ }
+
}
diff --git a/src/renderer/system/BdapUserState.ts b/src/renderer/system/BdapUserState.ts
index 616beeae..b8a233a5 100644
--- a/src/renderer/system/BdapUserState.ts
+++ b/src/renderer/system/BdapUserState.ts
@@ -1 +1 @@
-export type BdapUserState = "normal" | "pending" | "linked";
+export type BdapUserState = "normal" | "pending" | "linked" | "pending-invite";
diff --git a/src/renderer/system/useInterval.tsx b/src/renderer/system/useInterval.tsx
new file mode 100644
index 00000000..1207c453
--- /dev/null
+++ b/src/renderer/system/useInterval.tsx
@@ -0,0 +1,17 @@
+import { useEffect, useRef } from "react";
+export function useInterval(callback: () => void, delay: number) {
+ const savedCallback = useRef<(() => void) | null>(null);
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+ useEffect(() => {
+ function tick() {
+ savedCallback.current && savedCallback.current();
+ }
+ if (delay !== null) {
+ let id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ return () => { };
+ }, [delay]);
+}
diff --git a/src/renderer/system/useOutsideAlerter.tsx b/src/renderer/system/useOutsideAlerter.tsx
new file mode 100644
index 00000000..be784a54
--- /dev/null
+++ b/src/renderer/system/useOutsideAlerter.tsx
@@ -0,0 +1,18 @@
+import { useEffect, MutableRefObject, useRef } from "react";
+export function useOutsideAlerter(elementRef: MutableRefObject, callback: (ev: MouseEvent) => void) {
+ const savedCallback = useRef<((ev: MouseEvent) => void) | null>(null);
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+ function handleClickOutside(event: MouseEvent) {
+ if (elementRef.current && !elementRef.current.contains(event.target)) {
+ savedCallback.current && savedCallback.current(event);
+ }
+ }
+ useEffect(() => {
+ document.addEventListener("click", handleClickOutside, { capture: true });
+ return () => {
+ document.removeEventListener("click", handleClickOutside, { capture: true });
+ };
+ });
+}
diff --git a/src/rtc/sagas/helpers/ProgressHandler.ts b/src/rtc/sagas/helpers/ProgressHandler.ts
new file mode 100644
index 00000000..32c8b5d1
--- /dev/null
+++ b/src/rtc/sagas/helpers/ProgressHandler.ts
@@ -0,0 +1 @@
+export type ProgressHandler = (progress: number, speed: number, eta: number | undefined, downloadedBytes: number, size: number) => any;
diff --git a/src/rtc/sagas/helpers/copyFileToRTCPeer.ts b/src/rtc/sagas/helpers/copyFileToRTCPeer.ts
deleted file mode 100644
index d35c6ca2..00000000
--- a/src/rtc/sagas/helpers/copyFileToRTCPeer.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { call, race } from "redux-saga/effects";
-import { toArrayBuffer } from "../../../shared/system/bufferConversion";
-import { RTCPeer } from "../../system/webRtc/RTCPeer";
-import { createPromiseResolver } from "../../../shared/system/createPromiseResolver";
-import { delay } from "redux-saga";
-import * as util from 'util'
-import * as fs from 'fs'
-
-const fileReadBufferSize = 65536; // 64KiB
-const maxSendBuffered = 2097152; // 2MiB
-const sendBufferedAmountLowThreshold = 262144; // 256KiB
-
-const fsReadAsync = util.promisify(fs.read);
-const fsCloseAsync = util.promisify(fs.close);
-const fsOpenAsync = util.promisify(fs.open);
-
-export const copyFileToRTCPeer =
-
- (filePath: string, peer: RTCPeer, progressHandler?: ((progress: number, downloadedBytes: number, size: number) => any)) => call(function* () {
- const dataChannel = peer.dataChannel;
- dataChannel.bufferedAmountLowThreshold = sendBufferedAmountLowThreshold;
- let totalRead = 0;
- let totalSent = 0;
- const { size: fileSize }: fs.Stats = yield call(() => fs.promises.stat(filePath))
- var buffer = new Buffer(fileReadBufferSize);
- const fileDescriptor: number = yield call(() => fsOpenAsync(filePath, "r"));
- let currentProgressPct = -1
- for (; ;) {
- console.log("reading chunk");
- const amtToRead = Math.min(fileSize - totalRead, fileReadBufferSize);
- const { bytesRead }: {
- bytesRead: number;
- } = yield call(() => fsReadAsync(fileDescriptor, buffer, 0, amtToRead, totalRead));
- console.log("chunk read");
- if (bytesRead === 0) {
- if (totalSent !== fileSize || totalSent !== totalRead) {
- throw Error("transfer length mismatch");
- }
- break;
- }
- totalRead += bytesRead;
- if (dataChannel.bufferedAmount > maxSendBuffered) {
- console.log("buffer high");
- const pr = createPromiseResolver();
- dataChannel.onbufferedamountlow = () => pr.resolve(true);
-
- const { success } = yield race({
- timeout: call(function* () {
- let currBufAmt = dataChannel.bufferedAmount
- let now = performance.now()
- let moved = false
- for (; ;) {
- yield delay(250)
- moved = moved || dataChannel.bufferedAmount !== currBufAmt
- currBufAmt = dataChannel.bufferedAmount
- if (moved) {
- now = performance.now()
- moved = false
- } else {
- if ((performance.now() - now) > 120000) {
- break
- }
- }
- }
- }),
- success: call(() => pr.promise)
- })
- if (!success) {
- throw Error("timeout")
- }
-
-
- console.log("buffer emptied");
- }
- dataChannel.send(toArrayBuffer(buffer, 0, bytesRead));
- totalSent += bytesRead;
-
- const progressPct = Math.trunc((totalSent / fileSize) * 100)
- if (progressPct != currentProgressPct) {
- currentProgressPct = progressPct
- if (progressHandler) {
- yield progressHandler(currentProgressPct, totalSent, fileSize)
- }
- }
- yield delay(0);
- }
- yield call(() => fsCloseAsync(fileDescriptor));
- let bufferedAmt = dataChannel.bufferedAmount
- let now = performance.now()
- let moved = false
- while (bufferedAmt > 0) {
- yield delay(250);
- moved = moved || dataChannel.bufferedAmount !== bufferedAmt
- bufferedAmt = dataChannel.bufferedAmount
- if (moved) {
- now = performance.now()
- moved = false
- } else {
- if ((performance.now() - now) > 120000) {
- throw Error("timeout")
- }
- }
- }
- });
-
diff --git a/src/rtc/sagas/helpers/copyFromRTCPeerToStream.ts b/src/rtc/sagas/helpers/copyFromRTCPeerToStream.ts
new file mode 100644
index 00000000..11022f22
--- /dev/null
+++ b/src/rtc/sagas/helpers/copyFromRTCPeerToStream.ts
@@ -0,0 +1,146 @@
+import { call, race, take } from "redux-saga/effects";
+import { RTCPeer } from "../../system/webRtc/RTCPeer";
+//import * as util from "util";
+//import * as fs from "fs";
+import { eventChannel, END } from "redux-saga";
+import { createRTCPeerReadStream } from "./createRTCPeerReadStream";
+import progressStream from "progress-stream";
+import { Progress } from "progress-stream";
+import * as stream from "stream";
+import { ProgressHandler } from "./ProgressHandler";
+
+//const fsUnlinkAsync = util.promisify(fs.unlink);
+
+export const copyFromRTCPeerToStream = <
+ T extends string,
+ TData extends string | Blob | ArrayBuffer | ArrayBufferView
+>(
+ stream: stream.Writable,
+ size: number,
+ peer: RTCPeer,
+ progressHandler?: ProgressHandler
+) =>
+ call(function*() {
+ const len = size;
+ //const writeStream = fs.createWriteStream(savePath);
+ const readStream = createRTCPeerReadStream(peer, len);
+
+ //cleanupOperations.push(() => writeStream.close());
+ yield* copyStream(readStream, stream, len, progressHandler);
+ });
+
+function* copyStream(
+ readStream: stream.Readable,
+ writeStream: stream.Writable,
+ len: number,
+ progressHandler?: ProgressHandler
+) {
+ const cleanupOperations: (() => void)[] = [];
+ try {
+ const progStream = progressStream({ length: len, time: 500 });
+ const progressChannel = eventChannel(emitter => {
+ const handler = (progress: Progress) => {
+ emitter(progress);
+ if (progress.remaining === 0) {
+ emitter(END);
+ }
+ };
+ progStream.on("progress", handler);
+ return () => progStream.off("progress", handler);
+ });
+ cleanupOperations.push(() => progressChannel.close());
+
+ const pipe = readStream.pipe(progStream).pipe(writeStream);
+ const errorChannel = eventChannel(emitter => {
+ const handler = (err: Error) => emitter(err);
+ pipe.on("error", handler);
+ return () => pipe.off("error", handler);
+ });
+ cleanupOperations.push(() => errorChannel.close());
+
+ const endChannel = eventChannel<{}>(emitter => {
+ const handler = () => emitter({});
+ pipe.on("finish", handler);
+ return () => pipe.off("finish", handler);
+ });
+ cleanupOperations.push(() => endChannel.close());
+ let currentProgressPct = 0;
+
+ if (progressHandler) {
+ yield progressHandler(0, 0, undefined, 0, len);
+ }
+
+ // yield put(
+ // RtcActions.fileReceiveProgress({
+ // fileRequest,
+ // totalBytes: len,
+ // downloadedBytes: 0,
+ // downloadedPct: 0,
+ // speed: 0,
+ // })
+ // );
+
+ for (;;) {
+ const progTake = take(progressChannel);
+ const errTake = take(errorChannel);
+ const endTake = take(endChannel);
+ const result = yield race({
+ progress: progTake,
+ error: errTake,
+ end: endTake,
+ });
+ if (result.err) {
+ throw result.err;
+ }
+ if (result.end) {
+ throw Error("unexpected end");
+ }
+ if (result.progress) {
+ const p: Progress = result.progress;
+ const newPct = Math.trunc(p.percentage);
+ if (newPct !== currentProgressPct) {
+ currentProgressPct = newPct;
+ if (progressHandler) {
+ yield progressHandler(
+ currentProgressPct,
+ p.speed,
+ p.eta,
+ p.transferred,
+ len
+ );
+ }
+ // yield put(
+ // RtcActions.fileReceiveProgress({
+ // fileRequest,
+ // totalBytes: len,
+ // downloadedBytes: p.transferred,
+ // downloadedPct: currentProgressPct,
+ // speed: p.speed,
+ // eta: p.eta,
+ // })
+ // );
+ }
+
+ if (p.remaining === 0) {
+ yield take(endChannel);
+ if (progressHandler) {
+ yield progressHandler(100, 0, 0, p.transferred, len);
+ }
+ // yield put(
+ // RtcActions.fileReceiveProgress({
+ // fileRequest,
+ // totalBytes: len,
+ // downloadedBytes: p.transferred,
+ // downloadedPct: 100,
+ // speed: 0,
+ // eta: 0,
+ // })
+ // );
+ break;
+ }
+ }
+ }
+ } finally {
+ cleanupOperations.forEach(op => op());
+ }
+}
diff --git a/src/rtc/sagas/helpers/copyStreamToRTCPeer.ts b/src/rtc/sagas/helpers/copyStreamToRTCPeer.ts
new file mode 100644
index 00000000..501b85ac
--- /dev/null
+++ b/src/rtc/sagas/helpers/copyStreamToRTCPeer.ts
@@ -0,0 +1,97 @@
+import { call, race, take } from "redux-saga/effects";
+import { RTCPeer } from "../../system/webRtc/RTCPeer";
+import { eventChannel, END } from "redux-saga";
+import { createRTCDataChannelWriteStream } from "./createRTCDataChannelWriteStream";
+import progressStream from "progress-stream";
+import { Progress } from "progress-stream";
+import * as stream from "stream";
+import { ProgressHandler } from "./ProgressHandler";
+
+const sendBufferedAmountLowThreshold = 262144; // 256KiB
+export const copyStreamToRTCPeer = <
+ T extends string,
+ TData extends string | Blob | ArrayBuffer | ArrayBufferView
+>(
+ stream: stream.Readable,
+ size: number,
+ peer: RTCPeer,
+ progressHandler?: ProgressHandler
+) =>
+ call(function*() {
+ const dataChannel = peer.dataChannel;
+ dataChannel.bufferedAmountLowThreshold = sendBufferedAmountLowThreshold;
+
+ const writeStream = createRTCDataChannelWriteStream(dataChannel);
+
+ yield* copyStream(stream, writeStream, size, progressHandler);
+ });
+
+function* copyStream(
+ readStream: stream.Readable,
+ writeStream: stream.Writable,
+ fileSize: number,
+ progressHandler?: ProgressHandler
+) {
+ const cleanupOperations: (() => void)[] = [];
+ try {
+ const progStream = progressStream({ length: fileSize, time: 500 });
+ const progressChannel = eventChannel(emitter => {
+ const handler = (progress: Progress) => {
+ emitter(progress);
+ if (progress.remaining === 0) {
+ emitter(END);
+ }
+ };
+ progStream.on("progress", handler);
+ return () => progStream.off("progress", handler);
+ });
+ cleanupOperations.push(() => progressChannel.close());
+ const pipe = readStream.pipe(progStream).pipe(writeStream);
+ const endChannel = eventChannel<{}>(emitter => {
+ const handler = () => emitter({});
+ pipe.on("finish", handler);
+ return () => pipe.off("finish", handler);
+ });
+ cleanupOperations.push(() => endChannel.close());
+ const errorChannel = eventChannel(emitter => {
+ const handler = (err: Error) => emitter(err);
+ pipe.on("error", handler);
+ return () => pipe.off("error", handler);
+ });
+ cleanupOperations.push(() => errorChannel.close());
+ let currentProgressPct = 0;
+ if (progressHandler) {
+ yield progressHandler(0, 0, undefined, 0, fileSize);
+ }
+ for (;;) {
+ const progTake = take(progressChannel);
+ const errTake = take(errorChannel);
+ const result = yield race({ progress: progTake, error: errTake });
+ if (result.err) {
+ throw result.err;
+ }
+ if (result.progress) {
+ const p: Progress = result.progress;
+ if (progressHandler) {
+ const newPct = Math.trunc(p.percentage);
+ if (newPct !== currentProgressPct) {
+ currentProgressPct = newPct;
+ yield progressHandler(
+ currentProgressPct,
+ p.speed,
+ p.eta,
+ p.transferred,
+ p.length
+ );
+ }
+ }
+ if (p.remaining === 0) {
+ break;
+ }
+ }
+ }
+ yield take(endChannel);
+ } finally {
+ cleanupOperations.forEach(op => op());
+ }
+}
diff --git a/src/rtc/sagas/helpers/createRTCDataChannelWriteStream.ts b/src/rtc/sagas/helpers/createRTCDataChannelWriteStream.ts
new file mode 100644
index 00000000..5c5ed2af
--- /dev/null
+++ b/src/rtc/sagas/helpers/createRTCDataChannelWriteStream.ts
@@ -0,0 +1,66 @@
+import * as stream from "stream"
+import { toArrayBuffer } from "../../../shared/system/bufferConversion";
+import { createPromiseResolver } from "../../../shared/system/createPromiseResolver";
+import { delay } from "../../../shared/system/delay";
+
+const maxSendBuffered = 2097152; // 2MiB
+
+const timeoutOnNoDataMoved = async (dataChannel: RTCDataChannel) => {
+ let currBufAmt = dataChannel.bufferedAmount
+ let now = performance.now()
+ let moved = false
+ for (; ;) {
+ await delay(250)
+ moved = moved || dataChannel.bufferedAmount !== currBufAmt
+ currBufAmt = dataChannel.bufferedAmount
+ if (moved) {
+ now = performance.now()
+ moved = false
+ } else {
+ if ((performance.now() - now) > 120000) {
+ break
+ }
+ }
+ }
+}
+
+const waitUntilSafeToSend = async (dataChannel: RTCDataChannel) => {
+ if (dataChannel.bufferedAmount > maxSendBuffered) {
+ const pr = createPromiseResolver();
+ dataChannel.onbufferedamountlow = () => pr.resolve();
+
+ const timeout = timeoutOnNoDataMoved(dataChannel)
+
+
+ await Promise.race([pr.promise, timeout].map(p => p.then(() => [p]))).then(([p]) => {
+ if (p === timeout) {
+ throw Error("timeout")
+ }
+ })
+
+ }
+}
+
+
+class RTCDataChannelWriteStream extends stream.Writable {
+ constructor(private dataChannel: RTCDataChannel, opts?: stream.WritableOptions) {
+ super(opts)
+ }
+ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
+ if (!Buffer.isBuffer(chunk)) {
+ throw Error("unsupported")
+ }
+ waitUntilSafeToSend(this.dataChannel).then(() => {
+ try {
+ this.dataChannel.send(toArrayBuffer(chunk, 0, chunk.length))
+ } catch (err) {
+ callback(err)
+ return
+ }
+ callback();
+ }).catch(err => callback(err))
+ }
+}
+
+export const createRTCDataChannelWriteStream =
+ (dataChannel: RTCDataChannel, opts?: stream.WritableOptions) => new RTCDataChannelWriteStream(dataChannel, opts)
\ No newline at end of file
diff --git a/src/rtc/sagas/helpers/createRTCPeerReadStream.ts b/src/rtc/sagas/helpers/createRTCPeerReadStream.ts
new file mode 100644
index 00000000..637b0f26
--- /dev/null
+++ b/src/rtc/sagas/helpers/createRTCPeerReadStream.ts
@@ -0,0 +1,87 @@
+import * as stream from "stream";
+import { toBuffer } from "../../../shared/system/bufferConversion";
+import { delay } from "../../../shared/system/delay";
+import { RTCPeer } from "../../../rtc/system/webRtc/RTCPeer";
+
+const hasArrayBuffer = typeof ArrayBuffer === "function";
+function isArrayBuffer(value: unknown): value is ArrayBuffer {
+ return (
+ hasArrayBuffer &&
+ (value instanceof ArrayBuffer ||
+ toString.call(value) === "[object ArrayBuffer]")
+ );
+}
+class RTCPeerReadStream<
+ T extends string,
+ TData extends string | Blob | ArrayBuffer | ArrayBufferView
+> extends stream.Readable {
+ private remaining: number;
+ private isReading: boolean;
+ private errors: Error[];
+ constructor(
+ private peer: RTCPeer,
+ length: number,
+ opts?: stream.ReadableOptions
+ ) {
+ super(opts);
+ this.remaining = length;
+ this.isReading = false;
+ this.errors = [];
+ }
+ _read(size: number): void {
+ const err =
+ this.errors.length > 0 ? this.errors[this.errors.length - 1] : null;
+ if (err) {
+ throw err;
+ }
+ if (this.isReading) {
+ return;
+ }
+ this.isReading = true;
+ (async () => {
+ try {
+ for (;;) {
+ if (this.remaining === 0) {
+ this.push(null);
+ break;
+ }
+ const messagePromise = this.peer.incomingMessageQueue.receive();
+ const timeoutPromise = delay(120000);
+
+ const [p] = await Promise.race(
+ [messagePromise, timeoutPromise].map(p =>
+ p.then(() => [p])
+ )
+ );
+ if (p === timeoutPromise) {
+ throw Error("timeout");
+ }
+ const data: TData = await (p as Promise);
+ if (!isArrayBuffer(data)) {
+ throw Error("unsupported");
+ }
+ console.log("pushing " + data.byteLength);
+ const pushResult = this.push(
+ toBuffer(data, 0, data.byteLength)
+ );
+ this.remaining -= data.byteLength;
+ if (!pushResult) {
+ break;
+ }
+ }
+ } catch (err) {
+ this.errors.push(err);
+ }
+ this.isReading = false;
+ })();
+ }
+}
+
+export const createRTCPeerReadStream = <
+ T extends string,
+ TData extends string | Blob | ArrayBuffer | ArrayBufferView
+>(
+ peer: RTCPeer,
+ length: number,
+ opts?: stream.ReadableOptions
+) => new RTCPeerReadStream(peer, length, opts);
diff --git a/src/rtc/sagas/helpers/receiveFileFromRTCPeer.ts b/src/rtc/sagas/helpers/receiveFileFromRTCPeer.ts
deleted file mode 100644
index ffcb8fd5..00000000
--- a/src/rtc/sagas/helpers/receiveFileFromRTCPeer.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { call, put, race } from "redux-saga/effects";
-import { RTCPeer } from "../../system/webRtc/RTCPeer";
-import * as util from 'util'
-import * as fs from 'fs'
-import { FileInfo } from "../../../shared/actions/payloadTypes/FileInfo";
-import { FileRequest } from "../../../shared/actions/payloadTypes/FileRequest";
-import { RtcActions } from "../../../shared/actions/rtc";
-import * as crypto from 'crypto'
-import { toBuffer } from "../../../shared/system/bufferConversion";
-import { delay } from "redux-saga";
-
-const fsOpenAsync = util.promisify(fs.open)
-const fsCloseAsync = util.promisify(fs.close)
-const fsWriteAsync = util.promisify(fs.write)
-const fsUnlinkAsync = util.promisify(fs.unlink)
-
-export const receiveFileFromRTCPeer =
-
- (savePath: string, peer: RTCPeer, fileNameInfo: FileInfo, fileRequest: FileRequest) => call(function* () {
-
- try {
- const shasum = crypto.createHash('sha256');
- const fileDescriptor: number = yield call(() => fsOpenAsync(savePath, "w"));
- try {
- let total = 0;
- let currentPct = -1
- for (; ;) {
- const { msg, timeout }: { msg: ArrayBuffer, timeout: any } = yield race({
- msg: call(() => peer.incomingMessageQueue.receive()),
- timeout: delay(120000)
- })
- if (timeout) {
- throw Error("timeout")
- }
- total += msg.byteLength;
- //console.log(`answerpeer received : ${total}`);
-
- const buffer = toBuffer(msg, 0, msg.byteLength)
- shasum.update(buffer)
- yield call(() => fsWriteAsync(fileDescriptor, buffer));
-
- if (total > fileNameInfo.size) {
- throw Error("more data than expected");
- }
- if (total === fileNameInfo.size) {
- break;
- }
- const pct = (total * 100 / fileNameInfo.size) >> 0
- if (currentPct !== pct) {
- currentPct = pct
- yield put(RtcActions.fileReceiveProgress({ fileRequest, totalBytes: fileNameInfo.size, downloadedBytes: total, downloadedPct: pct }))
- }
-
- }
- }
- finally {
- yield call(() => fsCloseAsync(fileDescriptor));
- }
- const computedHash = shasum.digest("base64")
- if (computedHash !== fileRequest.fileId) {
- throw Error("Checksum error: hash of downloaded data does not match that of requested data")
- }
- }
- catch (err) {
- try {
- yield call(() => fsUnlinkAsync(savePath));
- }
- catch { }
- throw err;
- }
- finally {
- peer.close();
- }
-
- });
diff --git a/src/rtc/sagas/processIncomingOfferSaga.ts b/src/rtc/sagas/processIncomingOfferSaga.ts
index a964bf3c..5c3eaf68 100644
--- a/src/rtc/sagas/processIncomingOfferSaga.ts
+++ b/src/rtc/sagas/processIncomingOfferSaga.ts
@@ -5,7 +5,7 @@ import { LinkMessageEnvelope } from "../../shared/actions/payloadTypes/LinkMessa
import { FileInfo } from "../../shared/actions/payloadTypes/FileInfo";
import { PromiseType } from "../../shared/system/generic-types/PromiseType";
import { getAnswerPeer } from "../system/webRtc/getAnswerPeer";
-import { copyFileToRTCPeer } from "./helpers/copyFileToRTCPeer";
+import { copyStreamToRTCPeer } from "./helpers/copyStreamToRTCPeer";
import { RtcActions } from "../../shared/actions/rtc";
import { prepareErrorForSerialization } from "../../shared/proxy/prepareErrorForSerialization";
import { FileRequest } from "../../shared/actions/payloadTypes/FileRequest";
@@ -15,86 +15,274 @@ import { blinq } from "blinq";
import { BdapActions } from "../../shared/actions/bdap";
import { SessionDescriptionEnvelope } from "../../shared/actions/payloadTypes/SessionDescriptionEnvelope";
import { ClientDownloadActions } from "../../shared/actions/clientDownload";
+import * as fs from "fs";
+import {
+ resourceScope,
+ ResourceScope,
+} from "../../shared/system/redux-saga/resourceScope";
+import { isFileListRequest } from "../../shared/actions/payloadTypes/FileListRequest";
+import { MainRootState } from "../../main/reducers";
+import { PublicSharedFile } from "../../shared/types/PublicSharedFile";
+import { entries } from "../../shared/system/entries";
+import { ReadableStreamBuffer } from "stream-buffers";
+import * as stream from "stream";
+//import { tuple } from "../../shared/system/tuple";
+//import { sharedFiles } from "src/shared/reducers";
+import { FileListResponse } from "../../shared/actions/payloadTypes/FileListResponse";
export function* processIncomingOfferSaga() {
const pred = (action: BdapActions) => {
switch (action.type) {
case getType(BdapActions.linkMessageReceived):
- return action.payload.message.type === "pshare-offer"
+ return action.payload.message.type === "pshare-offer";
default:
return false;
}
- }
- yield takeEvery(pred, function* (action: ActionType) {
- const offerEnvelope: LinkMessageEnvelope> = action.payload.message
- const { id: transactionId, payload: { sessionDescription: offerSdp, payload: fileRequest } } = offerEnvelope;
- const rtcConfig: RTCConfiguration = yield select((s: RtcRootState) => s.rtcConfig)
- const answerPeer: PromiseType> = yield call(() => getAnswerPeer(rtcConfig));
- const internalFileInfo: InternalFileInfo | null = yield getFileInfo(fileRequest);
+ };
+ yield takeEvery(pred, function*(
+ action: ActionType
+ ) {
+ const offerEnvelope: LinkMessageEnvelope<
+ SessionDescriptionEnvelope
+ > = action.payload.message;
+ const {
+ id: transactionId,
+ payload: { sessionDescription: offerSdp, payload: fileRequest },
+ } = offerEnvelope;
+
+ const internalFileInfo: MessageInfo | null = yield getFileInfo(
+ fileRequest
+ );
if (!internalFileInfo) {
- console.warn("could not retrieve file info for file request")
+ console.warn("could not retrieve file info for file request");
- return
+ return;
}
- const { localPath, ...fileInfo } = internalFileInfo;
- yield put(ClientDownloadActions.clientDownloadStarted({ fileRequest, fileInfo }))
- try {
- console.log(fileRequest);
- const offerSessionDescription = new RTCSessionDescription(offerSdp);
- const answer: RTCSessionDescription = yield call(() => answerPeer.getAnswer(offerSessionDescription));
-
- const answerEnvelope: LinkMessageEnvelope> = {
+ let answerPeer:
+ | PromiseType>
+ | undefined;
+ if (internalFileInfo.size > 0) {
+ const rtcConfig: RTCConfiguration = yield select(
+ (s: RtcRootState) => s.rtcConfig
+ );
+ answerPeer = yield call(() => getAnswerPeer(rtcConfig));
+ }
- id: transactionId,
- timestamp: Math.trunc((new Date()).getTime()),
- type: "pshare-answer",
- payload: { sessionDescription: answer.toJSON(), payload: fileInfo }
- };
- const routeEnvelope: LinkRouteEnvelope>> = {
- recipient: fileRequest.requestorUserName,
- payload: answerEnvelope
- };
- yield put(BdapActions.sendLinkMessage(routeEnvelope));
- yield call(() => answerPeer.waitForDataChannelOpen());
- try {
- yield copyFileToRTCPeer(localPath, answerPeer, (progressPct, downloadedBytes, size) => put(ClientDownloadActions.clientDownloadProgress({ fileRequest, progressPct, downloadedBytes, size })));
- }
- catch (err) {
- yield put(RtcActions.fileSendFailed(prepareErrorForSerialization(err)));
- return;
+ const scope = resourceScope(answerPeer, async peer => {
+ if (peer) {
+ await peer.close();
}
- finally {
- answerPeer.dataChannel.close();
+ });
+ yield* scope.use(function*(answerPeer) {
+ const { localPath, fileInfo, alternativeStream } = (() => {
+ if (isInternalFileInfo(internalFileInfo)) {
+ const {
+ localPath,
+ ...fileInfo
+ }: InternalFileInfo = internalFileInfo;
+ return { localPath, fileInfo, alternativeStream: null };
+ } else if (isInternalDirectoryInfo(internalFileInfo)) {
+ const { size, type, payload } = internalFileInfo;
+ return {
+ localPath: null,
+ fileInfo: { path: "file-list", size, type },
+ alternativeStream: payload,
+ };
+ } else {
+ throw Error("unexpected internalFileInfo type");
+ }
+ })();
+
+ if (fileInfo != null) {
+ //const { localPath, ...fileInfo } = internalFileInfo;
+
+ yield put(
+ ClientDownloadActions.clientDownloadStarted({
+ fileRequest,
+ fileInfo,
+ })
+ );
}
- } finally {
- yield put(ClientDownloadActions.clientDownloadComplete(fileRequest))
- }
+ let answer: RTCSessionDescription | undefined;
+ try {
+ if (answerPeer) {
+ console.log(fileRequest);
+ const offerSessionDescription = new RTCSessionDescription(
+ offerSdp
+ );
+ answer = yield call(() =>
+ answerPeer!.getAnswer(offerSessionDescription)
+ );
+ }
+
+ const answerEnvelope: LinkMessageEnvelope<
+ SessionDescriptionEnvelope
+ > = {
+ id: transactionId,
+ timestamp: Math.trunc(new Date().getTime()),
+ type: "pshare-answer",
+ payload: {
+ sessionDescription: answer
+ ? answer.toJSON()
+ : undefined,
+ payload: fileInfo,
+ },
+ };
+ const routeEnvelope: LinkRouteEnvelope<
+ LinkMessageEnvelope>
+ > = {
+ recipient: fileRequest.requestorUserName,
+ payload: answerEnvelope,
+ };
+ yield put(BdapActions.sendLinkMessage(routeEnvelope));
+ if (answerPeer) {
+ yield call(() => answerPeer!.waitForDataChannelOpen());
+ let fileSize: number;
+ let scope: ResourceScope;
+ if (localPath) {
+ const stats: fs.Stats = yield call(() =>
+ fs.promises.stat(localPath)
+ );
+ fileSize = stats.size;
+ const stream = fs.createReadStream(localPath);
+ scope = resourceScope(stream, () => stream.close());
+ } else if (alternativeStream) {
+ fileSize = fileInfo.size;
+ scope = resourceScope(alternativeStream, () => {});
+ } else {
+ throw Error("no localpath or alternativeStream");
+ }
+
+ //const scope = resourceScope(stream, s => s.close());
+ yield* scope.use(function*(readStream) {
+ try {
+ yield copyStreamToRTCPeer(
+ readStream,
+ fileSize,
+ answerPeer!,
+ (
+ progressPct,
+ speed,
+ eta,
+ downloadedBytes,
+ size
+ ) =>
+ put(
+ ClientDownloadActions.clientDownloadProgress(
+ {
+ fileRequest,
+ progressPct,
+ downloadedBytes,
+ size,
+ speed,
+ eta,
+ }
+ )
+ )
+ );
+ } catch (err) {
+ yield put(
+ RtcActions.fileSendFailed(
+ prepareErrorForSerialization(err)
+ )
+ );
+ return;
+ }
+ });
+ }
+ } finally {
+ yield put(
+ ClientDownloadActions.clientDownloadComplete(fileRequest)
+ );
+ }
+ });
});
}
-interface InternalFileInfo {
- localPath: string;
+
+interface MessageInfo {
type: string;
size: number;
+}
+interface InternalFileInfo extends MessageInfo {
+ localPath: string;
path: string;
}
+function isInternalFileInfo(item: MessageInfo): item is InternalFileInfo {
+ const x = item as InternalFileInfo;
+ return x.hasOwnProperty("localPath") && x.hasOwnProperty("path");
+}
+interface InternalDirectoryInfo extends MessageInfo {
+ requestId: string;
+ payload: stream.Readable;
+}
+function isInternalDirectoryInfo(
+ item: MessageInfo
+): item is InternalDirectoryInfo {
+ const x = item as InternalDirectoryInfo;
+ return x.hasOwnProperty("requestId") && x.hasOwnProperty("payload");
+}
function getFileInfo(fileRequest: FileRequest) {
- return call(function* () {
- const sharedFiles: SharedFile[] =
- yield select((s: RtcRootState) =>
- (s.fileWatch.users[fileRequest.requestorUserName] && Object.values(s.fileWatch.users[fileRequest.requestorUserName].out)) || [])
- const sharedFile = blinq(sharedFiles).firstOrDefault(f => f.hash === fileRequest.fileId)
- if (typeof sharedFile === 'undefined') {
- return null
+ return call(function*() {
+ if (isFileListRequest(fileRequest)) {
+ const filesRecord: Record = yield select(
+ (s: MainRootState) => {
+ if (s.fileWatch.users[fileRequest.requestorUserName]) {
+ return s.fileWatch.users[fileRequest.requestorUserName]
+ .out;
+ } else {
+ return {};
+ }
+ }
+ );
+ const sharedFiles: PublicSharedFile[] = entries(filesRecord)
+ .select(([fileName, v]) => ({
+ fileName,
+ //hash: v.hash!,
+ size: v.size!,
+ contentType: v.contentType!,
+ }))
+ .toArray();
+
+ const memStream = new ReadableStreamBuffer();
+
+ const response: FileListResponse = {
+ requestId: fileRequest.requestId,
+ sharedFiles,
+ };
+ memStream.put(JSON.stringify(response));
+ memStream.stop();
+
+ const di: InternalDirectoryInfo = {
+ requestId: fileRequest.requestId,
+ payload: memStream,
+ size: memStream.size(),
+ type: "application/json",
+ };
+ return di;
+ }
+ const sharedFiles: SharedFile[] = yield select(
+ (s: RtcRootState) =>
+ (s.fileWatch.users[fileRequest.requestorUserName] &&
+ Object.values(
+ s.fileWatch.users[fileRequest.requestorUserName].out
+ )) ||
+ []
+ );
+ const sharedFile = blinq(sharedFiles).firstOrDefault(
+ f => f.relativePath === fileRequest.fileName
+ );
+ if (typeof sharedFile === "undefined") {
+ return null;
}
const output: InternalFileInfo = {
localPath: sharedFile.path,
type: sharedFile.contentType!,
size: sharedFile.size!,
- path: sharedFile.relativePath
+ path: sharedFile.relativePath,
};
return output;
});
diff --git a/src/rtc/sagas/requestFileSaga.ts b/src/rtc/sagas/requestFileSaga.ts
index 50ff0c77..f30f528b 100644
--- a/src/rtc/sagas/requestFileSaga.ts
+++ b/src/rtc/sagas/requestFileSaga.ts
@@ -6,102 +6,279 @@ import { LinkMessageEnvelope } from "../../shared/actions/payloadTypes/LinkMessa
import { FileRequest } from "../../shared/actions/payloadTypes/FileRequest";
import { PromiseType } from "../../shared/system/generic-types/PromiseType";
import { getOfferPeer } from "../system/webRtc/getOfferPeer";
-import { v4 as uuid } from 'uuid';
-import * as path from 'path'
+import { v4 as uuid } from "uuid";
+import * as path from "path";
import { RtcActions } from "../../shared/actions/rtc";
-import { receiveFileFromRTCPeer } from "./helpers/receiveFileFromRTCPeer";
+import { copyFromRTCPeerToStream } from "./helpers/copyFromRTCPeerToStream";
import { prepareErrorForSerialization } from "../../shared/proxy/prepareErrorForSerialization";
-import { UserSharePaths, getOrCreateShareDirectoriesForUser } from "./helpers/getOrCreateShareDirectoriesForUser";
+import {
+ UserSharePaths,
+ getOrCreateShareDirectoriesForUser,
+} from "./helpers/getOrCreateShareDirectoriesForUser";
import { delay } from "redux-saga";
-import * as fs from 'fs'
-import { FileRequestWithSavePath } from "../../shared/actions/payloadTypes/FileRequestWithSavePath";
+import * as fs from "fs";
import { RtcRootState } from "../reducers";
import { BdapActions } from "../../shared/actions/bdap";
import { SessionDescriptionEnvelope } from "../../shared/actions/payloadTypes/SessionDescriptionEnvelope";
import { FileInfo } from "../../shared/actions/payloadTypes/FileInfo";
+import { resourceScope } from "../../shared/system/redux-saga/resourceScope";
+import * as util from "util";
+import { deleteProperty } from "../../shared/system/deleteProperty";
+import { WritableStreamBuffer } from "stream-buffers";
+import * as stream from "stream";
+import { isFileRequestWithSavePath } from "../../shared/actions/payloadTypes/FileRequestWithSavePath";
+import { isFileListRequest } from "../../shared/actions/payloadTypes/FileListRequest";
+import { FileListResponse } from "../../shared/actions/payloadTypes/FileListResponse";
+const fsUnlinkAsync = util.promisify(fs.unlink);
//this runs in rtc
export function* requestFileSaga() {
- yield takeEvery(getType(FileSharingActions.requestFileWithSavePath), function* (action: ActionType) {
- const rtcConfig: RTCConfiguration = yield select((s: RtcRootState) => s.rtcConfig)
+ yield takeEvery(getType(FileSharingActions.startRequestFile), function*(
+ action: ActionType
+ ) {
+ const rtcConfig: RTCConfiguration = yield select(
+ (s: RtcRootState) => s.rtcConfig
+ );
- const peer: PromiseType> = yield call(() => getOfferPeer(rtcConfig))
- try {
- const fileRequest: FileRequestWithSavePath = action.payload;
- yield put(RtcActions.fileReceiveProgress({ fileRequest, downloadedBytes: 0, totalBytes: 0, downloadedPct: 0, status: "negotiating connection" }))
- const offer: RTCSessionDescription = yield call(() => peer.createOffer())
- yield put(RtcActions.fileReceiveProgress({ fileRequest, downloadedBytes: 0, totalBytes: 0, downloadedPct: 0, status: "sending offer" }))
- const offerEnvelope: LinkMessageEnvelope> = {
+ const peer: PromiseType> = yield call(
+ () => getOfferPeer(rtcConfig)
+ );
- payload: { payload: fileRequest, sessionDescription: offer.toJSON() },
+ const scope = resourceScope(peer, peer => peer.close());
+ yield* scope.use(function*(peer) {
+ const incomingFileRequest = action.payload;
+ const fileRequest: FileRequest = isFileRequestWithSavePath(
+ incomingFileRequest
+ )
+ ? deleteProperty(incomingFileRequest, "savePath")
+ : incomingFileRequest;
+ yield put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes: 0,
+ totalBytes: 0,
+ downloadedPct: 0,
+ status: "negotiating connection",
+ speed: 0,
+ })
+ );
+ const offer: RTCSessionDescription = yield call(() =>
+ peer.createOffer()
+ );
+ yield put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes: 0,
+ totalBytes: 0,
+ downloadedPct: 0,
+ status: "sending offer",
+ speed: 0,
+ })
+ );
+ const offerEnvelope: LinkMessageEnvelope<
+ SessionDescriptionEnvelope
+ > = {
+ payload: {
+ payload: fileRequest,
+ sessionDescription: offer.toJSON(),
+ },
id: uuid(),
- timestamp: Math.trunc((new Date()).getTime()),
- type: "pshare-offer"
- }
- const routeEnvelope: LinkRouteEnvelope>> = {
+ timestamp: Math.trunc(new Date().getTime()),
+ type: "pshare-offer",
+ };
+ const routeEnvelope: LinkRouteEnvelope<
+ LinkMessageEnvelope>
+ > = {
recipient: action.payload.ownerUserName,
- payload: offerEnvelope
- }
- yield put(BdapActions.sendLinkMessage(routeEnvelope))
- yield put(RtcActions.fileReceiveProgress({ fileRequest, downloadedBytes: 0, totalBytes: 0, downloadedPct: 0, status: "waiting for answer" }))
+ payload: offerEnvelope,
+ };
+ yield put(BdapActions.sendLinkMessage(routeEnvelope));
+ yield put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes: 0,
+ totalBytes: 0,
+ downloadedPct: 0,
+ status: "waiting for answer",
+ speed: 0,
+ })
+ );
const pred = (action: BdapActions) => {
switch (action.type) {
case getType(BdapActions.linkMessageReceived):
- return action.payload.message.type === "pshare-answer" && action.payload.message.id === offerEnvelope.id
+ return (
+ action.payload.message.type === "pshare-answer" &&
+ action.payload.message.id === offerEnvelope.id
+ );
default:
return false;
}
- }
+ };
-
-
- const { linkMessage }: { linkMessage: ActionType } = yield race({
+ const {
+ linkMessage,
+ }: {
+ linkMessage: ActionType;
+ } = yield race({
timeout: delay(60 * 1000),
- linkMessage: take(pred)
- })
+ linkMessage: take(pred),
+ });
if (!linkMessage) {
- yield put(RtcActions.fileReceiveFailed({ fileRequest, error: prepareErrorForSerialization(Error("timeout")) }))
- yield delay(10000)
- yield put(RtcActions.fileReceiveReset(fileRequest))
- return
+ yield put(
+ RtcActions.fileReceiveFailed({
+ fileRequest,
+ error: prepareErrorForSerialization(Error("timeout")),
+ })
+ );
+ yield delay(10000);
+ yield put(RtcActions.fileReceiveReset(fileRequest));
+ return;
}
- const answerEnvelope: LinkMessageEnvelope> = linkMessage.payload.message
- yield put(RtcActions.fileReceiveProgress({ fileRequest, downloadedBytes: 0, totalBytes: 0, downloadedPct: 0, status: "connecting to peer" }))
-
- const { payload: { sessionDescription: answerSdp, payload: fileInfo } } = answerEnvelope
+ const answerEnvelope: LinkMessageEnvelope<
+ SessionDescriptionEnvelope
+ > = linkMessage.payload.message;
+ yield put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes: 0,
+ totalBytes: 0,
+ downloadedPct: 0,
+ status: "connecting to peer",
+ speed: 0,
+ })
+ );
- const answerSessionDescription = new RTCSessionDescription(answerSdp);
- yield call(() => peer.setRemoteDescription(answerSessionDescription))
- yield call(() => peer.waitForDataChannelOpen())
- yield put(RtcActions.fileReceiveProgress({ fileRequest, downloadedBytes: 0, totalBytes: 0, downloadedPct: 0, status: "connected to peer" }))
+ const {
+ payload: { sessionDescription: answerSdp, payload: fileInfo },
+ } = answerEnvelope;
+ const otherEndUser = action.payload.ownerUserName;
+ const {
+ temp,
+ }: UserSharePaths = yield getOrCreateShareDirectoriesForUser(
+ otherEndUser
+ );
+ const tempPath = path.join(temp, `__${uuid()}`);
+ if (fileInfo.size > 0) {
+ const answerSessionDescription = new RTCSessionDescription(
+ answerSdp
+ );
+ yield call(() =>
+ peer.setRemoteDescription(answerSessionDescription)
+ );
+ yield call(() => peer.waitForDataChannelOpen());
+ yield put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes: 0,
+ totalBytes: 0,
+ downloadedPct: 0,
+ status: "connected to peer",
+ speed: 0,
+ })
+ );
+ //debugger
+ try {
+ const scope = isFileRequestWithSavePath(incomingFileRequest)
+ ? resourceScope(
+ fs.createWriteStream(tempPath) as stream.Writable,
+ s => (s as fs.WriteStream).close()
+ )
+ : resourceScope(
+ new WritableStreamBuffer({}) as stream.Writable,
+ () => {}
+ );
+ yield* scope.use(function*(s: stream.Writable) {
+ yield copyFromRTCPeerToStream(
+ s,
+ fileInfo.size,
+ peer,
+ (progress, speed, eta, downloadedBytes, size) =>
+ put(
+ RtcActions.fileReceiveProgress({
+ fileRequest,
+ downloadedBytes,
+ totalBytes: size,
+ downloadedPct: progress,
+ status: "downloading",
+ speed,
+ eta,
+ })
+ )
+ );
+ if (!isFileRequestWithSavePath(incomingFileRequest)) {
+ const streamBuffer = s as WritableStreamBuffer;
+ const message = streamBuffer.getContentsAsString();
+ if (isFileListRequest(incomingFileRequest)) {
+ console.log(`MESSAGE: ${message}`);
+ } else {
+ throw Error("unexpected fileRequest type");
+ }
+ if (message) {
+ const response: FileListResponse = JSON.parse(
+ message
+ );
+ yield put(
+ FileSharingActions.fileListResponse(
+ response
+ )
+ ); // dispatch response to app
+ }
+ }
+ });
+ } catch (err) {
+ if (isFileRequestWithSavePath(incomingFileRequest)) {
+ yield call(() => fsUnlinkAsync(tempPath));
+ }
- const otherEndUser = action.payload.ownerUserName
- const { temp }: UserSharePaths = yield getOrCreateShareDirectoriesForUser(otherEndUser);
- const tempPath = path.join(temp, `__${uuid()}`)
-
- //debugger
- try {
- yield receiveFileFromRTCPeer(tempPath, peer, fileInfo, fileRequest)
- } catch (err) {
- yield put(RtcActions.fileReceiveFailed({ fileRequest, error: prepareErrorForSerialization(err) }))
- yield delay(10000)
- yield put(RtcActions.fileReceiveReset(fileRequest))
- return
+ yield put(
+ RtcActions.fileReceiveFailed({
+ fileRequest,
+ error: prepareErrorForSerialization(err),
+ })
+ );
+ yield delay(10000);
+ yield put(RtcActions.fileReceiveReset(fileRequest));
+ return;
+ }
+ } else {
+ yield call(() => touchFile(tempPath));
}
+ if (isFileRequestWithSavePath(incomingFileRequest)) {
+ yield call(() =>
+ fs.promises.rename(tempPath, incomingFileRequest.savePath)
+ );
+ }
- yield call(() => fs.promises.rename(tempPath, fileRequest.savePath));
- yield put(RtcActions.fileReceiveSuccess(fileRequest))
- yield delay(10000)
- yield put(RtcActions.fileReceiveReset(fileRequest))
+ yield put(RtcActions.fileReceiveSuccess(fileRequest));
+ yield delay(10000);
+ yield put(RtcActions.fileReceiveReset(fileRequest));
+ });
+ });
+}
- }
- finally {
- peer.close()
- }
- })
+function touchFile(filePath: string) {
+ const time = new Date();
+ return new Promise((resolve, reject) =>
+ fs.utimes(filePath, time, time, err => {
+ if (err) {
+ fs.open(filePath, "w", (err, fd) => {
+ if (err) {
+ reject(err);
+ }
+ fs.close(fd, err => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+ })
+ );
}
diff --git a/src/rtc/system/webRtc/RTCPeer.ts b/src/rtc/system/webRtc/RTCPeer.ts
index 9b30ea08..412de4d7 100644
--- a/src/rtc/system/webRtc/RTCPeer.ts
+++ b/src/rtc/system/webRtc/RTCPeer.ts
@@ -5,5 +5,5 @@ export interface RTCPeer;
readonly dataChannel: RTCDataChannel;
send: (data: TData) => void;
- close: () => void;
+ close: () => Promise;
}
diff --git a/src/rtc/system/webRtc/getAnswerPeer.ts b/src/rtc/system/webRtc/getAnswerPeer.ts
index ab52ff5d..e1e8b0f5 100644
--- a/src/rtc/system/webRtc/getAnswerPeer.ts
+++ b/src/rtc/system/webRtc/getAnswerPeer.ts
@@ -3,9 +3,12 @@ import { createPromiseResolver } from "../../../shared/system/createPromiseResol
import { createAsyncQueue } from "../../../shared/system/createAsyncQueue";
import { AnswerPeerEvents } from "./AnswerPeerEvents";
import { RTCAnswerPeer } from "./RTCAnswerPeer";
+import { waitForDrained } from "./waitForDrained";
+import { delay } from "../../../shared/system/delay";
-export async function getAnswerPeer(peerConnectionConfig: RTCConfiguration): Promise> {
-
+export async function getAnswerPeer<
+ T extends string | Blob | ArrayBuffer | ArrayBufferView
+>(peerConnectionConfig: RTCConfiguration): Promise> {
const eventDispatcher = createEventEmitter();
const peer = new RTCPeerConnection(peerConnectionConfig);
@@ -20,10 +23,10 @@ export async function getAnswerPeer queue.post(e.data);
dataChannel.onopen = e => eventDispatcher.dispatchEvent("open", e);
- eventDispatcher.dispatchEvent("datachannel", dataChannel)
+ eventDispatcher.dispatchEvent("datachannel", dataChannel);
};
- peer.onicecandidate = (event) => {
- console.log('answerPeer ice candidate');
+ peer.onicecandidate = event => {
+ console.log("answerPeer ice candidate");
if (event.candidate) {
// These would normally be sent to answerPeer over some other transport,
// like a websocket, but since this is local we can just set it here.
@@ -31,70 +34,80 @@ export async function getAnswerPeer {
- const pr = createPromiseResolver()
- eventDispatcher.once("sessiondescription", (sd: RTCSessionDescription) => pr.resolve(sd))
- await peer.setRemoteDescription(offer)
- const answer = new RTCSessionDescription(await peer.createAnswer({}))
- await peer.setLocalDescription(answer)
- return await pr.promise
+ const pr = createPromiseResolver();
+ eventDispatcher.once(
+ "sessiondescription",
+ (sd: RTCSessionDescription) => pr.resolve(sd)
+ );
+ await peer.setRemoteDescription(offer);
+ const answer = new RTCSessionDescription(
+ await peer.createAnswer({})
+ );
+ await peer.setLocalDescription(answer);
+ return await pr.promise;
},
waitForDataChannelOpen: async () => {
- const pr = createPromiseResolver()
- eventDispatcher.once("datachannel", (dataChannel: RTCDataChannel) => pr.resolve(dataChannel))
- const dc = await pr.promise
+ const pr = createPromiseResolver();
+ eventDispatcher.once("datachannel", (dataChannel: RTCDataChannel) =>
+ pr.resolve(dataChannel)
+ );
+ const dc = await pr.promise;
if (dc.readyState !== "open") {
- const prom = createPromiseResolver()
+ const prom = createPromiseResolver();
const res = () => prom.resolve();
const rej: (evtObj: any) => void = e => prom.reject(e);
- eventDispatcher.addEventListener("open", res)
- eventDispatcher.addEventListener("error", rej)
+ eventDispatcher.addEventListener("open", res);
+ eventDispatcher.addEventListener("error", rej);
try {
- await (prom.promise)
-
+ await prom.promise;
} finally {
- eventDispatcher.removeEventListener("open", res)
- eventDispatcher.removeEventListener("error", rej)
-
+ eventDispatcher.removeEventListener("open", res);
+ eventDispatcher.removeEventListener("error", rej);
}
}
- dataChannel = dc
- return dc
-
+ dataChannel = dc;
+ return dc;
},
addEventListener: eventDispatcher.addEventListener,
once: eventDispatcher.once,
removeEventListener: eventDispatcher.removeEventListener,
- get incomingMessageQueue() { return queue },
+ get incomingMessageQueue() {
+ return queue;
+ },
get dataChannel() {
if (dataChannel) {
- return dataChannel
+ return dataChannel;
} else {
- throw Error("RTCDataChannel not yet acquired, did you waitForDataChannelOpen()?")
+ throw Error(
+ "RTCDataChannel not yet acquired, did you waitForDataChannelOpen()?"
+ );
}
},
send: (data: T) => {
if (dataChannel) {
dataChannel.send(data as any);
} else {
- throw Error("no data channel")
+ throw Error("no data channel");
}
-
},
- close: () => {
+ close: async () => {
+ await waitForDrained(rtcPeer);
+ await delay(20000);
dataChannel && dataChannel.close();
- peer.close()
- }
- }
+ peer.close();
+ },
+ };
+ return rtcPeer;
}
diff --git a/src/rtc/system/webRtc/getOfferPeer.ts b/src/rtc/system/webRtc/getOfferPeer.ts
index dedc15b1..b5f56d8d 100644
--- a/src/rtc/system/webRtc/getOfferPeer.ts
+++ b/src/rtc/system/webRtc/getOfferPeer.ts
@@ -3,13 +3,16 @@ import { createAsyncQueue } from "../../../shared/system/createAsyncQueue";
import { createPromiseResolver } from "../../../shared/system/createPromiseResolver";
import { RTCOfferPeer } from "./RTCOfferPeer";
import { OfferPeerEvents } from "./OfferPeerEvents";
+import { waitForDrained } from "./waitForDrained";
+import { delay } from "redux-saga";
-export async function getOfferPeer(peerConnectionConfig: RTCConfiguration): Promise> {
-
+export async function getOfferPeer<
+ T extends string | Blob | ArrayBuffer | ArrayBufferView
+>(peerConnectionConfig: RTCConfiguration): Promise> {
const eventDispatcher = createEventEmitter();
const peer = new RTCPeerConnection(peerConnectionConfig);
peer.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
- console.log('offerPeer ice candidate');
+ console.log("offerPeer ice candidate");
if (event.candidate) {
// These would normally be sent to answerPeer over some other transport,
// like a websocket, but since this is local we can just set it here.
@@ -17,12 +20,15 @@ export async function getOfferPeer();
@@ -31,42 +37,51 @@ export async function getOfferPeer queue.post(e.data);
dataChannel.onopen = e => eventDispatcher.dispatchEvent("open", e);
- return {
+ const rtcPeer = {
createOffer: async () => {
- const pr = createPromiseResolver()
- eventDispatcher.once("sessiondescription", (sd: RTCSessionDescription) => pr.resolve(sd))
+ const pr = createPromiseResolver();
+ eventDispatcher.once(
+ "sessiondescription",
+ (sd: RTCSessionDescription) => pr.resolve(sd)
+ );
const offerInit = await peer.createOffer({});
const offer = new RTCSessionDescription(offerInit);
await peer.setLocalDescription(offer);
- return await pr.promise
+ return await pr.promise;
},
waitForDataChannelOpen: async () => {
- const prom = createPromiseResolver()
+ const prom = createPromiseResolver();
const res = () => prom.resolve();
const rej: (evtObj: any) => void = e => prom.reject(e);
- eventDispatcher.addEventListener("open", res)
- eventDispatcher.addEventListener("error", rej)
+ eventDispatcher.addEventListener("open", res);
+ eventDispatcher.addEventListener("error", rej);
try {
- await (prom.promise)
-
+ await prom.promise;
} finally {
- eventDispatcher.removeEventListener("open", res)
- eventDispatcher.removeEventListener("error", rej)
-
+ eventDispatcher.removeEventListener("open", res);
+ eventDispatcher.removeEventListener("error", rej);
}
- return dataChannel
+ return dataChannel;
},
- setRemoteDescription: (sessionDescription: RTCSessionDescription) => peer.setRemoteDescription(sessionDescription),
+ setRemoteDescription: (sessionDescription: RTCSessionDescription) =>
+ peer.setRemoteDescription(sessionDescription),
//addIceCandidate: (candidate: RTCIceCandidate) => peer.addIceCandidate(candidate),
addEventListener: eventDispatcher.addEventListener,
once: eventDispatcher.once,
removeEventListener: eventDispatcher.removeEventListener,
- get incomingMessageQueue() { return queue },
- get dataChannel() { return dataChannel },
+ get incomingMessageQueue() {
+ return queue;
+ },
+ get dataChannel() {
+ return dataChannel;
+ },
send: (data: T) => dataChannel.send(data as any),
- close: () => {
+ close: async () => {
+ await waitForDrained(rtcPeer);
+ await delay(20000);
dataChannel && dataChannel.close();
- peer.close()
- }
+ peer.close();
+ },
};
+ return rtcPeer;
}
diff --git a/src/rtc/system/webRtc/waitForDrained.ts b/src/rtc/system/webRtc/waitForDrained.ts
new file mode 100644
index 00000000..25a4f306
--- /dev/null
+++ b/src/rtc/system/webRtc/waitForDrained.ts
@@ -0,0 +1,23 @@
+import { delay } from "../../../shared/system/delay";
+import { OfferPeerEvents } from "./OfferPeerEvents";
+import { AnswerPeerEvents } from "./AnswerPeerEvents";
+import { RTCPeer } from "./RTCPeer";
+
+export const waitForDrained = async <
+ TEvents extends AnswerPeerEvents | OfferPeerEvents,
+ T extends string | Blob | ArrayBuffer | ArrayBufferView
+>(
+ peer: RTCPeer
+) => {
+ const bufferWaitTimeout = delay(120000);
+ while (peer && peer.dataChannel && peer.dataChannel.bufferedAmount > 0) {
+ const intervalDelay = delay(250);
+ const [winningPromise] = await Promise.race(
+ [bufferWaitTimeout, intervalDelay].map(p => p.then(() => [p]))
+ );
+
+ if (winningPromise === bufferWaitTimeout) {
+ break;
+ }
+ }
+};
diff --git a/src/shared/actions/addFile.ts b/src/shared/actions/addFile.ts
index 8dd4a0c2..ed6cb017 100644
--- a/src/shared/actions/addFile.ts
+++ b/src/shared/actions/addFile.ts
@@ -3,5 +3,6 @@ import { FilePathInfo } from '../types/FilePathInfo';
export const AddFileActions = {
close: createStandardAction('add_files/CLOSE')(),
filesSelected: createStandardAction('add_files/FILES_SELECTED')(),
+ failed: createStandardAction('add_files/FAILED')(),
};
export type AddFileActions = ActionType;
diff --git a/src/shared/actions/bdap.ts b/src/shared/actions/bdap.ts
index 18d7d6f5..a3940933 100644
--- a/src/shared/actions/bdap.ts
+++ b/src/shared/actions/bdap.ts
@@ -37,6 +37,14 @@ export const BdapActions = {
getPendingAcceptLinksSuccess: createStandardAction('bdap/GET_PENDING_ACCEPT_LINKS_SUCCESS')(),
getPendingAcceptLinksFailed: createStandardAction('bdap/GET_PENDING_ACCEPT_LINKS_FAILED')(),
+ getBalance: createStandardAction('bdap/GET_BALANCE')(),
+ getBalanceSuccess: createStandardAction('bdap/GET_BALANCE_SUCCESS')(),
+ getBalanceFailed: createStandardAction('bdap/GET_BALANCE_FAILED')(),
+
+ getTopUpAddress: createStandardAction('bdap/GET_TOP_UP_ADDRESS')(),
+ getTopUpAddressSuccess: createStandardAction('bdap/GET_TOP_UP_ADDRESS_SUCCESS')(),
+ getTopUpAddressFailed: createStandardAction('bdap/GET_TOP_UP_ADDRESS_FAILED')(),
+
bdapDataFetchSuccess: createStandardAction('bdap/BDAP_DATA_FETCH_SUCCESS')(),
bdapDataFetchFailed: createStandardAction('bdap/BDAP_DATA_FETCH_FAILED')(),
@@ -57,7 +65,10 @@ export const BdapActions = {
sendLinkMessage: createStandardAction('bdap/SEND_LINK_MESSAGE')>>(),
- linkMessageReceived:createStandardAction('bdap/LINK_MESSAGE_RECEIVED')<{message:LinkMessageEnvelope,rawMessage:LinkMessage}>(),
+ linkMessageReceived: createStandardAction('bdap/LINK_MESSAGE_RECEIVED')<{ message: LinkMessageEnvelope, rawMessage: LinkMessage }>(),
+
+ insufficientFunds: createStandardAction('bdap/INSUFFICIENT_FUNDS')(),
+ fundsDialogDismissed: createStandardAction('bdap/FUNDS_DIALOG_DISMISSED')(),
}
diff --git a/src/shared/actions/bulkImport.ts b/src/shared/actions/bulkImport.ts
new file mode 100644
index 00000000..f782f501
--- /dev/null
+++ b/src/shared/actions/bulkImport.ts
@@ -0,0 +1,26 @@
+import { createStandardAction, ActionType } from "typesafe-actions";
+import { FilePathInfo } from "../types/FilePathInfo";
+import { RequestStatus } from "../../main/sagas/bulkImportSaga";
+
+export const BulkImportActions = {
+ previewBulkImport: createStandardAction('bulkImport/PREVIEW_BULK_IMPORT')(),
+ previewData: createStandardAction('bulkImport/PREVIEW_DATA')