Merge branch 'master' into daemon19

This commit is contained in:
liamcardenas 2018-03-08 10:20:28 -08:00
commit cbe05c3bcc
46 changed files with 2661 additions and 1314 deletions

View file

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

View file

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

1
.gitignore vendored
View file

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

View file

@ -1,28 +1,50 @@
env:
global:
- TRANSIFEX_USER: api
- 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=
- secure: h1r9Qzv2xHRQl7nDHcscB4qDv7KlF/ncgHko1YuoY4oLZipBV8mzQXDmn3nlMKwaKOe1/Tty/bjoZexkWict4cwKBzU7/1HtJeMa6nxRICuS6DiVhLUNGZEddK6jQLxeEZFxkFPSCZyjybPWtasF8f8jd0lqqLIL4/FcIVV56aRCKAsUwCbedxi8Vnc19l74xjaQIK82xBFYOQPK078OBovk9DDOnicTjMulUo3/pKEZD1njSdcEhfSRv+MFE+31B/a6lpoLo7twPlyzLMfpo30NlEzIN0TeMAk44e4PV6DYg0wntC2GJ21p4BqMnDGocwnZwm7gpjflzUZdW6hF0esGLcqOdbyJLUb3rNX9AzQmn0p9KwDC3S80peZFxSiuLJGL8eivceVDUK/jwWinu3OHDJ/eO5iMDm9odm2ALemPtrDTSlRNT8HzNCY9PQTU9Dhdm4Q/dGDsRPWibFJSJ/qGKhVgadk2CUEAPua0hB1zZ556PkTGx4R1JDscgFDAkgemzgKl4Z/4qK3xGDoEtz1HmBlvQtn+B/PuhA2essADj0iTDiItxb7AYTA7EzsHEcRMmrbYarZ3Eh2onWy2GOpAGRN5Xl9cBIDbibcSC6BLI1m2PcLABpP7DhUX4bJbsVNSiGesHEU7o9Dgn8Ig09eHW/8F9i0VVoGUZXxKMJ8=
os: linux
dist: xenial
branches:
only:
- "/^v\\d+\\.\\d+\\.\\d+$/"
install:
- rvm install 2.3.1
matrix:
include:
- os: osx
osx_image: xcode9.2
language: node_js
node_js: "9"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux
services: docker
language: generic
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
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:
- rvm use 2.3.1 && gem install danger --version '~> 4.0' && danger
- FULL_BUILD=true ./build.sh
sudo: required
after_success:
- pip install virtualenv
- virtualenv ~/env
- source ~/env/bin/activate
- pip install transifex-client
- sudo echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername= '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\ntoken = '"$TRANSIFEX_API_TOKEN"$'\n' > ~/.transifexrc
- tx push -s
- |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
docker run --rm \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \
/bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --linux --win"
else
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]
### 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
@ -16,8 +19,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
*
### 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
*

View file

