Upgrade daemon to 0.19.0 #1032

Merged
liamcardenas merged 15 commits from daemon19 into master 2018-03-09 08:53:39 +01:00
46 changed files with 2661 additions and 1314 deletions
Showing only changes of commit cbe05c3bcc - Show all commits

View file

@ -4,7 +4,7 @@
"airbnb", "airbnb",
"plugin:import/electron", "plugin:import/electron",
"plugin:flowtype/recommended", "plugin:flowtype/recommended",
"plugin:prettier/recommended" "prettier"
], ],
"settings": { "settings": {
"import/resolver": { "import/resolver": {

View file

@ -1,10 +1,9 @@
[ignore] [ignore]
.*/node_modules/**
[include] [include]
[libs] [libs]
flow-typed ./flow-typed
[lints] [lints]

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
/node_modules /node_modules
/dist /dist
/build/daemon.ver /build/daemon.ver
/build/daemon.zip
/build/venv /build/venv
*.pyc *.pyc
/static/daemon/lbrynet* /static/daemon/lbrynet*

View file

@ -1,28 +1,50 @@
env: matrix:
global: include:
- TRANSIFEX_USER: api - os: osx
- secure: u6gwnZlPGJLnvpoPPCpUZ+jBPajQuIW1+aq6UGW57z54AUjTAECxaYpqcGTGtDBjYark/yeiso887wP/EmJva7hMHeNMf6uLqwzP3YFsIv/Iv+9c1f4MXbJNgOrEKN834o/BdkD4ifi9CCiH9uPPVYGPx1bvfaxGpcHmQXdW4F4S0uj+jePB257mt+afGiNlz9wET6kWJKNNZf/4BNmefldVNq7h6oTSLsyO1TBhDcvSjatpKIwmXUNQfSUTFWvrtpWUB/m/IzitGuUtrt82vU2fPl7tuH6BHNrNp58MINjFzXXJLC+mMybb2UBDIAuc3+k7vj4J0U2rkcTloxDNCKNmYa1jBogOKBRgGp98Ct7E0V2vuLGAPniUbvBcCGK1wwed7uwDjsz3YNCGxUEcyyWc3OVDgN/up4+gXHxkh9FTpZy8Q3rSZx4Lwj700impBUQIVh/5p7Vgv+bSUdOeVRAMlcP9yT83jX50w9LkJMfICFPNv1tOZ3/SOnnB+JdW/ahpplFI4Z68/fBLttZTeaNcU4f28oJvPer8Wll+Elx5kxwLqLbwVUFNlxTxY1LYnPB7SPjGxrFNy3mVTRq5Pxp1hMiTZF4TlapkfHgR+gEzk2wpcJGmub70tW2baZaJF0jDBWIh7GXV+EGve53BKDhpX2Z6jTK0gkhrSUW1MT8= osx_image: xcode9.2
- secure: h1r9Qzv2xHRQl7nDHcscB4qDv7KlF/ncgHko1YuoY4oLZipBV8mzQXDmn3nlMKwaKOe1/Tty/bjoZexkWict4cwKBzU7/1HtJeMa6nxRICuS6DiVhLUNGZEddK6jQLxeEZFxkFPSCZyjybPWtasF8f8jd0lqqLIL4/FcIVV56aRCKAsUwCbedxi8Vnc19l74xjaQIK82xBFYOQPK078OBovk9DDOnicTjMulUo3/pKEZD1njSdcEhfSRv+MFE+31B/a6lpoLo7twPlyzLMfpo30NlEzIN0TeMAk44e4PV6DYg0wntC2GJ21p4BqMnDGocwnZwm7gpjflzUZdW6hF0esGLcqOdbyJLUb3rNX9AzQmn0p9KwDC3S80peZFxSiuLJGL8eivceVDUK/jwWinu3OHDJ/eO5iMDm9odm2ALemPtrDTSlRNT8HzNCY9PQTU9Dhdm4Q/dGDsRPWibFJSJ/qGKhVgadk2CUEAPua0hB1zZ556PkTGx4R1JDscgFDAkgemzgKl4Z/4qK3xGDoEtz1HmBlvQtn+B/PuhA2essADj0iTDiItxb7AYTA7EzsHEcRMmrbYarZ3Eh2onWy2GOpAGRN5Xl9cBIDbibcSC6BLI1m2PcLABpP7DhUX4bJbsVNSiGesHEU7o9Dgn8Ig09eHW/8F9i0VVoGUZXxKMJ8= language: node_js
node_js: "9"
os: linux env:
dist: xenial - ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
branches: - os: linux
only: services: docker
- "/^v\\d+\\.\\d+\\.\\d+$/" language: generic
before_cache:
install: - rm -rf $HOME/.cache/electron-builder/wine
- rvm install 2.3.1 cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
before_install:
- |
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.3.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.3.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1
export PATH="/tmp/git-lfs:$PATH"
fi
before_script:
- git lfs pull
script: script:
- rvm use 2.3.1 && gem install danger --version '~> 4.0' && danger - |
- FULL_BUILD=true ./build.sh if [ "$TRAVIS_OS_NAME" == "linux" ]; then
docker run --rm \
sudo: required --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
after_success: -v ${PWD}:/project \
- pip install virtualenv -v ~/.cache/electron:/root/.cache/electron \
- virtualenv ~/env -v ~/.cache/electron-builder:/root/.cache/electron-builder \
- source ~/env/bin/activate electronuserland/builder:wine \
- pip install transifex-client /bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --linux --win"
- sudo echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername= '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\ntoken = '"$TRANSIFEX_API_TOKEN"$'\n' > ~/.transifexrc else
- tx push -s yarn build
fi
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"
addons:
artifacts:
working_dir: dist
paths:
- $(git ls-files -o dist/{*.dmg,*.exe,*.AppImage} | tr "\n" ":")
target_paths:
- /commit-${TRAVIS_COMMIT:0:7}_build-${TRAVIS_BUILD_NUMBER}_tag-${TRAVIS_TAG}

View file

@ -8,7 +8,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* * Save app state when closing to tray ([#968](https://github.com/lbryio/lbry-app/issues/968))
* Added startup-troubleshooting FAQ URL to daemon error ([#1039](https://github.com/lbryio/lbry-app/pull/1039))
* Added ability to export wallet transactions to JSON and CSV format ([#976](https://github.com/lbryio/lbry-app/pull/976))
* Add Rewards FAQ to LBRY app ([#1041](https://github.com/lbryio/lbry-app/pull/1041))
* *
### Changed ### Changed
@ -16,8 +19,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
* *
### Fixed ### Fixed
* * Fixed sort by date of published content ([#986](https://github.com/lbryio/lbry-app/issues/986))
* * Fix night mode start time, set to 9PM (#1050)
* Fix night mode start time, set to 9PM ([#1050](https://github.com/lbryio/lbry-app/issues/1050))
* Disable drag and drop of files into the app ([#1045](https://github.com/lbryio/lbry-app/pull/1045))
### Deprecated ### Deprecated
* *

View file

@ -43,32 +43,6 @@ fi
yarn install yarn install
####################
# daemon and cli #
####################
echo -e "\033[0;32mGrabbing Daemon and CLI\x1b[m"
if $OSX; then
OSNAME="macos"
else
OSNAME="linux"
fi
DAEMON_VER=$(node -e "console.log(require(\"$ROOT/package.json\").lbrySettings.lbrynetDaemonVersion)")
DAEMON_URL_TEMPLATE=$(node -e "console.log(require(\"$ROOT/package.json\").lbrySettings.lbrynetDaemonUrlTemplate)")
DAEMON_URL=$(echo ${DAEMON_URL_TEMPLATE//DAEMONVER/$DAEMON_VER} | sed "s/OSNAME/$OSNAME/g")
DAEMON_VER_PATH="$BUILD_DIR/daemon.ver"
echo "$DAEMON_VER_PATH"
if [[ ! -f $DAEMON_VER_PATH || ! -f $ROOT/static/daemon/lbrynet-daemon || "$(< "$DAEMON_VER_PATH")" != "$DAEMON_VER" ]]; then
curl -sL -o "$BUILD_DIR/daemon.zip" "$DAEMON_URL"
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/static/daemon/"
rm "$BUILD_DIR/daemon.zip"
echo "$DAEMON_VER" > "$DAEMON_VER_PATH"
else
echo -e "\033[4;31mAlready have daemon version $DAEMON_VER, skipping download\x1b[m"
fi
################### ###################
# Build the app # # Build the app #
################### ###################
@ -79,23 +53,6 @@ if [ "$FULL_BUILD" == "true" ]; then
yarn build yarn build
# Workaround: TeamCity expects the dmg to be in dist/mac, but in the new electron-builder
# it's put directly in dist/ (the right way to solve this is to update the TeamCity config)
if $OSX; then
cp dist/*.dmg dist/mac
fi
# electron-build has a publish feature, but I had a hard time getting
# it to reliably work and it also seemed difficult to configure. Not proud of
# this, but it seemed better to write my own.
VENV="$BUILD_DIR/venv"
if [ -d "$VENV" ]; then
rm -rf "$VENV"
fi
virtualenv "$VENV"
"$VENV/bin/pip" install -r "$BUILD_DIR/requirements.txt"
"$VENV/bin/python" "$BUILD_DIR/upload_assets.py"
echo -e '\033[0;32mBuild and packaging complete.\x1b[m' echo -e '\033[0;32mBuild and packaging complete.\x1b[m'
else else
echo -e 'Build complete. Run \033[1;31myarn dev\x1b[m to launch the app' echo -e 'Build complete. Run \033[1;31myarn dev\x1b[m to launch the app'

47
build/downloadDaemon.js Normal file
View file

@ -0,0 +1,47 @@
/* eslint-disable no-console,import/no-commonjs */
const path = require('path');
const fs = require('fs');
const packageJSON = require('../package.json');
// eslint-disable-next-line import/no-extraneous-dependencies
const axios = require('axios');
// eslint-disable-next-line import/no-extraneous-dependencies
const decompress = require('decompress');
const os = require('os');
const daemonURLTemplate = packageJSON.lbrySettings.lbrynetDaemonUrlTemplate;
const daemonVersion = packageJSON.lbrySettings.lbrynetDaemonVersion;
let currentPlatform = os.platform();
if (currentPlatform === 'darwin') currentPlatform = 'macos';
if (currentPlatform === 'win32') currentPlatform = 'windows';
const daemonURL = daemonURLTemplate
.replace(/DAEMONVER/g, daemonVersion)
.replace(/OSNAME/g, currentPlatform);
const tmpZipPath = 'build/daemon.zip';
console.log('\x1b[34minfo\x1b[0m Downloading daemon...');
axios
.request({
responseType: 'arraybuffer',
url: daemonURL,
method: 'get',
headers: {
'Content-Type': 'application/zip',
},
})
.then(result => {
fs.writeFileSync(tmpZipPath, result.data);
return true;
})
.then(() => {
decompress(tmpZipPath, 'static/daemon', {
filter: file =>
path.basename(file.path).replace(path.extname(file.path), '') === 'lbrynet-daemon',
});
})
.then(() => {
console.log('\x1b[32msuccess\x1b[0m Daemon downloaded!');
})
.catch(error => {
console.error(`\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m`);
});

View file

@ -1,11 +1,13 @@
{ {
"appId": "io.lbry.LBRY", "appId": "io.lbry.LBRY",
"publish": { "productName": "LBRY",
"publish": [{
"provider": "s3", "provider": "s3",
"bucket": "releases.lbry.io", "bucket": "releases.lbry.io",
"path": "app/latest" "path": "app/latest"
}, }, "github"],
"mac": { "mac": {
"target": "dmg",
"category": "public.app-category.entertainment" "category": "public.app-category.entertainment"
}, },
"dmg": { "dmg": {
@ -28,36 +30,24 @@
"width": 500, "width": 500,
"height": 300 "height": 300
}, },
"background": "build/background.png" "background": "./build/background.png"
}, },
"protocols": [ "protocols": [
{ {
"name": "lbry", "name": "LBRY URI",
"schemes": ["lbry"], "schemes": ["lbry"],
"role": "Viewer" "role": "Viewer"
} }
], ],
"linux": { "linux": {
"target": "deb", "target": "AppImage",
"category": "Video;AudioVideo;", "category": "AudioVideo;Video",
"desktop": { "desktop": {
"MimeType": "x-scheme-handler/lbry", "MimeType": "x-scheme-handler/lbry;"
"Exec": "/opt/LBRY/lbry %U"
} }
}, },
"deb": {
"depends": [
"gconf2",
"gconf-service",
"libnotify4",
"libappindicator1",
"libxtst6",
"libnss3",
"libsecret-1-0"
]
},
"nsis": { "nsis": {
"perMachine": true "perMachine": true
}, },
"artifactName": "${productName}_${version}_${arch}.${ext}" "artifactName": "${productName}_${version}.${ext}"
} }

View file

@ -1,5 +1,5 @@
{ {
"name": "LBRY", "name": "lbry-app",
"version": "0.20.0", "version": "0.20.0",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"homepage": "https://lbry.io/", "homepage": "https://lbry.io/",
@ -21,7 +21,8 @@
"compile": "electron-webpack && yarn extract-langs", "compile": "electron-webpack && yarn extract-langs",
"build": "yarn compile && electron-builder build", "build": "yarn compile && electron-builder build",
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null", "build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
"postinstall": "electron-builder install-app-deps", "release": "yarn compile && electron-builder build",
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js",
"lint": "eslint 'src/**/*.{js,jsx}' --fix", "lint": "eslint 'src/**/*.{js,jsx}' --fix",
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write" "format": "prettier 'src/**/*.{js,jsx,scss,json}' --write"
}, },
@ -33,16 +34,17 @@
"classnames": "^2.2.5", "classnames": "^2.2.5",
"country-data": "^0.0.31", "country-data": "^0.0.31",
"electron-dl": "^1.6.0", "electron-dl": "^1.6.0",
"electron-is-dev": "^0.3.0",
"electron-log": "^2.2.12", "electron-log": "^2.2.12",
"electron-publisher-s3": "^19.47.0", "electron-publisher-s3": "^20.2.0",
"electron-updater": "^2.18.2", "electron-updater": "^2.21.0",
"find-process": "^1.1.0", "find-process": "^1.1.0",
"formik": "^0.10.4", "formik": "^0.10.4",
"from2": "^2.3.0", "from2": "^2.3.0",
"install": "^0.10.2", "install": "^0.10.2",
"jayson": "^2.0.2", "jayson": "^2.0.2",
"jshashes": "^1.0.7", "jshashes": "^1.0.7",
"keytar-prebuild": "^4.0.4", "keytar-prebuild": "^4.1.1",
"localforage": "^1.5.0", "localforage": "^1.5.0",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",
"moment": "^2.20.1", "moment": "^2.20.1",
@ -72,43 +74,41 @@
"y18n": "^4.0.0" "y18n": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^8.0.3", "axios": "^0.18.0",
"babel-plugin-module-resolver": "^3.0.0", "babel-eslint": "^8.2.2",
"babel-plugin-react-require": "^3.0.0", "babel-plugin-module-resolver": "^3.1.0",
"babel-polyfill": "^6.20.0", "babel-polyfill": "^6.20.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"decompress": "^4.2.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"electron": "^1.7.11", "electron": "^1.8.3",
"electron-builder": "^19.55.2", "electron-builder": "^20.3.1",
"electron-devtools-installer": "^2.2.1", "electron-devtools-installer": "^2.2.3",
"electron-webpack": "^1.11.0", "electron-webpack": "^1.13.0",
"eslint": "^4.13.1", "eslint": "^4.18.2",
"eslint-config-airbnb": "^16.1.0", "eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-import-resolver-webpack": "^0.8.3", "eslint-import-resolver-webpack": "^0.8.4",
"eslint-plugin-flowtype": "^2.40.1", "eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^2.4.0", "eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.5.1", "eslint-plugin-react": "^7.7.0",
"flow-babel-webpack-plugin": "^1.1.0", "flow-babel-webpack-plugin": "^1.1.1",
"flow-bin": "^0.61.0", "flow-bin": "^0.66.0",
"flow-typed": "^2.2.3", "flow-typed": "^2.3.0",
"husky": "^0.14.3", "husky": "^0.14.3",
"i18n-extract": "^0.5.1", "i18n-extract": "^0.5.1",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lint-staged": "^6.0.0", "lint-staged": "^7.0.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"prettier": "^1.4.2", "prettier": "^1.11.1",
"sass-loader": "^6.0.6", "sass-loader": "^6.0.7",
"webpack": "^3.10.0", "webpack": "^3.10.0",
"webpack-build-notifier": "^0.1.18" "webpack-build-notifier": "^0.1.23"
},
"resolutions": {
"webpack/webpack-sources": "1.0.1"
}, },
"engines": { "engines": {
"node": ">=6", "node": ">=6",

View file

@ -1,63 +0,0 @@
import { app, Menu, Tray as ElectronTray } from 'electron';
import path from 'path';
import createWindow from './createWindow';
export default class Tray {
window;
updateAttachedWindow;
tray;
constructor(window, updateAttachedWindow) {
this.window = window;
this.updateAttachedWindow = updateAttachedWindow;
}
create() {
let iconPath;
switch (process.platform) {
case 'darwin': {
iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png');
break;
}
case 'win32': {
iconPath = path.join(__static, '/img/tray/windows/tray.ico');
break;
}
default: {
iconPath = path.join(__static, '/img/tray/default/tray.png');
}
}
this.tray = new ElectronTray(iconPath);
this.tray.on('double-click', () => {
if (!this.window || this.window.isDestroyed()) {
this.window = createWindow();
this.updateAttachedWindow(this.window);
} else {
this.window.show();
this.window.focus();
}
});
this.tray.setToolTip('LBRY App');
const template = [
{
label: `Open ${app.getName()}`,
click: () => {
if (!this.window || this.window.isDestroyed()) {
this.window = createWindow();
this.updateAttachedWindow(this.window);
} else {
this.window.show();
this.window.focus();
}
},
},
{ role: 'quit' },
];
const contextMenu = Menu.buildFromTemplate(template);
this.tray.setContextMenu(contextMenu);
}
}

41
src/main/createTray.js Normal file
View file

@ -0,0 +1,41 @@
import { app, Menu, Tray } from 'electron';
import path from 'path';
export default window => {
let iconPath;
switch (process.platform) {
case 'darwin': {
iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png');
break;
}
case 'win32': {
iconPath = path.join(__static, '/img/tray/windows/tray.ico');
break;
}
default: {
iconPath = path.join(__static, '/img/tray/default/tray.png');
}
}
const tray = new Tray(iconPath);
tray.on('double-click', () => {
window.show();
});
tray.setToolTip('LBRY App');
const template = [
{
label: `Open ${app.getName()}`,
click: () => {
window.show();
},
},
{ role: 'quit' },
];
const contextMenu = Menu.buildFromTemplate(template);
tray.setContextMenu(contextMenu);
return tray;
};

View file

@ -1,8 +1,9 @@
import { app, BrowserWindow, dialog } from 'electron'; import { app, BrowserWindow, dialog } from 'electron';
import isDev from 'electron-is-dev';
import setupBarMenu from './menu/setupBarMenu'; import setupBarMenu from './menu/setupBarMenu';
import setupContextMenu from './menu/setupContextMenu'; import setupContextMenu from './menu/setupContextMenu';
export default deepLinkingURIArg => { export default appState => {
let windowConfiguration = { let windowConfiguration = {
backgroundColor: '#155B4A', backgroundColor: '#155B4A',
minWidth: 800, minWidth: 800,
@ -12,20 +13,18 @@ export default deepLinkingURIArg => {
}; };
// Disable renderer process's webSecurity on development to enable CORS. // Disable renderer process's webSecurity on development to enable CORS.
windowConfiguration = windowConfiguration = isDev
process.env.NODE_ENV === 'development' ? {
? { ...windowConfiguration,
...windowConfiguration, webPreferences: {
webPreferences: { webSecurity: false,
webSecurity: false, },
}, }
} : windowConfiguration;
: windowConfiguration;
const rendererURL = const rendererURL = isDev
process.env.NODE_ENV === 'development' ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : `file://${__dirname}/index.html`;
: `file://${__dirname}/index.html`;
let window = new BrowserWindow(windowConfiguration); let window = new BrowserWindow(windowConfiguration);
@ -35,11 +34,7 @@ export default deepLinkingURIArg => {
let deepLinkingURI; let deepLinkingURI;
// Protocol handler for win32 // Protocol handler for win32
if ( if (process.platform === 'win32' && String(process.argv[1]).startsWith('lbry')) {
!deepLinkingURIArg &&
process.platform === 'win32' &&
String(process.argv[1]).startsWith('lbry')
) {
// Keep only command line / deep linked arguments // Keep only command line / deep linked arguments
// Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to // Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to
// restore the original URI that was typed. // restore the original URI that was typed.
@ -48,15 +43,16 @@ export default deepLinkingURIArg => {
// - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as // - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as
// an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well. // an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well.
deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#'); deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#');
} else {
deepLinkingURI = deepLinkingURIArg;
} }
setupBarMenu(); setupBarMenu();
setupContextMenu(window); setupContextMenu(window);
window.on('closed', () => { window.on('close', event => {
window = null; if (!appState.isQuitting) {
event.preventDefault();
window.hide();
}
}); });
window.on('focus', () => { window.on('focus', () => {
@ -87,7 +83,7 @@ export default deepLinkingURIArg => {
window.webContents.on('did-finish-load', () => { window.webContents.on('did-finish-load', () => {
window.webContents.send('open-uri-requested', deepLinkingURI, true); window.webContents.send('open-uri-requested', deepLinkingURI, true);
window.webContents.session.setUserAgent(`LBRY/${app.getVersion()}`); window.webContents.session.setUserAgent(`LBRY/${app.getVersion()}`);
if (process.env.NODE_ENV === 'development') { if (isDev) {
window.webContents.openDevTools(); window.webContents.openDevTools();
} }
}); });

View file

@ -7,8 +7,9 @@ import url from 'url';
import https from 'https'; import https from 'https';
import { shell, app, ipcMain, dialog } from 'electron'; import { shell, app, ipcMain, dialog } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import isDev from 'electron-is-dev';
import Daemon from './Daemon'; import Daemon from './Daemon';
import Tray from './Tray'; import createTray from './createTray';
import createWindow from './createWindow'; import createWindow from './createWindow';
autoUpdater.autoDownload = true; autoUpdater.autoDownload = true;
@ -32,11 +33,7 @@ let rendererWindow;
let tray; let tray;
let daemon; let daemon;
let isQuitting; const appState = {};
const updateRendererWindow = window => {
rendererWindow = window;
};
const installExtensions = async () => { const installExtensions = async () => {
// eslint-disable-next-line import/no-extraneous-dependencies,global-require // eslint-disable-next-line import/no-extraneous-dependencies,global-require
@ -64,28 +61,27 @@ app.on('ready', async () => {
daemon = new Daemon(); daemon = new Daemon();
daemon.on('exit', () => { daemon.on('exit', () => {
daemon = null; daemon = null;
if (!isQuitting) { if (!appState.isQuitting) {
dialog.showErrorBox( dialog.showErrorBox(
'Daemon has Exited', 'Daemon has Exited',
'The daemon may have encountered an unexpected error, or another daemon instance is already running.' 'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
'For more information please visit: \n' +
'https://lbry.io/faq/startup-troubleshooting'
); );
app.quit(); app.quit();
} }
}); });
daemon.launch(); daemon.launch();
} }
if (process.env.NODE_ENV === 'development') { if (isDev) {
await installExtensions(); await installExtensions();
} }
rendererWindow = createWindow(); rendererWindow = createWindow(appState);
tray = new Tray(rendererWindow, updateRendererWindow); tray = createTray(rendererWindow);
tray.create();
}); });
app.on('activate', () => { app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the rendererWindow.show();
// dock icon is clicked and there are no other windows open.
if (!rendererWindow) rendererWindow = createWindow();
}); });
app.on('will-quit', event => { app.on('will-quit', event => {
@ -117,7 +113,7 @@ app.on('will-quit', event => {
return; return;
} }
isQuitting = true; appState.isQuitting = true;
if (daemon) daemon.quit(); if (daemon) daemon.quit();
}); });
@ -126,18 +122,13 @@ app.on('will-finish-launching', () => {
// Protocol handler for macOS // Protocol handler for macOS
app.on('open-url', (event, URL) => { app.on('open-url', (event, URL) => {
event.preventDefault(); event.preventDefault();
if (rendererWindow && !rendererWindow.isDestroyed()) { rendererWindow.webContents.send('open-uri-requested', URL);
rendererWindow.webContents.send('open-uri-requested', URL); rendererWindow.show();
rendererWindow.show();
rendererWindow.focus();
} else {
rendererWindow = createWindow(URL);
}
}); });
}); });
app.on('window-all-closed', () => { app.on('before-quit', () => {
// Subscribe to event so the app doesn't quit when closing the window. appState.isQuitting = true;
}); });
ipcMain.on('upgrade', (event, installerPath) => { ipcMain.on('upgrade', (event, installerPath) => {
@ -224,7 +215,7 @@ ipcMain.on('set-auth-token', (event, token) => {
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`); dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
isQuitting = true; appState.isQuitting = true;
if (daemon) daemon.quit(); if (daemon) daemon.quit();
app.exit(1); app.exit(1);
}); });
@ -240,14 +231,8 @@ const isSecondInstance = app.makeSingleInstance(argv => {
URI = argv[1].replace(/\/$/, '').replace('/#', '#'); URI = argv[1].replace(/\/$/, '').replace('/#', '#');
} }
if (rendererWindow && !rendererWindow.isDestroyed()) { rendererWindow.webContents.send('open-uri-requested', URI);
rendererWindow.webContents.send('open-uri-requested', URI); rendererWindow.show();
rendererWindow.show();
rendererWindow.focus();
} else {
rendererWindow = createWindow(URI);
}
}); });
if (isSecondInstance) { if (isSecondInstance) {

View file

@ -1,5 +1,6 @@
// @flow // @flow
import { Menu, BrowserWindow } from 'electron'; import { Menu, BrowserWindow } from 'electron';
import isDev from 'electron-is-dev';
export default (rendererWindow: BrowserWindow) => { export default (rendererWindow: BrowserWindow) => {
rendererWindow.webContents.on('context-menu', (e, params) => { rendererWindow.webContents.on('context-menu', (e, params) => {
@ -17,7 +18,7 @@ export default (rendererWindow: BrowserWindow) => {
}, },
]; ];
if (process.env.NODE_ENV === 'development') { if (isDev) {
template.push(...developmentTemplateAddition); template.push(...developmentTemplateAddition);
} }

View file

@ -1,20 +1,27 @@
// @flow // @flow
import mixpanel from 'mixpanel-browser'; import mixpanel from 'mixpanel-browser';
import Lbryio from 'lbryio';
import isDev from 'electron-is-dev';
mixpanel.init('691723e855cabb9d27a7a79002216967'); if (isDev) {
mixpanel.init('691723e855cabb9d27a7a79002216967');
} else {
mixpanel.init('af5c6b8110068fa4f5c4600c81f05e60');
}
type Analytics = { type Analytics = {
track: (string, ?Object) => void, track: (string, ?Object) => void,
setUser: (Object) => void, setUser: Object => void,
toggle: (boolean, ?boolean) => void toggle: (boolean, ?boolean) => void,
} apiLog: (string, string, string) => void,
};
let analyticsEnabled: boolean = false; let analyticsEnabled: boolean = false;
const analytics: Analytics = { const analytics: Analytics = {
track: (name: string, payload: ?Object): void => { track: (name: string, payload: ?Object): void => {
if(analyticsEnabled) { if (analyticsEnabled) {
if(payload) { if (payload) {
mixpanel.track(name, payload); mixpanel.track(name, payload);
} else { } else {
mixpanel.track(name); mixpanel.track(name);
@ -22,21 +29,30 @@ const analytics: Analytics = {
} }
}, },
setUser: (user: Object): void => { setUser: (user: Object): void => {
if(user.id) { if (user.id) {
mixpanel.identify(user.id); mixpanel.identify(user.id);
} }
if(user.primary_email) { if (user.primary_email) {
mixpanel.people.set({ mixpanel.people.set({
"$email": user.primary_email $email: user.primary_email,
}); });
} }
}, },
toggle: (enabled: boolean, logDisabled: ?boolean): void => { toggle: (enabled: boolean, logDisabled: ?boolean): void => {
if(!enabled && logDisabled) { if (!enabled && logDisabled) {
mixpanel.track('DISABLED'); mixpanel.track('DISABLED');
} }
analyticsEnabled = enabled; analyticsEnabled = enabled;
} },
} apiLog: (uri: string, outpoint: string, claimId: string): void => {
if (analyticsEnabled) {
Lbryio.call('file', 'view', {
uri,
outpoint,
claim_id: claimId,
}).catch(() => {});
}
},
};
export default analytics; export default analytics;

View file

@ -2,6 +2,7 @@ import store from 'store';
import { remote } from 'electron'; import { remote } from 'electron';
import Path from 'path'; import Path from 'path';
import y18n from 'y18n'; import y18n from 'y18n';
import isDev from 'electron-is-dev';
const env = process.env.NODE_ENV || 'production'; const env = process.env.NODE_ENV || 'production';
const i18n = y18n({ const i18n = y18n({
@ -22,7 +23,7 @@ const app = {
}; };
// Workaround for https://github.com/electron-userland/electron-webpack/issues/52 // Workaround for https://github.com/electron-userland/electron-webpack/issues/52
if (env !== 'development') { if (!isDev) {
window.staticResourcesPath = Path.join(remote.app.getAppPath(), '../static').replace( window.staticResourcesPath = Path.join(remote.app.getAppPath(), '../static').replace(
/\\/g, /\\/g,
'\\\\' '\\\\'

View file

@ -0,0 +1,67 @@
import fs from 'fs';
import path from 'path';
import React from 'react';
import PropTypes from 'prop-types';
import Link from 'component/link';
import parseData from 'util/parseData';
import * as icons from 'constants/icons';
const { remote } = require('electron');
class FileExporter extends React.PureComponent {
static propTypes = {
data: PropTypes.array,
title: PropTypes.string,
label: PropTypes.string,
defaultPath: PropTypes.string,
onFileCreated: PropTypes.func,
};
constructor(props) {
super(props);
}
handleFileCreation(filename, data) {
const { onFileCreated } = this.props;
fs.writeFile(filename, data, err => {
if (err) throw err;
// Do something after creation
onFileCreated && onFileCreated(filename);
});
}
handleButtonClick() {
const { title, defaultPath, data } = this.props;
const options = {
title,
defaultPath,
filters: [{ name: 'JSON', extensions: ['json'] }, { name: 'CSV', extensions: ['csv'] }],
};
remote.dialog.showSaveDialog(options, filename => {
// User hit cancel so do nothing:
if (!filename) return;
// Get extension and remove initial dot
const format = path.extname(filename).replace(/\./g, '');
// Parse data to string with the chosen format
const parsed = parseData(data, format);
// Write file
parsed && this.handleFileCreation(filename, parsed);
});
}
render() {
const { title, label } = this.props;
return (
<Link
button="primary"
icon={icons.DOWNLOAD}
title={title || __('Export')}
label={label || __('Export')}
onClick={() => this.handleButtonClick()}
/>
);
}
}
export default FileExporter;

View file

@ -9,12 +9,33 @@ class FileList extends React.PureComponent {
super(props); super(props);
this.state = { this.state = {
sortBy: 'date', sortBy: 'dateNew',
}; };
this._sortFunctions = { this._sortFunctions = {
date(fileInfos) { dateNew(fileInfos) {
return fileInfos.slice().reverse(); return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = fileInfo1.height;
const height2 = fileInfo2.height;
if (height1 > height2) {
return -1;
} else if (height1 < height2) {
return 1;
}
return 0;
});
},
dateOld(fileInfos) {
return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = fileInfo1.height;
const height2 = fileInfo2.height;
if (height1 < height2) {
return -1;
} else if (height1 > height2) {
return 1;
}
return 0;
});
}, },
title(fileInfos) { title(fileInfos) {
return fileInfos.slice().sort((fileInfo1, fileInfo2) => { return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
@ -95,7 +116,8 @@ class FileList extends React.PureComponent {
<span className="sort-section"> <span className="sort-section">
{__('Sort by')}{' '} {__('Sort by')}{' '}
<FormField type="select" onChange={this.handleSortChanged.bind(this)}> <FormField type="select" onChange={this.handleSortChanged.bind(this)}>
<option value="date">{__('Date')}</option> <option value="dateNew">{__('Newest First')}</option>
<option value="dateOld">{__('Oldest First')}</option>
<option value="title">{__('Title')}</option> <option value="title">{__('Title')}</option>
</FormField> </FormField>
</span> </span>

View file

@ -14,6 +14,10 @@ const RewardSummary = (props: Props) => {
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h3>{__('Rewards')}</h3> <h3>{__('Rewards')}</h3>
<p className="help">
{__('Read our')} <Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '}
{__('to learn more about LBRY Rewards')}.
</p>
</div> </div>
<div className="card__content"> <div className="card__content">
{unclaimedRewardAmount > 0 ? ( {unclaimedRewardAmount > 0 ? (

View file

@ -2,6 +2,7 @@ import React from 'react';
import TransactionListItem from './internal/TransactionListItem'; import TransactionListItem from './internal/TransactionListItem';
import FormField from 'component/formField'; import FormField from 'component/formField';
import Link from 'component/link'; import Link from 'component/link';
import FileExporter from 'component/file-exporter.js';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import * as modals from 'constants/modal_types'; import * as modals from 'constants/modal_types';
@ -43,6 +44,13 @@ class TransactionList extends React.PureComponent {
return ( return (
<div> <div>
{Boolean(transactionList.length) && (
<FileExporter
data={transactionList}
title={__('Export Transactions')}
label={__('Export')}
/>
)}
{(transactionList.length || this.state.filter) && ( {(transactionList.length || this.state.filter) && (
<span className="sort-section"> <span className="sort-section">
{__('Filter')}{' '} {__('Filter')}{' '}

View file

@ -1,5 +1,4 @@
const { remote } = require('electron'); import { remote } from 'electron';
import React from 'react'; import React from 'react';
import { Thumbnail } from 'component/common'; import { Thumbnail } from 'component/common';
import player from 'render-media'; import player from 'render-media';
@ -21,33 +20,21 @@ class VideoPlayer extends React.PureComponent {
this.togglePlayListener = this.togglePlay.bind(this); this.togglePlayListener = this.togglePlay.bind(this);
} }
componentWillReceiveProps(next) {
const el = this.refs.media.children[0];
if (!this.props.paused && next.paused && !el.paused) el.pause();
}
componentDidMount() { componentDidMount() {
const container = this.refs.media; const container = this.media;
const { const { contentType, changeVolume, volume, position, claim } = this.props;
contentType,
downloadPath,
mediaType,
changeVolume,
volume,
position,
claim,
uri,
} = this.props;
const loadedMetadata = e => { const loadedMetadata = () => {
this.setState({ hasMetadata: true, startedPlaying: true }); this.setState({ hasMetadata: true, startedPlaying: true });
this.refs.media.children[0].play(); this.media.children[0].play();
}; };
const renderMediaCallback = err => {
if (err) this.setState({ unplayable: true }); const renderMediaCallback = error => {
if (error) this.setState({ unplayable: true });
}; };
// Handle fullscreen change for the Windows platform // Handle fullscreen change for the Windows platform
const win32FullScreenChange = e => { const win32FullScreenChange = () => {
const win = remote.BrowserWindow.getFocusedWindow(); const win = remote.BrowserWindow.getFocusedWindow();
if (process.platform === 'win32') { if (process.platform === 'win32') {
win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu()); win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu());
@ -61,13 +48,13 @@ class VideoPlayer extends React.PureComponent {
player.append( player.append(
this.file(), this.file(),
container, container,
{ autoplay: false, controls: true }, { autoplay: true, controls: true },
renderMediaCallback.bind(this) renderMediaCallback.bind(this)
); );
} }
document.addEventListener('keydown', this.togglePlayListener); document.addEventListener('keydown', this.togglePlayListener);
const mediaElement = this.refs.media.children[0]; const mediaElement = this.media.children[0];
if (mediaElement) { if (mediaElement) {
mediaElement.currentTime = position || 0; mediaElement.currentTime = position || 0;
mediaElement.addEventListener('play', () => this.props.doPlay()); mediaElement.addEventListener('play', () => this.props.doPlay());
@ -87,29 +74,38 @@ class VideoPlayer extends React.PureComponent {
} }
} }
componentWillReceiveProps(next) {
const el = this.media.children[0];
if (!this.props.paused && next.paused && !el.paused) el.pause();
}
componentDidUpdate() {
const { contentType, downloadCompleted } = this.props;
const { startedPlaying } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.media.children[0];
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(this.media, true);
} else {
player.render(this.file(), container, {
autoplay: true,
controls: true,
});
}
}
}
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('keydown', this.togglePlayListener); document.removeEventListener('keydown', this.togglePlayListener);
const mediaElement = this.refs.media.children[0]; const mediaElement = this.media.children[0];
if (mediaElement) { if (mediaElement) {
mediaElement.removeEventListener('click', this.togglePlayListener); mediaElement.removeEventListener('click', this.togglePlayListener);
} }
this.props.doPause(); this.props.doPause();
} }
renderAudio(container, autoplay) {
if (container.firstChild) {
container.firstChild.remove();
}
// clear the container
const { downloadPath } = this.props;
const audio = document.createElement('audio');
audio.autoplay = autoplay;
audio.controls = true;
audio.src = downloadPath;
container.appendChild(audio);
}
togglePlay(event) { togglePlay(event) {
// ignore all events except click and spacebar keydown, or input events in a form control // ignore all events except click and spacebar keydown, or input events in a form control
if ( if (
@ -119,7 +115,7 @@ class VideoPlayer extends React.PureComponent {
return; return;
} }
event.preventDefault(); event.preventDefault();
const mediaElement = this.refs.media.children[0]; const mediaElement = this.media.children[0];
if (mediaElement) { if (mediaElement) {
if (!mediaElement.paused) { if (!mediaElement.paused) {
mediaElement.pause(); mediaElement.pause();
@ -129,24 +125,6 @@ class VideoPlayer extends React.PureComponent {
} }
} }
componentDidUpdate() {
const { contentType, downloadCompleted } = this.props;
const { startedPlaying } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.refs.media.children[0];
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(this.refs.media, true);
} else {
player.render(this.file(), container, {
autoplay: true,
controls: true,
});
}
}
}
file() { file() {
const { downloadPath, filename } = this.props; const { downloadPath, filename } = this.props;
@ -162,14 +140,26 @@ class VideoPlayer extends React.PureComponent {
return ['audio', 'video'].indexOf(mediaType) !== -1; return ['audio', 'video'].indexOf(mediaType) !== -1;
} }
renderAudio(container, autoplay) {
if (container.firstChild) {
container.firstChild.remove();
}
// clear the container
const { downloadPath } = this.props;
const audio = document.createElement('audio');
audio.autoplay = autoplay;
audio.controls = true;
audio.src = downloadPath;
container.appendChild(audio);
}
render() { render() {
const { mediaType, poster } = this.props; const { mediaType, poster } = this.props;
const { hasMetadata, unplayable } = this.state; const { hasMetadata, unplayable } = this.state;
const noMetadataMessage = 'Waiting for metadata.'; const noMetadataMessage = 'Waiting for metadata.';
const unplayableMessage = "Sorry, looks like we can't play this file."; const unplayableMessage = "Sorry, looks like we can't play this file.";
const needsMetadata = this.playableType();
return ( return (
<div> <div>
{['audio', 'application'].indexOf(mediaType) !== -1 && {['audio', 'application'].indexOf(mediaType) !== -1 &&
@ -179,7 +169,12 @@ class VideoPlayer extends React.PureComponent {
!hasMetadata && !hasMetadata &&
!unplayable && <LoadingScreen status={noMetadataMessage} />} !unplayable && <LoadingScreen status={noMetadataMessage} />}
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />} {unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
<div ref="media" className="media" /> <div
ref={container => {
this.media = container;
}}
className="media"
/>
</div> </div>
); );
} }

View file

@ -164,6 +164,10 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE'; export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS'; export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
// Video controls // Video controls
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';

View file

@ -3,3 +3,4 @@ export const LOCAL = 'folder';
export const FILE = 'file'; export const FILE = 'file';
export const HISTORY = 'history'; export const HISTORY = 'history';
export const HELP_CIRCLE = 'question-circle'; export const HELP_CIRCLE = 'question-circle';
export const DOWNLOAD = 'download';

View file

@ -2,7 +2,7 @@ export const CONFIRM_FILE_REMOVE = 'confirmFileRemove';
export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon'; export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon';
export const FILE_TIMEOUT = 'file_timeout'; export const FILE_TIMEOUT = 'file_timeout';
export const DOWNLOADING = 'downloading'; export const DOWNLOADING = 'downloading';
export const AUTO_UPDATE_DOWNLOADED = "auto_update_downloaded"; export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded';
export const AUTO_UPDATE_CONFIRM = 'auto_update_confirm'; export const AUTO_UPDATE_CONFIRM = 'auto_update_confirm';
export const ERROR = 'error'; export const ERROR = 'error';
export const INSUFFICIENT_CREDITS = 'insufficient_credits'; export const INSUFFICIENT_CREDITS = 'insufficient_credits';

View file

@ -8,10 +8,14 @@ import lbry from 'lbry';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doShowSnackBar, doAutoUpdate } from 'redux/actions/app'; import {
import { doUpdateIsNightAsync } from 'redux/actions/settings'; doConditionalAuthNavigate,
doDaemonReady,
doShowSnackBar,
doAutoUpdate,
} from 'redux/actions/app';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { doDownloadLanguages } from 'redux/actions/settings'; import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
import { doUserEmailVerify } from 'redux/actions/user'; import { doUserEmailVerify } from 'redux/actions/user';
import 'scss/all.scss'; import 'scss/all.scss';
import store from 'store'; import store from 'store';
@ -20,12 +24,7 @@ import analytics from './analytics';
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
autoUpdater.logger = remote.require("electron-log"); autoUpdater.logger = remote.require('electron-log');
window.addEventListener('contextmenu', event => {
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
event.preventDefault();
});
ipcRenderer.on('open-uri-requested', (event, uri, newSession) => { ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
if (uri && uri.startsWith('lbry://')) { if (uri && uri.startsWith('lbry://')) {
@ -62,6 +61,12 @@ ipcRenderer.on('window-is-focused', () => {
dock.setBadge(''); dock.setBadge('');
}); });
document.addEventListener('dragover', event => {
event.preventDefault();
});
document.addEventListener('drop', event => {
event.preventDefault();
});
document.addEventListener('click', event => { document.addEventListener('click', event => {
let { target } = event; let { target } = event;
while (target && target !== document) { while (target && target !== document) {
@ -91,22 +96,21 @@ document.addEventListener('click', event => {
}); });
const init = () => { const init = () => {
autoUpdater.on("update-downloaded", () => { autoUpdater.on('update-downloaded', () => {
app.store.dispatch(doAutoUpdate());
});
autoUpdater.on('update-available', () => {
console.log('Update available');
});
autoUpdater.on('update-not-available', () => {
console.log('Update not available');
});
autoUpdater.on('update-downloaded', () => {
console.log('Update downloaded');
app.store.dispatch(doAutoUpdate()); app.store.dispatch(doAutoUpdate());
}); });
if (["win32", "darwin"].includes(process.platform)) {
autoUpdater.on("update-available", () => {
console.log("Update available");
});
autoUpdater.on("update-not-available", () => {
console.log("Update not available");
});
autoUpdater.on("update-downloaded", () => {
console.log("Update downloaded");
app.store.dispatch(doAutoUpdate());
});
}
app.store.dispatch(doUpdateIsNightAsync()); app.store.dispatch(doUpdateIsNightAsync());
app.store.dispatch(doDownloadLanguages()); app.store.dispatch(doDownloadLanguages());

View file

@ -15,7 +15,7 @@ jsonrpc.call = (
return response.json().then(json => { return response.json().then(json => {
let error; let error;
if (json.error) { if (json.error) {
error = new Error(json.error); error = new Error(json.error.message);
} else { } else {
error = new Error('Protocol error with unknown response signature'); error = new Error('Protocol error with unknown response signature');
} }

View file

@ -1,7 +1,7 @@
import React from "react"; import React from 'react';
import { connect } from "react-redux"; import { connect } from 'react-redux';
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app"; import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
import ModalAutoUpdateConfirm from "./view"; import ModalAutoUpdateConfirm from './view';
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()), closeModal: () => dispatch(doCloseModal()),

View file

@ -1,9 +1,9 @@
import React from "react"; import React from 'react';
import { Modal } from "modal/modal"; import { Modal } from 'modal/modal';
import { Line } from "rc-progress"; import { Line } from 'rc-progress';
import Link from "component/link/index"; import Link from 'component/link/index';
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
class ModalAutoUpdateConfirm extends React.PureComponent { class ModalAutoUpdateConfirm extends React.PureComponent {
render() { render() {
@ -13,11 +13,11 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
<Modal <Modal
isOpen={true} isOpen={true}
type="confirm" type="confirm"
contentLabel={__("Update Downloaded")} contentLabel={__('Update Downloaded')}
confirmButtonLabel={__("Upgrade")} confirmButtonLabel={__('Upgrade')}
abortButtonLabel={__("Not now")} abortButtonLabel={__('Not now')}
onConfirmed={() => { onConfirmed={() => {
ipcRenderer.send("autoUpdateAccepted"); ipcRenderer.send('autoUpdateAccepted');
}} }}
onAborted={() => { onAborted={() => {
declineAutoUpdate(); declineAutoUpdate();
@ -25,12 +25,8 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
}} }}
> >
<section> <section>
<h3 className="text-center">{__("LBRY Update Ready")}</h3> <h3 className="text-center">{__('LBRY Update Ready')}</h3>
<p> <p>{__('Your LBRY update is ready. Restart LBRY now to use it!')}</p>
{__(
'Your LBRY update is ready. Restart LBRY now to use it!'
)}
</p>
<p className="meta text-center"> <p className="meta text-center">
{__('Want to know what has changed?')} See the{' '} {__('Want to know what has changed?')} See the{' '}
<Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />. <Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />.

View file

@ -1,7 +1,7 @@
import React from "react"; import React from 'react';
import { connect } from "react-redux"; import { connect } from 'react-redux';
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app"; import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
import ModalAutoUpdateDownloaded from "./view"; import ModalAutoUpdateDownloaded from './view';
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()), closeModal: () => dispatch(doCloseModal()),

View file

@ -1,9 +1,9 @@
import React from "react"; import React from 'react';
import { Modal } from "modal/modal"; import { Modal } from 'modal/modal';
import { Line } from "rc-progress"; import { Line } from 'rc-progress';
import Link from "component/link/index"; import Link from 'component/link/index';
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
class ModalAutoUpdateDownloaded extends React.PureComponent { class ModalAutoUpdateDownloaded extends React.PureComponent {
render() { render() {
@ -13,20 +13,20 @@ class ModalAutoUpdateDownloaded extends React.PureComponent {
<Modal <Modal
isOpen={true} isOpen={true}
type="confirm" type="confirm"
contentLabel={__("Update Downloaded")} contentLabel={__('Update Downloaded')}
confirmButtonLabel={__("Use it Now")} confirmButtonLabel={__('Use it Now')}
abortButtonLabel={__("Upgrade on Close")} abortButtonLabel={__('Upgrade on Close')}
onConfirmed={() => { onConfirmed={() => {
ipcRenderer.send("autoUpdateAccepted"); ipcRenderer.send('autoUpdateAccepted');
}} }}
onAborted={() => { onAborted={() => {
declineAutoUpdate(); declineAutoUpdate();
ipcRenderer.send("autoUpdateDeclined"); ipcRenderer.send('autoUpdateDeclined');
closeModal(); closeModal();
}} }}
> >
<section> <section>
<h3 className="text-center">{__("LBRY Leveled Up")}</h3> <h3 className="text-center">{__('LBRY Leveled Up')}</h3>
<p> <p>
{__( {__(
'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.' 'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.'

View file

@ -30,7 +30,12 @@ class ModalEmailCollection extends React.PureComponent {
<Modal type="custom" isOpen contentLabel="Email"> <Modal type="custom" isOpen contentLabel="Email">
<section> <section>
<h3 className="modal__header">Can We Stay In Touch?</h3> <h3 className="modal__header">Can We Stay In Touch?</h3>
{this.renderInner()} <div className="card__content">{this.renderInner()}</div>
<div className="card__content">
<div className="help">
{`${__('Your email may be used to sync usage data across devices.')} `}
</div>
</div>
</section> </section>
</Modal> </Modal>
); );

View file

@ -68,7 +68,7 @@ export class AuthPage extends React.PureComponent {
<div className="card__content"> <div className="card__content">
<div className="help"> <div className="help">
{`${__( {`${__(
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards.' 'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.'
)} `} )} `}
<Link onClick={() => navigate('/discover')} label={__('Return home')} />. <Link onClick={() => navigate('/discover')} label={__('Return home')} />.
</div> </div>

View file

@ -4,6 +4,7 @@ import { doFetchFileInfo } from 'redux/actions/file_info';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import { selectRewardContentClaimIds } from 'redux/selectors/content'; import { selectRewardContentClaimIds } from 'redux/selectors/content';
import { doFetchCostInfoForUri } from 'redux/actions/cost_info'; import { doFetchCostInfoForUri } from 'redux/actions/cost_info';
import { checkSubscriptionLatest } from 'redux/actions/subscriptions';
import { import {
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
@ -13,6 +14,7 @@ import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import FilePage from './view'; import FilePage from './view';
import { makeSelectCurrentParam } from 'redux/selectors/navigation'; import { makeSelectCurrentParam } from 'redux/selectors/navigation';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
@ -23,12 +25,15 @@ const select = (state, props) => ({
tab: makeSelectCurrentParam('tab')(state), tab: makeSelectCurrentParam('tab')(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
subscriptions: selectSubscriptions(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
checkSubscriptionLatest: (subscription, uri) =>
dispatch(checkSubscriptionLatest(subscription, uri)),
}); });
export default connect(select, perform)(FilePage); export default connect(select, perform)(FilePage);

View file

@ -17,6 +17,7 @@ class FilePage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props); this.fetchCostInfo(this.props);
this.checkSubscriptionLatest(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -35,6 +36,28 @@ class FilePage extends React.PureComponent {
} }
} }
checkSubscriptionLatest(props) {
if (
props.subscriptions
.map(subscription => subscription.channelName)
.indexOf(props.claim.channel_name) !== -1
) {
props.checkSubscriptionLatest(
{
channelName: props.claim.channel_name,
uri: buildURI(
{
contentName: props.claim.channel_name,
claimId: props.claim.value.publisherSignature.certificateId,
},
false
),
},
buildURI({ contentName: props.claim.name, claimId: props.claim.claim_id }, false)
);
}
}
render() { render() {
const { const {
claim, claim,

View file

@ -14,6 +14,8 @@ class SettingsPage extends React.PureComponent {
this.state = { this.state = {
clearingCache: false, clearingCache: false,
}; };
this.onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this);
} }
clearCache() { clearCache() {
@ -62,11 +64,16 @@ class SettingsPage extends React.PureComponent {
onThemeChange(event) { onThemeChange(event) {
const { value } = event.target; const { value } = event.target;
if (value === 'dark') {
this.onAutomaticDarkModeChange(false);
}
this.props.setClientSetting(settings.THEME, value); this.props.setClientSetting(settings.THEME, value);
} }
onAutomaticDarkModeChange(event) { onAutomaticDarkModeChange(value) {
this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, event.target.checked); this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value);
} }
onInstantPurchaseEnabledChange(enabled) { onInstantPurchaseEnabledChange(enabled) {
@ -143,6 +150,7 @@ class SettingsPage extends React.PureComponent {
</main> </main>
); );
} }
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
@ -300,7 +308,9 @@ class SettingsPage extends React.PureComponent {
type="checkbox" type="checkbox"
onChange={this.onShareDataChange.bind(this)} onChange={this.onShareDataChange.bind(this)}
defaultChecked={daemonSettings.share_usage_data} defaultChecked={daemonSettings.share_usage_data}
label={__('Help make LBRY better by contributing diagnostic data about my usage')} label={__(
'Help make LBRY better by contributing analytics and diagnostic data and about my usage'
)}
/> />
</div> </div>
</section> </section>
@ -325,8 +335,9 @@ class SettingsPage extends React.PureComponent {
<FormRow <FormRow
type="checkbox" type="checkbox"
onChange={this.onAutomaticDarkModeChange.bind(this)} disabled={theme === 'dark'}
defaultChecked={automaticDarkModeEnabled} onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
checked={automaticDarkModeEnabled}
label={__('Automatic dark mode (9pm to 8am)')} label={__('Automatic dark mode (9pm to 8am)')}
/> />
</div> </div>

View file

@ -1,32 +1,32 @@
/* eslint-disable import/no-commonjs */ import isDev from 'electron-is-dev';
import Lbry from 'lbry';
import path from 'path';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import { ipcRenderer, remote } from 'electron'; import { ipcRenderer, remote } from 'electron';
import Lbry from 'lbry';
import Path from 'path';
import { doFetchRewardedContent } from 'redux/actions/content'; import { doFetchRewardedContent } from 'redux/actions/content';
import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info'; import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info';
import { doAuthNavigate } from 'redux/actions/navigation'; import { doAuthNavigate } from 'redux/actions/navigation';
import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthenticate } from 'redux/actions/user'; import { doAuthenticate } from 'redux/actions/user';
import { doBalanceSubscribe } from 'redux/actions/wallet'; import { doBalanceSubscribe } from 'redux/actions/wallet';
import { doPause } from "redux/actions/media"; import { doPause } from 'redux/actions/media';
import { doCheckSubscriptions } from 'redux/actions/subscriptions';
import { import {
selectCurrentModal, selectCurrentModal,
selectIsUpgradeSkipped, selectIsUpgradeSkipped,
selectRemoteVersion,
selectUpdateUrl, selectUpdateUrl,
selectUpgradeDownloadItem, selectUpgradeDownloadItem,
selectUpgradeDownloadPath, selectUpgradeDownloadPath,
selectUpgradeFilename, selectUpgradeFilename,
selectAutoUpdateDeclined, selectAutoUpdateDeclined,
} from 'redux/selectors/app'; } from 'redux/selectors/app';
import { lbrySettings as config } from 'package.json';
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
const { download } = remote.require('electron-dl'); const { download } = remote.require('electron-dl');
const Fs = remote.require('fs'); const Fs = remote.require('fs');
const { lbrySettings: config } = require('package.json');
const CHECK_UPGRADE_INTERVAL = 10 * 60 * 1000; const CHECK_UPGRADE_INTERVAL = 10 * 60 * 1000;
@ -84,23 +84,19 @@ export function doDownloadUpgradeRequested() {
const autoUpdateDeclined = selectAutoUpdateDeclined(state); const autoUpdateDeclined = selectAutoUpdateDeclined(state);
if (['win32', 'darwin'].includes(process.platform)) { // electron-updater behavior if (autoUpdateDeclined) {
if (autoUpdateDeclined) { // The user declined an update before, so show the "confirm" dialog
// The user declined an update before, so show the "confirm" dialog dispatch({
dispatch({ type: ACTIONS.OPEN_MODAL,
type: ACTIONS.OPEN_MODAL, data: { modal: MODALS.AUTO_UPDATE_CONFIRM },
data: { modal: MODALS.AUTO_UPDATE_CONFIRM }, });
}); } else {
} else { // The user was never shown the original update dialog (e.g. because they were
// The user was never shown the original update dialog (e.g. because they were // watching a video). So show the initial "update downloaded" dialog.
// watching a video). So show the inital "update downloaded" dialog. dispatch({
dispatch({ type: ACTIONS.OPEN_MODAL,
type: ACTIONS.OPEN_MODAL, data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED }, });
});
}
} else { // Old behavior for Linux
dispatch(doDownloadUpgrade());
} }
}; };
} }
@ -109,7 +105,7 @@ export function doDownloadUpgrade() {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
const dir = Fs.mkdtempSync(remote.app.getPath('temp') + Path.sep); const dir = Fs.mkdtempSync(remote.app.getPath('temp') + path.sep);
const upgradeFilename = selectUpgradeFilename(state); const upgradeFilename = selectUpgradeFilename(state);
const options = { const options = {
@ -127,7 +123,7 @@ export function doDownloadUpgrade() {
type: ACTIONS.UPGRADE_DOWNLOAD_COMPLETED, type: ACTIONS.UPGRADE_DOWNLOAD_COMPLETED,
data: { data: {
downloadItem, downloadItem,
path: Path.join(dir, upgradeFilename), path: path.join(dir, upgradeFilename),
}, },
}); });
}); });
@ -145,8 +141,7 @@ export function doDownloadUpgrade() {
} }
export function doAutoUpdate() { export function doAutoUpdate() {
return function(dispatch, getState) { return dispatch => {
const state = getState();
dispatch({ dispatch({
type: ACTIONS.AUTO_UPDATE_DOWNLOADED, type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
}); });
@ -159,12 +154,11 @@ export function doAutoUpdate() {
} }
export function doAutoUpdateDeclined() { export function doAutoUpdateDeclined() {
return function(dispatch, getState) { return dispatch => {
const state = getState();
dispatch({ dispatch({
type: ACTIONS.AUTO_UPDATE_DECLINED, type: ACTIONS.AUTO_UPDATE_DECLINED,
}); });
} };
} }
export function doCancelUpgrade() { export function doCancelUpgrade() {
@ -181,6 +175,7 @@ export function doCancelUpgrade() {
try { try {
upgradeDownloadItem.cancel(); upgradeDownloadItem.cancel();
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error(err); console.error(err);
// Do nothing // Do nothing
} }
@ -197,47 +192,11 @@ export function doCheckUpgradeAvailable() {
type: ACTIONS.CHECK_UPGRADE_START, type: ACTIONS.CHECK_UPGRADE_START,
}); });
if (["win32", "darwin"].includes(process.platform)) { const autoUpdateDeclined = selectAutoUpdateDeclined(state);
// On Windows and Mac, updates happen silently through
// electron-updater.
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
if (!autoUpdateDeclined) { if (!autoUpdateDeclined && !isDev) {
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
}
return;
} }
const success = ({ remoteVersion, upgradeAvailable }) => {
dispatch({
type: ACTIONS.CHECK_UPGRADE_SUCCESS,
data: {
upgradeAvailable,
remoteVersion,
},
});
if (
upgradeAvailable &&
!selectCurrentModal(state) &&
(!selectIsUpgradeSkipped(state) || remoteVersion !== selectRemoteVersion(state))
) {
dispatch({
type: ACTIONS.OPEN_MODAL,
data: {
modal: MODALS.UPGRADE,
},
});
}
};
const fail = () => {
dispatch({
type: ACTIONS.CHECK_UPGRADE_FAIL,
});
};
Lbry.getAppVersionInfo().then(success, fail);
}; };
} }
@ -296,6 +255,7 @@ export function doDaemonReady() {
dispatch(doCheckUpgradeAvailable()); dispatch(doCheckUpgradeAvailable());
} }
dispatch(doCheckUpgradeSubscribe()); dispatch(doCheckUpgradeSubscribe());
dispatch(doCheckSubscriptions());
}; };
} }

View file

@ -7,6 +7,7 @@ import Lbryio from 'lbryio';
import { normalizeURI, buildURI } from 'lbryURI'; import { normalizeURI, buildURI } from 'lbryURI';
import { doAlertError, doOpenModal } from 'redux/actions/app'; import { doAlertError, doOpenModal } from 'redux/actions/app';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
import { setSubscriptionLatest } from 'redux/actions/subscriptions';
import { selectBadgeNumber } from 'redux/selectors/app'; import { selectBadgeNumber } from 'redux/selectors/app';
import { selectMyClaimsRaw } from 'redux/selectors/claims'; import { selectMyClaimsRaw } from 'redux/selectors/claims';
import { selectResolvingUris } from 'redux/selectors/content'; import { selectResolvingUris } from 'redux/selectors/content';
@ -21,6 +22,7 @@ import { selectBalance } from 'redux/selectors/wallet';
import batchActions from 'util/batchActions'; import batchActions from 'util/batchActions';
import setBadge from 'util/setBadge'; import setBadge from 'util/setBadge';
import setProgressBar from 'util/setProgressBar'; import setProgressBar from 'util/setProgressBar';
import analytics from 'analytics';
const DOWNLOAD_POLL_INTERVAL = 250; const DOWNLOAD_POLL_INTERVAL = 250;
@ -226,11 +228,7 @@ export function doDownloadFile(uri, streamInfo) {
return dispatch => { return dispatch => {
dispatch(doStartDownload(uri, streamInfo.outpoint)); dispatch(doStartDownload(uri, streamInfo.outpoint));
Lbryio.call('file', 'view', { analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
uri,
outpoint: streamInfo.outpoint,
claim_id: streamInfo.claim_id,
}).catch(() => {});
dispatch(doClaimEligiblePurchaseRewards()); dispatch(doClaimEligiblePurchaseRewards());
}; };
@ -288,7 +286,7 @@ export function doLoadVideo(uri) {
}; };
} }
export function doPurchaseUri(uri) { export function doPurchaseUri(uri, specificCostInfo) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const balance = selectBalance(state); const balance = selectBalance(state);
@ -321,7 +319,7 @@ export function doPurchaseUri(uri) {
return; return;
} }
const costInfo = makeSelectCostInfoForUri(uri)(state); const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
const { cost } = costInfo; const { cost } = costInfo;
if (cost > balance) { if (cost > balance) {
@ -358,6 +356,25 @@ export function doFetchClaimsByChannel(uri, page) {
const claimResult = result[uri] || {}; const claimResult = result[uri] || {};
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult; const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;
if (claimsInChannel && claimsInChannel.length) {
const latest = claimsInChannel[0];
dispatch(
setSubscriptionLatest(
{
channelName: latest.channel_name,
uri: buildURI(
{
contentName: latest.channel_name,
claimId: latest.value.publisherSignature.certificateId,
},
false
),
},
buildURI({ contentName: latest.name, claimId: latest.claim_id }, false)
)
);
}
dispatch({ dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED, type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: { data: {

View file

@ -56,6 +56,20 @@ export function doGetThemes() {
}; };
} }
export function doUpdateIsNight() {
const momentNow = moment();
return {
type: ACTIONS.UPDATE_IS_NIGHT,
data: {
isNight: (() => {
const startNightMoment = moment('21:00', 'HH:mm');
const endNightMoment = moment('8:00', 'HH:mm');
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
})(),
},
};
}
export function doUpdateIsNightAsync() { export function doUpdateIsNightAsync() {
return dispatch => { return dispatch => {
dispatch(doUpdateIsNight()); dispatch(doUpdateIsNight());
@ -66,19 +80,6 @@ export function doUpdateIsNightAsync() {
}; };
} }
export function doUpdateIsNight() {
const momentNow = moment();
return {
type: ACTIONS.UPDATE_IS_NIGHT,
data: { isNight: (() => {
const startNightMoment = moment('19:00', 'HH:mm');
const endNightMoment = moment('8:00', 'HH:mm');
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
})()
},
};
}
export function doDownloadLanguage(langFile) { export function doDownloadLanguage(langFile) {
return dispatch => { return dispatch => {
const destinationPath = `${app.i18n.directory}/${langFile}`; const destinationPath = `${app.i18n.directory}/${langFile}`;

View file

@ -1,6 +1,13 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import type { Subscription, Dispatch } from 'redux/reducers/subscriptions'; import type { Subscription, Dispatch, SubscriptionState } from 'redux/reducers/subscriptions';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import Lbry from 'lbry';
import { doPurchaseUri } from 'redux/actions/content';
import { doNavigate } from 'redux/actions/navigation';
import { buildURI } from 'lbryURI';
const CHECK_SUBSCRIPTIONS_INTERVAL = 10 * 60 * 1000;
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) =>
dispatch({ dispatch({
@ -14,5 +21,113 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D
data: subscription, data: subscription,
}); });
export const doCheckSubscriptions = () => (
dispatch: Dispatch,
getState: () => SubscriptionState
) => {
const checkSubscriptionsTimer = setInterval(
() =>
selectSubscriptions(getState()).map((subscription: Subscription) =>
dispatch(doCheckSubscription(subscription))
),
CHECK_SUBSCRIPTIONS_INTERVAL
);
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
data: { checkSubscriptionsTimer },
});
};
export const doCheckSubscription = (subscription: Subscription) => (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
data: subscription,
});
Lbry.claim_list_by_channel({ uri: subscription.uri, page: 1 }).then(result => {
const claimResult = result[subscription.uri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
const count = subscription.latest
? claimsInChannel.reduce(
(prev, cur, index) =>
buildURI({ contentName: cur.name, claimId: cur.claim_id }, false) ===
subscription.latest
? index
: prev,
-1
)
: 1;
if (count !== 0) {
if (!claimsInChannel[0].value.stream.metadata.fee) {
dispatch(
doPurchaseUri(
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
),
{ cost: 0 }
)
);
}
const notif = new window.Notification(subscription.channelName, {
body: `Posted ${claimsInChannel[0].value.stream.metadata.title}${
count > 1 ? ` and ${count - 1} other new items` : ''
}${count < 0 ? ' and 9+ other new items' : ''}`,
silent: false,
});
notif.onclick = () => {
dispatch(
doNavigate('/show', {
uri: buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
true
),
})
);
};
}
//$FlowIssue
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
data: subscription,
});
});
};
export const checkSubscriptionLatest = (channel: Subscription, uri: string) => (
dispatch: Dispatch
) => {
Lbry.claim_list_by_channel({ uri: channel.uri, page: 1 }).then(result => {
const claimResult = result[channel.uri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
if (
claimsInChannel &&
claimsInChannel.length &&
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
) === uri
) {
dispatch(setSubscriptionLatest(channel, uri));
}
});
};
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
dispatch: Dispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
});
export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) =>
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS });

View file

@ -93,7 +93,7 @@ reducers[ACTIONS.AUTO_UPDATE_DECLINED] = state => {
return Object.assign({}, state, { return Object.assign({}, state, {
autoUpdateDeclined: true, autoUpdateDeclined: true,
}); });
} };
reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) => reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) =>
Object.assign({}, state, { Object.assign({}, state, {

View file

@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils';
export type Subscription = { export type Subscription = {
channelName: string, channelName: string,
uri: string, uri: string,
latest: ?string,
}; };
// Subscription redux types // Subscription redux types
@ -28,7 +29,30 @@ type HasFetchedSubscriptions = {
type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS, type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS,
}; };
export type Action = doChannelSubscribe | doChannelUnsubscribe | HasFetchedSubscriptions; type setSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
export type Action =
| doChannelSubscribe
| doChannelUnsubscribe
| HasFetchedSubscriptions
| setSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| Function;
export type Dispatch = (action: Action) => any; export type Dispatch = (action: Action) => any;
const defaultState = { const defaultState = {
@ -70,6 +94,18 @@ export default handleActions(
...state, ...state,
hasFetchedSubscriptions: true, hasFetchedSubscriptions: true,
}), }),
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState,
action: setSubscriptionLatest
): SubscriptionState => ({
...state,
subscriptions: state.subscriptions.map(
subscription =>
subscription.channelName === action.data.subscription.channelName
? { ...subscription, latest: action.data.uri }
: subscription
),
}),
}, },
defaultState defaultState
); );

View file

@ -56,9 +56,15 @@ export const selectUpgradeDownloadPath = createSelector(selectState, state => st
export const selectUpgradeDownloadItem = createSelector(selectState, state => state.downloadItem); export const selectUpgradeDownloadItem = createSelector(selectState, state => state.downloadItem);
export const selectAutoUpdateDownloaded = createSelector(selectState, state => state.autoUpdateDownloaded); export const selectAutoUpdateDownloaded = createSelector(
selectState,
state => state.autoUpdateDownloaded
);
export const selectAutoUpdateDeclined = createSelector(selectState, state => state.autoUpdateDeclined); export const selectAutoUpdateDeclined = createSelector(
selectState,
state => state.autoUpdateDeclined
);
export const selectModalsAllowed = createSelector(selectState, state => state.modalsAllowed); export const selectModalsAllowed = createSelector(selectState, state => state.modalsAllowed);

View file

@ -0,0 +1,38 @@
// Beautify JSON
const parseJson = data => JSON.stringify(data, null, '\t');
// No need for an external module:
// https://gist.github.com/btzr-io/55c3450ea3d709fc57540e762899fb85
const parseCsv = data => {
// Get items for header
const getHeaders = temp =>
Object.entries(temp)
.map(([key]) => key)
.join(',');
// Get rows content
const getData = list =>
list
.map(item => {
const row = Object.entries(item)
.map(([key, value]) => value)
.join(',');
return row;
})
.join('\n');
// Return CSV string
return `${getHeaders(data[0])} \n ${getData(data)}`;
};
const parseData = (data, format) => {
// Check for validation
const valid = data && data[0] && format;
// Pick a format
const formats = {
csv: list => parseCsv(list),
json: list => parseJson(list),
};
// Return parsed data: JSON || CSV
return valid && formats[format] ? formats[format](data) : undefined;
};
export default parseData;

View file

@ -2,10 +2,10 @@ import { remote } from 'electron';
const application = remote.app; const application = remote.app;
const { dock } = application; const { dock } = application;
const win = remote.BrowserWindow.getFocusedWindow(); const browserWindow = remote.getCurrentWindow();
const setBadge = text => { const setBadge = text => {
if (!dock) return; if (!dock) return;
if (win.isFocused()) return; if (browserWindow.isFocused()) return;
dock.setBadge(text); dock.setBadge(text);
}; };

View file

@ -1,5 +1,6 @@
const path = require('path'); const path = require('path');
const FlowFlowPlugin = require('./flowtype-plugin'); const FlowFlowPlugin = require('./flowtype-plugin');
const isDev = require('electron-is-dev');
const ELECTRON_RENDERER_PROCESS_ROOT = path.resolve(__dirname, 'src/renderer/'); const ELECTRON_RENDERER_PROCESS_ROOT = path.resolve(__dirname, 'src/renderer/');
@ -23,7 +24,7 @@ module.exports = {
}, },
}; };
if (process.env.NODE_ENV === 'development') { if (isDev) {
module.exports.plugins = [ module.exports.plugins = [
new FlowFlowPlugin({ new FlowFlowPlugin({
warn: true, warn: true,

2684
yarn.lock

File diff suppressed because it is too large Load diff