@ -43,32 +43,6 @@ fi
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 #
###################
@ -79,23 +53,6 @@ if [ "$FULL_BUILD" == "true" ]; then
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'
else
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",
"publish": {
"productName": "LBRY",
"publish": [{
"provider": "s3",
"bucket": "releases.lbry.io",
"path": "app/latest"
},
}, "github"],
"mac": {
"target": "dmg",
"category": "public.app-category.entertainment"
},
"dmg": {
@ -28,36 +30,24 @@
"width": 500,
"height": 300
},
"background": "build/background.png"
"background": "./build/background.png"
},
"protocols": [
{
"name": "lbry",
"name": "LBRY URI",
"schemes": ["lbry"],
"role": "Viewer"
}
],
"linux": {
"target": "deb",
"category": "Video;AudioVideo;",
"target": "AppImage",
"category": "AudioVideo;Video",
"desktop": {
"MimeType": "x-scheme-handler/lbry",
"Exec": "/opt/LBRY/lbry %U"
"MimeType": "x-scheme-handler/lbry;"
}
},
"deb": {
"depends": [
"gconf2",
"gconf-service",
"libnotify4",
"libappindicator1",
"libxtst6",
"libnss3",
"libsecret-1-0"
]
},
"nsis": {
"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",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"homepage": "https://lbry.io/",
@ -21,7 +21,8 @@
"compile": "electron-webpack && yarn extract-langs",
"build": "yarn compile && electron-builder build",
"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",
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write"
},
@ -33,16 +34,17 @@
"classnames": "^2.2.5",
"country-data": "^0.0.31",
"electron-dl": "^1.6.0",
"electron-is-dev": "^0.3.0",
"electron-log": "^2.2.12",
"electron-publisher-s3": "^19.47.0",
"electron-updater": "^2.18.2",
"electron-publisher-s3": "^20.2.0",
"electron-updater": "^2.21.0",
"find-process": "^1.1.0",
"formik": "^0.10.4",
"from2": "^2.3.0",
"install": "^0.10.2",
"jayson": "^2.0.2",
"jshashes": "^1.0.7",
"keytar-prebuild": "^4.0.4",
"keytar-prebuild": "^4.1.1",
"localforage": "^1.5.0",
"mixpanel-browser": "^2.17.1",
"moment": "^2.20.1",
@ -72,43 +74,41 @@
"y18n": "^4.0.0"
},
"devDependencies": {
"babel-eslint": "^8.0.3",
"babel-plugin-module-resolver": "^3.0.0",
"babel-plugin-react-require": "^3.0.0",
"axios": "^0.18.0",
"babel-eslint": "^8.2.2",
"babel-plugin-module-resolver": "^3.1.0",
"babel-polyfill": "^6.20.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0",
"decompress": "^4.2.0",
"devtron": "^1.4.0",
"electron": "^1.7.11",
"electron-builder": "^19.55.2",
"electron-devtools-installer": "^2.2.1",
"electron-webpack": "^1.11.0",
"eslint": "^4.13.1",
"electron": "^1.8.3",
"electron-builder": "^20.3.1",
"electron-devtools-installer": "^2.2.3",
"electron-webpack": "^1.13.0",
"eslint": "^4.18.2",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-import-resolver-webpack": "^0.8.3",
"eslint-plugin-flowtype": "^2.40.1",
"eslint-plugin-import": "^2.8.0",
"eslint-import-resolver-webpack": "^0.8.4",
"eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^2.4.0",
"eslint-plugin-react": "^7.5.1",
"flow-babel-webpack-plugin": "^1.1.0",
"flow-bin": "^0.61.0",
"flow-typed": "^2.2.3",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.7.0",
"flow-babel-webpack-plugin": "^1.1.1",
"flow-bin": "^0.66.0",
"flow-typed": "^2.3.0",
"husky": "^0.14.3",
"i18n-extract": "^0.5.1",
"json-loader": "^0.5.4",
"lint-staged": "^6.0.0",
"lint-staged": "^7.0.0",
"node-loader": "^0.6.0",
"node-sass": "^4.7.2",
"prettier": "^1.4.2",
"sass-loader": "^6.0.6",
"prettier": "^1.11.1",
"sass-loader": "^6.0.7",
"webpack": "^3.10.0",
"webpack-build-notifier": "^0.1.18"
},
"resolutions": {
"webpack/webpack-sources": "1.0.1"
"webpack-build-notifier": "^0.1.23"
},
"engines": {
"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 isDev from 'electron-is-dev';
import setupBarMenu from './menu/setupBarMenu';
import setupContextMenu from './menu/setupContextMenu';
export default deepLinkingURIArg => {
export default appState => {
let windowConfiguration = {
backgroundColor: '#155B4A',
minWidth: 800,
@ -12,20 +13,18 @@ export default deepLinkingURIArg => {
};
// Disable renderer process's webSecurity on development to enable CORS.
windowConfiguration =
process.env.NODE_ENV === 'development'
? {
...windowConfiguration,
webPreferences: {
webSecurity: false,
},
}
: windowConfiguration;
windowConfiguration = isDev
? {
...windowConfiguration,
webPreferences: {
webSecurity: false,
},
}
: windowConfiguration;
const rendererURL =
process.env.NODE_ENV === 'development'
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
: `file://${__dirname}/index.html`;
const rendererURL = isDev
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
: `file://${__dirname}/index.html`;
let window = new BrowserWindow(windowConfiguration);
@ -35,11 +34,7 @@ export default deepLinkingURIArg => {
let deepLinkingURI;
// Protocol handler for win32
if (
!deepLinkingURIArg &&
process.platform === 'win32' &&
String(process.argv[1]).startsWith('lbry')
) {
if (process.platform === 'win32' && String(process.argv[1]).startsWith('lbry')) {
// Keep only command line / deep linked arguments
// Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to
// 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
// an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well.
deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#');
} else {
deepLinkingURI = deepLinkingURIArg;
}
setupBarMenu();
setupContextMenu(window);
window.on('closed', () => {
window = null;
window.on('close', event => {
if (!appState.isQuitting) {
event.preventDefault();
window.hide();
}
});
window.on('focus', () => {
@ -87,7 +83,7 @@ export default deepLinkingURIArg => {
window.webContents.on('did-finish-load', () => {
window.webContents.send('open-uri-requested', deepLinkingURI, true);
window.webContents.session.setUserAgent(`LBRY/${app.getVersion()}`);
if (process.env.NODE_ENV === 'development') {
if (isDev) {
window.webContents.openDevTools();
}
});

View file

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

View file

@ -1,5 +1,6 @@
// @flow
import { Menu, BrowserWindow } from 'electron';
import isDev from 'electron-is-dev';
export default (rendererWindow: BrowserWindow) => {
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);
}

View file

@ -1,20 +1,27 @@
// @flow
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 = {
track: (string, ?Object) => void,
setUser: (Object) => void,
toggle: (boolean, ?boolean) => void
}
setUser: Object => void,
toggle: (boolean, ?boolean) => void,
apiLog: (string, string, string) => void,
};
let analyticsEnabled: boolean = false;
const analytics: Analytics = {
track: (name: string, payload: ?Object): void => {
if(analyticsEnabled) {
if(payload) {
if (analyticsEnabled) {
if (payload) {
mixpanel.track(name, payload);
} else {
mixpanel.track(name);
@ -22,21 +29,30 @@ const analytics: Analytics = {
}
},
setUser: (user: Object): void => {
if(user.id) {
if (user.id) {
mixpanel.identify(user.id);
}
if(user.primary_email) {
if (user.primary_email) {
mixpanel.people.set({
"$email": user.primary_email
$email: user.primary_email,
});
}
},
toggle: (enabled: boolean, logDisabled: ?boolean): void => {
if(!enabled && logDisabled) {
if (!enabled && logDisabled) {
mixpanel.track('DISABLED');
}
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;

View file

@ -2,6 +2,7 @@ import store from 'store';
import { remote } from 'electron';
import Path from 'path';
import y18n from 'y18n';
import isDev from 'electron-is-dev';
const env = process.env.NODE_ENV || 'production';
const i18n = y18n({
@ -22,7 +23,7 @@ const app = {
};
// 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(
/\\/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);
this.state = {
sortBy: 'date',
sortBy: 'dateNew',
};
this._sortFunctions = {
date(fileInfos) {
return fileInfos.slice().reverse();
dateNew(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;
});
},
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) {
return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
@ -95,7 +116,8 @@ class FileList extends React.PureComponent {
<span className="sort-section">
{__('Sort by')}{' '}
<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>
</FormField>
</span>

View file

@ -14,6 +14,10 @@ const RewardSummary = (props: Props) => {
<section className="card">
<div className="card__title-primary">
<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 className="card__content">
{unclaimedRewardAmount > 0 ? (

View file

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

View file

@ -1,5 +1,4 @@
const { remote } = require('electron');
import { remote } from 'electron';
import React from 'react';
import { Thumbnail } from 'component/common';
import player from 'render-media';
@ -21,33 +20,21 @@ class VideoPlayer extends React.PureComponent {
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() {
const container = this.refs.media;
const {
contentType,
downloadPath,
mediaType,
changeVolume,
volume,
position,
claim,
uri,
} = this.props;
const container = this.media;
const { contentType, changeVolume, volume, position, claim } = this.props;
const loadedMetadata = e => {
const loadedMetadata = () => {
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
const win32FullScreenChange = e => {
const win32FullScreenChange = () => {
const win = remote.BrowserWindow.getFocusedWindow();
if (process.platform === 'win32') {
win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu());
@ -61,13 +48,13 @@ class VideoPlayer extends React.PureComponent {
player.append(
this.file(),
container,
{ autoplay: false, controls: true },
{ autoplay: true, controls: true },
renderMediaCallback.bind(this)
);
}
document.addEventListener('keydown', this.togglePlayListener);
const mediaElement = this.refs.media.children[0];
const mediaElement = this.media.children[0];
if (mediaElement) {
mediaElement.currentTime = position || 0;
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() {
document.removeEventListener('keydown', this.togglePlayListener);
const mediaElement = this.refs.media.children[0];
const mediaElement = this.media.children[0];
if (mediaElement) {
mediaElement.removeEventListener('click', this.togglePlayListener);
}
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) {
// ignore all events except click and spacebar keydown, or input events in a form control
if (
@ -119,7 +115,7 @@ class VideoPlayer extends React.PureComponent {
return;
}
event.preventDefault();
const mediaElement = this.refs.media.children[0];
const mediaElement = this.media.children[0];
if (mediaElement) {
if (!mediaElement.paused) {
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() {
const { downloadPath, filename } = this.props;
@ -162,14 +140,26 @@ class VideoPlayer extends React.PureComponent {
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() {
const { mediaType, poster } = this.props;
const { hasMetadata, unplayable } = this.state;
const noMetadataMessage = 'Waiting for metadata.';
const unplayableMessage = "Sorry, looks like we can't play this file.";
const needsMetadata = this.playableType();
return (
<div>
{['audio', 'application'].indexOf(mediaType) !== -1 &&
@ -179,7 +169,12 @@ class VideoPlayer extends React.PureComponent {
!hasMetadata &&
!unplayable && <LoadingScreen status={noMetadataMessage} />}
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
<div ref="media" className="media" />
<div
ref={container => {
this.media = container;
}}
className="media"
/>
</div>
);
}

View file

@ -164,6 +164,10 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
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
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';

View file

@ -3,3 +3,4 @@ export const LOCAL = 'folder';
export const FILE = 'file';
export const HISTORY = 'history';
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 FILE_TIMEOUT = 'file_timeout';
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 ERROR = 'error';
export const INSUFFICIENT_CREDITS = 'insufficient_credits';

View file

@ -8,10 +8,14 @@ import lbry from 'lbry';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doShowSnackBar, doAutoUpdate } from 'redux/actions/app';
import { doUpdateIsNightAsync } from 'redux/actions/settings';
import {
doConditionalAuthNavigate,
doDaemonReady,
doShowSnackBar,
doAutoUpdate,
} from 'redux/actions/app';
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 'scss/all.scss';
import store from 'store';
@ -20,12 +24,7 @@ import analytics from './analytics';
const { autoUpdater } = remote.require('electron-updater');
autoUpdater.logger = remote.require("electron-log");
window.addEventListener('contextmenu', event => {
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
event.preventDefault();
});
autoUpdater.logger = remote.require('electron-log');
ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
if (uri && uri.startsWith('lbry://')) {
@ -62,6 +61,12 @@ ipcRenderer.on('window-is-focused', () => {
dock.setBadge('');
});
document.addEventListener('dragover', event => {
event.preventDefault();
});
document.addEventListener('drop', event => {
event.preventDefault();
});
document.addEventListener('click', event => {
let { target } = event;
while (target && target !== document) {
@ -91,22 +96,21 @@ document.addEventListener('click', event => {
});
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());
});
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(doDownloadLanguages());

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import React from "react";
import { Modal } from "modal/modal";
import { Line } from "rc-progress";
import Link from "component/link/index";
import React from 'react';
import { Modal } from 'modal/modal';
import { Line } from 'rc-progress';
import Link from 'component/link/index';
const { ipcRenderer } = require("electron");
const { ipcRenderer } = require('electron');
class ModalAutoUpdateDownloaded extends React.PureComponent {
render() {
@ -13,20 +13,20 @@ class ModalAutoUpdateDownloaded extends React.PureComponent {
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Update Downloaded")}
confirmButtonLabel={__("Use it Now")}
abortButtonLabel={__("Upgrade on Close")}
contentLabel={__('Update Downloaded')}
confirmButtonLabel={__('Use it Now')}
abortButtonLabel={__('Upgrade on Close')}
onConfirmed={() => {
ipcRenderer.send("autoUpdateAccepted");
ipcRenderer.send('autoUpdateAccepted');
}}
onAborted={() => {
declineAutoUpdate();
ipcRenderer.send("autoUpdateDeclined");
ipcRenderer.send('autoUpdateDeclined');
closeModal();
}}
>
<section>
<h3 className="text-center">{__("LBRY Leveled Up")}</h3>
<h3 className="text-center">{__('LBRY Leveled Up')}</h3>
<p>
{__(
'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">
<section>
<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>
</Modal>
);

View file

@ -68,7 +68,7 @@ export class AuthPage extends React.PureComponent {
<div className="card__content">
<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')} />.
</div>

View file

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

View file

@ -17,6 +17,7 @@ class FilePage extends React.PureComponent {
componentDidMount() {
this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props);
this.checkSubscriptionLatest(this.props);
}
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() {
const {
claim,

View file

@ -14,6 +14,8 @@ class SettingsPage extends React.PureComponent {
this.state = {
clearingCache: false,
};
this.onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this);
}
clearCache() {
@ -62,11 +64,16 @@ class SettingsPage extends React.PureComponent {
onThemeChange(event) {
const { value } = event.target;
if (value === 'dark') {
this.onAutomaticDarkModeChange(false);
}
this.props.setClientSetting(settings.THEME, value);
}
onAutomaticDarkModeChange(event) {
this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, event.target.checked);
onAutomaticDarkModeChange(value) {
this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value);
}
onInstantPurchaseEnabledChange(enabled) {
@ -143,6 +150,7 @@ class SettingsPage extends React.PureComponent {
</main>
);
}
return (
<main className="main--single-column">
<SubHeader />
@ -300,7 +308,9 @@ class SettingsPage extends React.PureComponent {
type="checkbox"
onChange={this.onShareDataChange.bind(this)}
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>
</section>
@ -325,8 +335,9 @@ class SettingsPage extends React.PureComponent {
<FormRow
type="checkbox"
onChange={this.onAutomaticDarkModeChange.bind(this)}
defaultChecked={automaticDarkModeEnabled}
disabled={theme === 'dark'}
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
checked={automaticDarkModeEnabled}
label={__('Automatic dark mode (9pm to 8am)')}
/>
</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 MODALS from 'constants/modal_types';
import { ipcRenderer, remote } from 'electron';
import Lbry from 'lbry';
import Path from 'path';
import { doFetchRewardedContent } from 'redux/actions/content';
import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info';
import { doAuthNavigate } from 'redux/actions/navigation';
import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthenticate } from 'redux/actions/user';
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 {
selectCurrentModal,
selectIsUpgradeSkipped,
selectRemoteVersion,
selectUpdateUrl,
selectUpgradeDownloadItem,
selectUpgradeDownloadPath,
selectUpgradeFilename,
selectAutoUpdateDeclined,
} from 'redux/selectors/app';
import { lbrySettings as config } from 'package.json';
const { autoUpdater } = remote.require('electron-updater');
const { download } = remote.require('electron-dl');
const Fs = remote.require('fs');
const { lbrySettings: config } = require('package.json');
const CHECK_UPGRADE_INTERVAL = 10 * 60 * 1000;
@ -84,23 +84,19 @@ export function doDownloadUpgradeRequested() {
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
if (['win32', 'darwin'].includes(process.platform)) { // electron-updater behavior
if (autoUpdateDeclined) {
// The user declined an update before, so show the "confirm" dialog
dispatch({
type: ACTIONS.OPEN_MODAL,
data: { modal: MODALS.AUTO_UPDATE_CONFIRM },
});
} else {
// The user was never shown the original update dialog (e.g. because they were
// watching a video). So show the inital "update downloaded" dialog.
dispatch({
type: ACTIONS.OPEN_MODAL,
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
});
}
} else { // Old behavior for Linux
dispatch(doDownloadUpgrade());
if (autoUpdateDeclined) {
// The user declined an update before, so show the "confirm" dialog
dispatch({
type: ACTIONS.OPEN_MODAL,
data: { modal: MODALS.AUTO_UPDATE_CONFIRM },
});
} else {
// The user was never shown the original update dialog (e.g. because they were
// watching a video). So show the initial "update downloaded" dialog.
dispatch({
type: ACTIONS.OPEN_MODAL,
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
});
}
};
}
@ -109,7 +105,7 @@ export function doDownloadUpgrade() {
return (dispatch, getState) => {
const state = getState();
// 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 options = {
@ -127,7 +123,7 @@ export function doDownloadUpgrade() {
type: ACTIONS.UPGRADE_DOWNLOAD_COMPLETED,
data: {
downloadItem,
path: Path.join(dir, upgradeFilename),
path: path.join(dir, upgradeFilename),
},
});
});
@ -145,8 +141,7 @@ export function doDownloadUpgrade() {
}
export function doAutoUpdate() {
return function(dispatch, getState) {
const state = getState();
return dispatch => {
dispatch({
type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
});
@ -159,12 +154,11 @@ export function doAutoUpdate() {
}
export function doAutoUpdateDeclined() {
return function(dispatch, getState) {
const state = getState();
return dispatch => {
dispatch({
type: ACTIONS.AUTO_UPDATE_DECLINED,
});
}
};
}
export function doCancelUpgrade() {
@ -181,6 +175,7 @@ export function doCancelUpgrade() {
try {
upgradeDownloadItem.cancel();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
// Do nothing
}
@ -197,47 +192,11 @@ export function doCheckUpgradeAvailable() {
type: ACTIONS.CHECK_UPGRADE_START,
});
if (["win32", "darwin"].includes(process.platform)) {
// On Windows and Mac, updates happen silently through
// electron-updater.
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
if (!autoUpdateDeclined) {
autoUpdater.checkForUpdates();
}
return;
if (!autoUpdateDeclined && !isDev) {
autoUpdater.checkForUpdates();
}
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(doCheckUpgradeSubscribe());
dispatch(doCheckSubscriptions());
};
}

View file

@ -7,6 +7,7 @@ import Lbryio from 'lbryio';
import { normalizeURI, buildURI } from 'lbryURI';
import { doAlertError, doOpenModal } from 'redux/actions/app';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
import { setSubscriptionLatest } from 'redux/actions/subscriptions';
import { selectBadgeNumber } from 'redux/selectors/app';
import { selectMyClaimsRaw } from 'redux/selectors/claims';
import { selectResolvingUris } from 'redux/selectors/content';
@ -21,6 +22,7 @@ import { selectBalance } from 'redux/selectors/wallet';
import batchActions from 'util/batchActions';
import setBadge from 'util/setBadge';
import setProgressBar from 'util/setProgressBar';
import analytics from 'analytics';
const DOWNLOAD_POLL_INTERVAL = 250;
@ -226,11 +228,7 @@ export function doDownloadFile(uri, streamInfo) {
return dispatch => {
dispatch(doStartDownload(uri, streamInfo.outpoint));
Lbryio.call('file', 'view', {
uri,
outpoint: streamInfo.outpoint,
claim_id: streamInfo.claim_id,
}).catch(() => {});
analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
dispatch(doClaimEligiblePurchaseRewards());
};
@ -288,7 +286,7 @@ export function doLoadVideo(uri) {
};
}
export function doPurchaseUri(uri) {
export function doPurchaseUri(uri, specificCostInfo) {
return (dispatch, getState) => {
const state = getState();
const balance = selectBalance(state);
@ -321,7 +319,7 @@ export function doPurchaseUri(uri) {
return;
}
const costInfo = makeSelectCostInfoForUri(uri)(state);
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
const { cost } = costInfo;
if (cost > balance) {
@ -358,6 +356,25 @@ export function doFetchClaimsByChannel(uri, page) {
const claimResult = result[uri] || {};
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({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
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() {
return dispatch => {
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) {
return dispatch => {
const destinationPath = `${app.i18n.directory}/${langFile}`;

View file

@ -1,6 +1,13 @@
// @flow
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) =>
dispatch({
@ -14,5 +21,113 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D
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) =>
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS });

View file

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

View file

@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils';
export type Subscription = {
channelName: string,
uri: string,
latest: ?string,
};
// Subscription redux types
@ -28,7 +29,30 @@ type HasFetchedSubscriptions = {
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;
const defaultState = {
@ -70,6 +94,18 @@ export default handleActions(
...state,
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
);

View file

@ -56,9 +56,15 @@ export const selectUpgradeDownloadPath = createSelector(selectState, state => st
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);

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 { dock } = application;
const win = remote.BrowserWindow.getFocusedWindow();
const browserWindow = remote.getCurrentWindow();
const setBadge = text => {
if (!dock) return;
if (win.isFocused()) return;
if (browserWindow.isFocused()) return;
dock.setBadge(text);
};

View file

@ -1,5 +1,6 @@
const path = require('path');
const FlowFlowPlugin = require('./flowtype-plugin');
const isDev = require('electron-is-dev');
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 = [
new FlowFlowPlugin({
warn: true,

2684
yarn.lock

File diff suppressed because it is too large Load diff