Compare commits
6 commits
master
...
new-sync-d
Author | SHA1 | Date | |
---|---|---|---|
|
d8be27aa50 | ||
|
bdfb8d29f5 | ||
|
6b21df2c17 | ||
|
ded2992e44 | ||
|
9321a0ba37 | ||
|
7ab5a0c978 |
219 changed files with 2261 additions and 7355 deletions
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,22 +38,7 @@ jobs:
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
if: startsWith(runner.os, 'mac')
|
if: startsWith(runner.os, 'mac')
|
||||||
with:
|
with:
|
||||||
xcode-version: '13.1.0'
|
xcode-version: '12.4.0'
|
||||||
# This is gonna be hacky.
|
|
||||||
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
|
||||||
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
|
||||||
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
|
||||||
# So for now we'll install python2 on mac and make sure it can find it.
|
|
||||||
# Remove this after successfully upgrading electron-builder.
|
|
||||||
# HACK part 1
|
|
||||||
- uses: Homebrew/actions/setup-homebrew@master
|
|
||||||
if: startsWith(runner.os, 'mac')
|
|
||||||
# HACK part 2
|
|
||||||
- name: Install Python2
|
|
||||||
if: startsWith(runner.os, 'mac')
|
|
||||||
run: |
|
|
||||||
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
|
||||||
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Download blockchain headers
|
- name: Download blockchain headers
|
||||||
run: |
|
run: |
|
||||||
|
@ -73,7 +58,7 @@ jobs:
|
||||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx
|
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
|
||||||
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
0
.yarn/versions/5bc94294.yml
vendored
0
.yarn/versions/5bc94294.yml
vendored
0
.yarn/versions/5f1212ad.yml
vendored
0
.yarn/versions/5f1212ad.yml
vendored
0
.yarn/versions/6b35c994.yml
vendored
0
.yarn/versions/6b35c994.yml
vendored
0
.yarn/versions/6be5ab70.yml
vendored
0
.yarn/versions/6be5ab70.yml
vendored
0
.yarn/versions/8e384637.yml
vendored
0
.yarn/versions/8e384637.yml
vendored
0
.yarn/versions/d1a18cef.yml
vendored
0
.yarn/versions/d1a18cef.yml
vendored
0
.yarn/versions/ec3a9ddf.yml
vendored
0
.yarn/versions/ec3a9ddf.yml
vendored
48
CHANGELOG.md
48
CHANGELOG.md
|
@ -1,53 +1,7 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [0.53.9] - [2023-2-8]
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Updated lbrynet to [0.113.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.113.0)
|
|
||||||
|
|
||||||
## [0.53.8] - [2022-11-17]
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Selecting a large file in publish no longer crashes ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
|
||||||
- Unfollowing unpublished channels ([#7737](https://github.com/lbryio/lbry-desktop/pull/7737))
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Updated xcode to 13.1 and hacked a fix for release ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
|
||||||
|
|
||||||
## [0.53.7] - [2022-11-10]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- 'Collections' to txo filter _community pr!_ ([#7711](https://github.com/lbryio/lbry-desktop/pull/7711))
|
|
||||||
- Swap comment servers _community pr!_ ([#7670](https://github.com/lbryio/lbry-desktop/pull/7670))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Thumbnails no longer disable publish ([#7714](https://github.com/lbryio/lbry-desktop/pull/7714))
|
|
||||||
- Publishing posts were empty ([#7715](https://github.com/lbryio/lbry-desktop/pull/7715))
|
|
||||||
- Minor layout fixes _community pr!_ ([#7709](https://github.com/lbryio/lbry-desktop/pull/7709))
|
|
||||||
- Comment section buttons layout ([#7716](https://github.com/lbryio/lbry-desktop/pull/7716))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Removed watchman and its errors ([#7710](https://github.com/lbryio/lbry-desktop/pull/7710))
|
|
||||||
- Updated lbrynet to [0.112.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.112.0)
|
|
||||||
|
|
||||||
## [0.53.6] - [2022-10-21]
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Make thumbnails optional ([#7690](https://github.com/lbryio/lbry-desktop/pull/7690))
|
|
||||||
- Show downloads newest first ([#7684](https://github.com/lbryio/lbry-desktop/pull/7684))
|
|
||||||
- Only allow images in image uploader ([#7672](https://github.com/lbryio/lbry-desktop/pull/7672))
|
|
||||||
- Fixed bug with csv exports ([#7697](https://github.com/lbryio/lbry-desktop/pull/7697))
|
|
||||||
- Fixed various upload bugs including transcoding ([#7688](https://github.com/lbryio/lbry-desktop/pull/7688))
|
|
||||||
- Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Upgraded Electron to v17.2.0 ([#7703](https://github.com/lbryio/lbry-desktop/pull/7703))
|
|
||||||
- Upgraded Electron to v17.0.0 ([#7691](https://github.com/lbryio/lbry-desktop/pull/7691))
|
|
||||||
- Updated lbrynet to [0.111.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.111.0)
|
|
||||||
|
|
||||||
## [0.53.5] - [2022-08-26]
|
## [0.53.5] - [2022-08-26]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
|
||||||
|
|
||||||
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
||||||
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||||
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -1,91 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const packageJSON = require('../package.json');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const decompress = require('decompress');
|
|
||||||
const os = require('os');
|
|
||||||
const del = require('del');
|
|
||||||
|
|
||||||
const downloadLBRYFirst = targetPlatform =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const lbryFirstURLTemplate = packageJSON.lbrySettings.LBRYFirstUrlTemplate;
|
|
||||||
const lbryFirstVersion = packageJSON.lbrySettings.LBRYFirstVersion;
|
|
||||||
const lbryFirstDir = path.join(__dirname, '..', packageJSON.lbrySettings.LBRYFirstDir);
|
|
||||||
let lbryFirstFileName = packageJSON.lbrySettings.LBRYFirstFileName;
|
|
||||||
|
|
||||||
const currentPlatform = os.platform();
|
|
||||||
|
|
||||||
let lbryFirstPlatform = process.env.TARGET || targetPlatform || currentPlatform;
|
|
||||||
if (lbryFirstPlatform === 'linux') lbryFirstPlatform = 'Linux';
|
|
||||||
if (lbryFirstPlatform === 'mac' || lbryFirstPlatform === 'darwin') lbryFirstPlatform = 'Darwin';
|
|
||||||
if (lbryFirstPlatform === 'win32' || lbryFirstPlatform === 'windows') {
|
|
||||||
lbryFirstPlatform = 'Windows';
|
|
||||||
lbryFirstFileName += '.exe';
|
|
||||||
}
|
|
||||||
const lbryFirstFilePath = path.join(lbryFirstDir, lbryFirstFileName);
|
|
||||||
const lbryFirstVersionPath = path.join(__dirname, 'lbryFirst.ver');
|
|
||||||
const tmpZipPath = path.join(__dirname, '..', 'dist', 'lbryFirst.zip');
|
|
||||||
const lbryFirstURL = lbryFirstURLTemplate.replace(/LBRYFIRSTVER/g, lbryFirstVersion).replace(/OSNAME/g, lbryFirstPlatform);
|
|
||||||
console.log('URL:', lbryFirstURL);
|
|
||||||
|
|
||||||
// If a lbryFirst and lbryFirst.ver exists, check to see if it matches the current lbryFirst version
|
|
||||||
const hasLbryFirstDownloaded = fs.existsSync(lbryFirstFilePath);
|
|
||||||
const hasLbryFirstVersion = fs.existsSync(lbryFirstVersionPath);
|
|
||||||
let downloadedLbryFirstVersion;
|
|
||||||
|
|
||||||
if (hasLbryFirstVersion) {
|
|
||||||
downloadedLbryFirstVersion = fs.readFileSync(lbryFirstVersionPath, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasLbryFirstDownloaded && hasLbryFirstVersion && downloadedLbryFirstVersion === lbryFirstVersion) {
|
|
||||||
console.log('\x1b[34minfo\x1b[0m LbryFirst already downloaded');
|
|
||||||
resolve('Done');
|
|
||||||
} else {
|
|
||||||
console.log('\x1b[34minfo\x1b[0m Downloading lbryFirst...');
|
|
||||||
fetch(lbryFirstURL, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/zip',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(response => response.buffer())
|
|
||||||
.then(
|
|
||||||
result =>
|
|
||||||
new Promise((newResolve, newReject) => {
|
|
||||||
const distPath = path.join(__dirname, '..', 'dist');
|
|
||||||
const hasDistFolder = fs.existsSync(distPath);
|
|
||||||
|
|
||||||
if (!hasDistFolder) {
|
|
||||||
fs.mkdirSync(distPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFile(tmpZipPath, result, error => {
|
|
||||||
if (error) return newReject(error);
|
|
||||||
return newResolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(() => del(`${lbryFirstFilePath}*`))
|
|
||||||
.then()
|
|
||||||
.then(() =>
|
|
||||||
decompress(tmpZipPath, lbryFirstDir, {
|
|
||||||
filter: file => path.basename(file.path) === lbryFirstFileName,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
console.log('\x1b[32msuccess\x1b[0m LbryFirst downloaded!');
|
|
||||||
if (hasLbryFirstVersion) {
|
|
||||||
del(lbryFirstVersionPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(lbryFirstVersionPath, lbryFirstVersion, 'utf8');
|
|
||||||
resolve('Done');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(`\x1b[31merror\x1b[0m LbryFirst download failed due to: \x1b[35m${error}\x1b[0m`);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadLBRYFirst();
|
|
|
@ -13,6 +13,7 @@ const config = {
|
||||||
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
||||||
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //player.odysee.com
|
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //player.odysee.com
|
||||||
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
||||||
|
LBRYSYNC_API: process.env.LBRYSYNC_API,
|
||||||
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
|
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
|
||||||
CLOUD_CONNECT_SITE_NAME: process.env.CLOUD_CONNECT_SITE_NAME,
|
CLOUD_CONNECT_SITE_NAME: process.env.CLOUD_CONNECT_SITE_NAME,
|
||||||
COMMENT_SERVER_API: process.env.COMMENT_SERVER_API,
|
COMMENT_SERVER_API: process.env.COMMENT_SERVER_API,
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
import { spawn, execSync } from 'child_process';
|
|
||||||
|
|
||||||
export default class LbryFirstInstance {
|
|
||||||
static lbryFirstPath =
|
|
||||||
process.env.LBRY_FIRST_DAEMON ||
|
|
||||||
(process.env.NODE_ENV === 'production'
|
|
||||||
? path.join(process.resourcesPath, 'static/lbry-first', 'lbry-first')
|
|
||||||
: path.join(__static, 'lbry-first/lbry-first'));
|
|
||||||
|
|
||||||
static headersPath =
|
|
||||||
process.env.LBRY_FIRST_DAEMON ||
|
|
||||||
(process.env.NODE_ENV === 'production'
|
|
||||||
? path.join(process.resourcesPath, 'static/lbry-first', 'headers')
|
|
||||||
: path.join(__static, 'lbry-first/headers'));
|
|
||||||
|
|
||||||
subprocess;
|
|
||||||
handlers;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.handlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
launch() {
|
|
||||||
let flags = ['serve'];
|
|
||||||
console.log(`LbryFirst: ${LbryFirstInstance.lbryFirstPath}`);
|
|
||||||
this.subprocess = spawn(LbryFirstInstance.lbryFirstPath, flags);
|
|
||||||
this.subprocess.stdout.on('data', data => console.log(`LbryFirst: ${data}`));
|
|
||||||
this.subprocess.stderr.on('data', data => console.error(`LbryFirst: ${data}`));
|
|
||||||
this.subprocess.on('exit', () => this.fire('exit'));
|
|
||||||
this.subprocess.on('error', error => console.error(`LbryFirst error: ${error}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
quit() {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
try {
|
|
||||||
execSync(`taskkill /pid ${this.subprocess.pid} /t /f`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.subprocess.kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Follows the publish/subscribe pattern
|
|
||||||
|
|
||||||
// Subscribe method
|
|
||||||
on(event, handler, context = handler) {
|
|
||||||
this.handlers.push({ event, handler: handler.bind(context) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish method
|
|
||||||
fire(event, args) {
|
|
||||||
this.handlers.forEach(topic => {
|
|
||||||
if (topic.event === event) topic.handler(args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ import https from 'https';
|
||||||
import { app, dialog, ipcMain, session, shell, BrowserWindow } from 'electron';
|
import { app, dialog, ipcMain, session, shell, BrowserWindow } from 'electron';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
import LbryFirstInstance from './LbryFirstInstance';
|
|
||||||
import Daemon from './Daemon';
|
import Daemon from './Daemon';
|
||||||
import isDev from 'electron-is-dev';
|
import isDev from 'electron-is-dev';
|
||||||
import createTray from './createTray';
|
import createTray from './createTray';
|
||||||
|
@ -18,14 +17,13 @@ import installDevtools from './installDevtools';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
||||||
|
import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac } from './sync/sync.js';
|
||||||
|
|
||||||
const { download } = require('electron-dl');
|
const { download } = require('electron-dl');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const remote = require('@electron/remote/main');
|
const remote = require('@electron/remote/main');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const sudo = require('sudo-prompt');
|
const sudo = require('sudo-prompt');
|
||||||
const probe = require('ffmpeg-probe');
|
|
||||||
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
|
|
||||||
|
|
||||||
remote.initialize();
|
remote.initialize();
|
||||||
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
||||||
|
@ -60,7 +58,6 @@ let rendererWindow;
|
||||||
|
|
||||||
let tray; // eslint-disable-line
|
let tray; // eslint-disable-line
|
||||||
let daemon;
|
let daemon;
|
||||||
let lbryFirst;
|
|
||||||
|
|
||||||
const appState = {};
|
const appState = {};
|
||||||
const PROTOCOL = 'lbry';
|
const PROTOCOL = 'lbry';
|
||||||
|
@ -117,51 +114,6 @@ const startDaemon = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let isLbryFirstRunning = false;
|
|
||||||
const startLbryFirst = async () => {
|
|
||||||
if (isLbryFirstRunning) {
|
|
||||||
console.log('LbryFirst already running');
|
|
||||||
handleLbryFirstLaunched();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('LbryFirst: Starting...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
lbryFirst = new LbryFirstInstance();
|
|
||||||
lbryFirst.on('exit', e => {
|
|
||||||
if (!isDev) {
|
|
||||||
lbryFirst = null;
|
|
||||||
isLbryFirstRunning = false;
|
|
||||||
if (!appState.isQuitting) {
|
|
||||||
dialog.showErrorBox(
|
|
||||||
'LbryFirst has Exited',
|
|
||||||
'The lbryFirst may have encountered an unexpected error, or another lbryFirst instance is already running. \n\n',
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log('LbryFirst: Failed to create new instance\n\n', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('LbryFirst: Running...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await lbryFirst.launch();
|
|
||||||
handleLbryFirstLaunched();
|
|
||||||
} catch (e) {
|
|
||||||
isLbryFirstRunning = false;
|
|
||||||
console.log('LbryFirst: Failed to start\n', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLbryFirstLaunched = () => {
|
|
||||||
isLbryFirstRunning = true;
|
|
||||||
rendererWindow.webContents.send('lbry-first-launched');
|
|
||||||
};
|
|
||||||
|
|
||||||
// When we are starting the app, ensure there are no other apps already running
|
// When we are starting the app, ensure there are no other apps already running
|
||||||
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
||||||
|
@ -273,10 +225,6 @@ app.on('will-quit', event => {
|
||||||
daemon.quit();
|
daemon.quit();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
if (lbryFirst) {
|
|
||||||
lbryFirst.quit();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rendererWindow) {
|
if (rendererWindow) {
|
||||||
tray.destroy();
|
tray.destroy();
|
||||||
|
@ -355,43 +303,6 @@ ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-file-details-from-path', async (event, path) => {
|
|
||||||
const isFfMp4 = (ffprobeResults) => {
|
|
||||||
return ffprobeResults &&
|
|
||||||
ffprobeResults.format &&
|
|
||||||
ffprobeResults.format.format_name &&
|
|
||||||
ffprobeResults.format.format_name.includes('mp4');
|
|
||||||
};
|
|
||||||
const folders = path.split(/[\\/]/);
|
|
||||||
const name = folders[folders.length - 1];
|
|
||||||
let duration = 0, size = 0, mimeType;
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(path);
|
|
||||||
let ffprobeResults;
|
|
||||||
try {
|
|
||||||
ffprobeResults = await probe(path);
|
|
||||||
duration = ffprobeResults.format.duration;
|
|
||||||
size = ffprobeResults.format.size;
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
let fileReadResult;
|
|
||||||
if (size < MAX_IPC_SEND_BUFFER_SIZE) {
|
|
||||||
try {
|
|
||||||
fileReadResult = await fs.promises.readFile(path);
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: use mmmagic to inspect file and get mime type
|
|
||||||
mimeType = isFfMp4(ffprobeResults) ? 'video/mp4' : mime.getType(name);
|
|
||||||
const fileData = {name, mime: mimeType || undefined, path, duration: duration, size, buffer: fileReadResult };
|
|
||||||
return fileData;
|
|
||||||
} catch (e) {
|
|
||||||
// no stat
|
|
||||||
return { error: 'no file' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('get-disk-space', async (event) => {
|
ipcMain.on('get-disk-space', async (event) => {
|
||||||
try {
|
try {
|
||||||
const { data_dir } = await Lbry.settings_get();
|
const { data_dir } = await Lbry.settings_get();
|
||||||
|
@ -416,6 +327,42 @@ ipcMain.on('get-disk-space', async (event) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync cryptography
|
||||||
|
ipcMain.on('get-salt-seed', () => {
|
||||||
|
const saltSeed = generateSaltSeed();
|
||||||
|
rendererWindow.webContents.send('got-salt-seed', saltSeed);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('get-secrets', (event, password, email, saltseed) => {
|
||||||
|
console.log('password, salt', password, email, saltseed);
|
||||||
|
const callback = (result) => {
|
||||||
|
console.log('callback result', result);
|
||||||
|
rendererWindow.webContents.send('got-secrets', result);
|
||||||
|
};
|
||||||
|
deriveSecrets(password, email, saltseed, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('invoke-get-secrets', (event, password, email, saltseed) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const callback = (err, result) => {
|
||||||
|
console.log('callback result', result);
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(result);
|
||||||
|
};
|
||||||
|
console.log('password, salt', password, email, saltseed);
|
||||||
|
deriveSecrets(password, email, saltseed, callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('invoke-get-salt-seed', (event) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const saltSeed = generateSaltSeed();
|
||||||
|
return resolve(saltSeed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('version-info-requested', () => {
|
ipcMain.on('version-info-requested', () => {
|
||||||
function formatRc(ver) {
|
function formatRc(ver) {
|
||||||
// Adds dash if needed to make RC suffix SemVer friendly
|
// Adds dash if needed to make RC suffix SemVer friendly
|
||||||
|
@ -492,15 +439,6 @@ ipcMain.on('version-info-requested', () => {
|
||||||
requestLatestRelease();
|
requestLatestRelease();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('launch-lbry-first', async () => {
|
|
||||||
try {
|
|
||||||
await startLbryFirst();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to start LbryFirst');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('uncaughtException', error => {
|
process.on('uncaughtException', error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
|
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
|
||||||
|
@ -667,3 +605,5 @@ ipcMain.on('upgrade', (event, installerPath) => {
|
||||||
});
|
});
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
12
electron/sync/package.json
Normal file
12
electron/sync/package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "testsync.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module"
|
||||||
|
}
|
87
electron/sync/sync.js
Normal file
87
electron/sync/sync.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export function generateSalt(email, seed) {
|
||||||
|
const hashInput = (email + ':' + seed).toString('utf8');
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(hashInput);
|
||||||
|
const hash_output = hash.digest('hex').toString('utf8');
|
||||||
|
return hash_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSaltSeed() {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHmac(serverWalletState, hmacKey) {
|
||||||
|
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkHmac(serverWalletState, hmacKey, hmac) {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveSecrets(rootPassword, email, saltSeed, callback) {
|
||||||
|
const encodedPassword = Buffer.from(rootPassword.normalize('NFKC'));
|
||||||
|
const encodedEmail = Buffer.from(email);
|
||||||
|
const SCRYPT_N = 1 << 20;
|
||||||
|
const SCRYPT_R = 8;
|
||||||
|
const SCRYPT_P = 1;
|
||||||
|
const KEY_LENGTH = 32;
|
||||||
|
const NUM_KEYS = 3;
|
||||||
|
const MAXMEM_MULTIPLIER = 256;
|
||||||
|
const DEFAULT_MAXMEM = MAXMEM_MULTIPLIER * SCRYPT_N * SCRYPT_R;
|
||||||
|
|
||||||
|
function getKeyParts(key) {
|
||||||
|
const providerKey = key.slice(0, KEY_LENGTH).toString('base64');
|
||||||
|
const hmacKey = key.slice(KEY_LENGTH, KEY_LENGTH * 2).toString('base64');
|
||||||
|
const dataKey = key.slice(KEY_LENGTH * 2).toString('base64');
|
||||||
|
return { providerKey, hmacKey, dataKey }; // Buffer aa bb cc 6c
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = generateSalt(encodedEmail, saltSeed);
|
||||||
|
|
||||||
|
const scryptCallback = (err, key) => {
|
||||||
|
if (err) {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err, getKeyParts(key));
|
||||||
|
};
|
||||||
|
|
||||||
|
crypto.scrypt(encodedPassword, salt, KEY_LENGTH * NUM_KEYS, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: DEFAULT_MAXMEM }, scryptCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function walletHmac(inputString, hmacKey) {
|
||||||
|
const res = crypto.createHmac('sha256', hmacKey)
|
||||||
|
.update(inputString.toString('utf8'))
|
||||||
|
.digest('hex');
|
||||||
|
console.log('hmac res', res)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesEncrypt(text, key) {
|
||||||
|
try {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
|
||||||
|
let encrypted = cipher.update(text);
|
||||||
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
|
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') };
|
||||||
|
} catch (e) {
|
||||||
|
return { error: `Wallet decrypt failed error: ${e.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesDecrypt(cipher, key) {
|
||||||
|
try {
|
||||||
|
let iv = Buffer.from(cipher.iv, 'hex');
|
||||||
|
let encryptedText = Buffer.from(cipher.encryptedData, 'hex');
|
||||||
|
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
|
||||||
|
let decrypted = decipher.update(encryptedText);
|
||||||
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
|
// handle errors here
|
||||||
|
return { result: decrypted.toString() };
|
||||||
|
} catch (e) {
|
||||||
|
return { error: `Wallet decrypt failed error: ${e.message}`};
|
||||||
|
}
|
||||||
|
}
|
70
electron/sync/testsync.js
Normal file
70
electron/sync/testsync.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import test from 'tape';
|
||||||
|
// import sync from '../sync.js';
|
||||||
|
import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac, aesEncrypt, aesDecrypt } from './sync.js';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs';
|
||||||
|
export default function doTest() {
|
||||||
|
test('Generate sync seed', (assert) => {
|
||||||
|
const seed = generateSaltSeed();
|
||||||
|
console.log('seed', seed);
|
||||||
|
assert.pass(seed.length === 64);
|
||||||
|
assert.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Generate salt', (assert) => {
|
||||||
|
const seed = '80b3aff252588c4097df8f79ad42c8a5cf8d7c9e5339efec8bdb8bc7c5cf25ca';
|
||||||
|
const email = 'example@example.com';
|
||||||
|
const salt = generateSalt(email, seed);
|
||||||
|
console.log('salt', salt);
|
||||||
|
const expected = 'be6074a96f3ce812ea51b95d48e4b10493e14f6a329df7c0816018c6239661fe';
|
||||||
|
const actual = salt;
|
||||||
|
|
||||||
|
assert.equal(actual, expected,
|
||||||
|
'salt is expected value.');
|
||||||
|
assert.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Derive Keys', (assert) => {
|
||||||
|
const seed = '80b3aff252588c4097df8f79ad42c8a5cf8d7c9e5339efec8bdb8bc7c5cf25ca';
|
||||||
|
const email = 'example@example.com';
|
||||||
|
|
||||||
|
const expectedHmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8='; //base64
|
||||||
|
const expectedProviderKey = 'HKo/J+x4Hsy2NkMvj2JB9RI0yrvEiB4QSA/NHPaT/cA=';
|
||||||
|
// add expectedDataKey to test
|
||||||
|
|
||||||
|
function cb(e, r) {
|
||||||
|
console.log('derive keys result:', r);
|
||||||
|
assert.equal(r.hmacKey, expectedHmacKey, 'hmac is expected value');
|
||||||
|
assert.equal(r.providerKey, expectedProviderKey, 'lbryid password is expected value');
|
||||||
|
assert.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveSecrets('pass', email, seed, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CreateHmac', (assert) => {
|
||||||
|
const hmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8=';
|
||||||
|
const sequence = 1;
|
||||||
|
const walletState = `zo4MTkyOjE2OjE68QlIU76+W91/v/F1tu8h+kGB0Ee`;
|
||||||
|
const expectedHmacHex = '9fe70ebdeaf85b3afe5ae42e52f946acc54ded0350acacdded821845217839d4';
|
||||||
|
|
||||||
|
const input_str = `${sequence}:${walletState}`;
|
||||||
|
const hmacHex = walletHmac(input_str, hmacKey);
|
||||||
|
assert.equal(hmacHex, expectedHmacHex);
|
||||||
|
assert.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Encrypt/Decrypt', (assert) => {
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
// const wrongKey = crypto.randomBytes(32); // todo: what tests should fail; how much error handling needed?
|
||||||
|
// test a handy json file
|
||||||
|
const input = fs.readFileSync('../../package.json', 'utf8');
|
||||||
|
const cipher = aesEncrypt(input, key);
|
||||||
|
console.log('cipher', cipher);
|
||||||
|
const output = aesDecrypt(cipher, key);
|
||||||
|
assert.equal(input, output.result);
|
||||||
|
assert.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doTest();
|
|
@ -1,184 +0,0 @@
|
||||||
// @flow
|
|
||||||
/*
|
|
||||||
LBRY FIRST does not work due to api changes
|
|
||||||
*/
|
|
||||||
import 'proxy-polyfill';
|
|
||||||
|
|
||||||
const CHECK_LBRYFIRST_STARTED_TRY_NUMBER = 200;
|
|
||||||
//
|
|
||||||
// Basic LBRYFIRST connection config
|
|
||||||
// Offers a proxy to call LBRYFIRST methods
|
|
||||||
|
|
||||||
//
|
|
||||||
const LbryFirst: LbryFirstTypes = {
|
|
||||||
isConnected: false,
|
|
||||||
connectPromise: null,
|
|
||||||
lbryFirstConnectionString: 'http://localhost:1337/rpc',
|
|
||||||
apiRequestHeaders: { 'Content-Type': 'application/json' },
|
|
||||||
|
|
||||||
// Allow overriding lbryFirst connection string (e.g. to `/api/proxy` for lbryweb)
|
|
||||||
setLbryFirstConnectionString: (value: string) => {
|
|
||||||
LbryFirst.lbryFirstConnectionString = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
setApiHeader: (key: string, value: string) => {
|
|
||||||
LbryFirst.apiRequestHeaders = Object.assign(LbryFirst.apiRequestHeaders, { [key]: value });
|
|
||||||
},
|
|
||||||
|
|
||||||
unsetApiHeader: key => {
|
|
||||||
Object.keys(LbryFirst.apiRequestHeaders).includes(key) &&
|
|
||||||
delete LbryFirst.apiRequestHeaders['key'];
|
|
||||||
},
|
|
||||||
// Allow overriding Lbry methods
|
|
||||||
overrides: {},
|
|
||||||
setOverride: (methodName, newMethod) => {
|
|
||||||
LbryFirst.overrides[methodName] = newMethod;
|
|
||||||
},
|
|
||||||
getApiRequestHeaders: () => LbryFirst.apiRequestHeaders,
|
|
||||||
|
|
||||||
// LbryFirst Methods
|
|
||||||
status: (params = {}) => lbryFirstCallWithResult('status', params),
|
|
||||||
stop: () => lbryFirstCallWithResult('stop', {}),
|
|
||||||
version: () => lbryFirstCallWithResult('version', {}),
|
|
||||||
|
|
||||||
// Upload to youtube
|
|
||||||
upload: (params: { title: string, description: string, file_path: ?string } = {}) => {
|
|
||||||
// Only upload when originally publishing for now
|
|
||||||
if (!params.file_path) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadParams: {
|
|
||||||
Title: string,
|
|
||||||
Description: string,
|
|
||||||
FilePath: string,
|
|
||||||
Category: string,
|
|
||||||
Keywords: string,
|
|
||||||
} = {
|
|
||||||
Title: params.title,
|
|
||||||
Description: params.description,
|
|
||||||
FilePath: params.file_path,
|
|
||||||
Category: '',
|
|
||||||
Keywords: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
return lbryFirstCallWithResult('youtube.Upload', uploadParams);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasYTAuth: (token: string) => {
|
|
||||||
const hasYTAuthParams = {};
|
|
||||||
hasYTAuthParams.AuthToken = token;
|
|
||||||
return lbryFirstCallWithResult('youtube.HasAuth', hasYTAuthParams);
|
|
||||||
},
|
|
||||||
|
|
||||||
ytSignup: () => {
|
|
||||||
const emptyParams = {};
|
|
||||||
return lbryFirstCallWithResult('youtube.Signup', emptyParams);
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: () => {
|
|
||||||
const emptyParams = {};
|
|
||||||
return lbryFirstCallWithResult('youtube.Remove', emptyParams);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Connect to lbry-first
|
|
||||||
connect: () => {
|
|
||||||
if (LbryFirst.connectPromise === null) {
|
|
||||||
LbryFirst.connectPromise = new Promise((resolve, reject) => {
|
|
||||||
let tryNum = 0;
|
|
||||||
// Check every half second to see if the lbryFirst is accepting connections
|
|
||||||
function checkLbryFirstStarted() {
|
|
||||||
tryNum += 1;
|
|
||||||
LbryFirst.status()
|
|
||||||
.then(resolve)
|
|
||||||
.catch(() => {
|
|
||||||
if (tryNum <= CHECK_LBRYFIRST_STARTED_TRY_NUMBER) {
|
|
||||||
setTimeout(checkLbryFirstStarted, tryNum < 50 ? 400 : 1000);
|
|
||||||
} else {
|
|
||||||
reject(new Error('Unable to connect to LBRY'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkLbryFirstStarted();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flow thinks this could be empty, but it will always return a promise
|
|
||||||
// $FlowFixMe
|
|
||||||
return LbryFirst.connectPromise;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function checkAndParse(response) {
|
|
||||||
if (response.status >= 200 && response.status < 300) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return response.json().then(json => {
|
|
||||||
let error;
|
|
||||||
if (json.error) {
|
|
||||||
const errorMessage = typeof json.error === 'object' ? json.error.message : json.error;
|
|
||||||
error = new Error(errorMessage);
|
|
||||||
} else {
|
|
||||||
error = new Error('Protocol error with unknown response signature');
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) {
|
|
||||||
const counter = new Date().getTime();
|
|
||||||
const paramsArray = [params];
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: LbryFirst.apiRequestHeaders,
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method,
|
|
||||||
params: paramsArray,
|
|
||||||
id: counter,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(LbryFirst.lbryFirstConnectionString, options)
|
|
||||||
.then(checkAndParse)
|
|
||||||
.then(response => {
|
|
||||||
const error = response.error || (response.result && response.result.error);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
return resolve(response.result);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lbryFirstCallWithResult(name: string, params: ?{} = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
apiCall(
|
|
||||||
name,
|
|
||||||
params,
|
|
||||||
result => {
|
|
||||||
resolve(result);
|
|
||||||
},
|
|
||||||
reject
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only for a fallback
|
|
||||||
// If there is a LbryFirst method that is being called by an app, it should be added to /flow-typed/LbryFirst.js
|
|
||||||
const lbryFirstProxy = new Proxy(LbryFirst, {
|
|
||||||
get(target: LbryFirstTypes, name: string) {
|
|
||||||
if (name in target) {
|
|
||||||
return target[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (params = {}) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
apiCall(name, params, resolve, reject);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default lbryFirstProxy;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Recsys from './recsys';
|
|
||||||
|
|
||||||
export default Recsys;
|
|
|
@ -1,257 +0,0 @@
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
|
|
||||||
import { v4 as Uuidv4 } from 'uuid';
|
|
||||||
import { parseURI } from 'util/lbryURI';
|
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
|
||||||
import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content';
|
|
||||||
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
|
|
||||||
import { history } from 'ui/store';
|
|
||||||
|
|
||||||
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
|
|
||||||
const recsysId = 'lighthouse-v0';
|
|
||||||
|
|
||||||
const getClaimIdsFromUris = (uris) => {
|
|
||||||
return uris
|
|
||||||
? uris.map((uri) => {
|
|
||||||
try {
|
|
||||||
const { claimId } = parseURI(uri);
|
|
||||||
return claimId;
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const recsys = {
|
|
||||||
entries: {},
|
|
||||||
debug: false,
|
|
||||||
/**
|
|
||||||
* Provides for creating, updating, and sending Clickstream data object Entries.
|
|
||||||
* Entries are Created either when recommendedContent loads, or when recommendedContent is clicked.
|
|
||||||
* If recommended content is clicked, An Entry with parentUuid is created.
|
|
||||||
* On page load, find an empty entry with your claimId, or create a new entry and record to it.
|
|
||||||
* The entry will be populated with the following:
|
|
||||||
* - parentUuid // optional
|
|
||||||
* - Uuid
|
|
||||||
* - claimId
|
|
||||||
* - recommendedClaims [] // optionally empty
|
|
||||||
* - playerEvents [] // optionally empty
|
|
||||||
* - recommendedClaimsIndexClicked [] // optionally empty
|
|
||||||
* - UserId
|
|
||||||
* - pageLoadedAt
|
|
||||||
* - isEmbed
|
|
||||||
* - pageExitedAt
|
|
||||||
* - recsysId // optional
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function: onClickedRecommended()
|
|
||||||
* Called when RecommendedContent was clicked.
|
|
||||||
* Adds index of clicked recommendation to parent entry
|
|
||||||
* Adds new Entry with parentUuid for destination page
|
|
||||||
* @param parentClaimId: string,
|
|
||||||
* @param newClaimId: string,
|
|
||||||
*/
|
|
||||||
onClickedRecommended: function (parentClaimId, newClaimId) {
|
|
||||||
const parentEntry = recsys.entries[parentClaimId] ? recsys.entries[parentClaimId] : null;
|
|
||||||
const parentUuid = parentEntry['uuid'];
|
|
||||||
const parentRecommendedClaims = parentEntry['recClaimIds'] || [];
|
|
||||||
const parentClickedIndexes = parentEntry['recClickedVideoIdx'] || [];
|
|
||||||
const indexClicked = parentRecommendedClaims.indexOf(newClaimId);
|
|
||||||
|
|
||||||
if (parentUuid) {
|
|
||||||
recsys.createRecsysEntry(newClaimId, parentUuid);
|
|
||||||
}
|
|
||||||
parentClickedIndexes.push(indexClicked);
|
|
||||||
recsys.log('onClickedRecommended', { parentClaimId, newClaimId });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page was loaded. Get or Create entry and populate it with default data, plus recommended content, recsysId, etc.
|
|
||||||
* Called from recommendedContent component
|
|
||||||
*/
|
|
||||||
onRecsLoaded: function (claimId, uris) {
|
|
||||||
if (window.store) {
|
|
||||||
const state = window.store.getState();
|
|
||||||
if (!recsys.entries[claimId]) {
|
|
||||||
recsys.createRecsysEntry(claimId);
|
|
||||||
}
|
|
||||||
const claimIds = getClaimIdsFromUris(uris);
|
|
||||||
recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId;
|
|
||||||
recsys.entries[claimId]['pageLoadedAt'] = Date.now();
|
|
||||||
recsys.entries[claimId]['recClaimIds'] = claimIds;
|
|
||||||
}
|
|
||||||
recsys.log('onRecsLoaded', claimId);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an Entry with optional parentUuid
|
|
||||||
* @param: claimId: string
|
|
||||||
* @param: parentUuid: string (optional)
|
|
||||||
*/
|
|
||||||
createRecsysEntry: function (claimId, parentUuid) {
|
|
||||||
if (window.store && claimId) {
|
|
||||||
const state = window.store.getState();
|
|
||||||
const user = selectUser(state);
|
|
||||||
const userId = user ? user.id : null;
|
|
||||||
if (parentUuid) {
|
|
||||||
// Make a stub entry that will be filled out on page load
|
|
||||||
recsys.entries[claimId] = {
|
|
||||||
uuid: Uuidv4(),
|
|
||||||
parentUuid: parentUuid,
|
|
||||||
uid: userId || null, // selectUser
|
|
||||||
claimId: claimId,
|
|
||||||
recClickedVideoIdx: [],
|
|
||||||
pageLoadedAt: Date.now(),
|
|
||||||
events: [],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
recsys.entries[claimId] = {
|
|
||||||
uuid: Uuidv4(),
|
|
||||||
uid: userId, // selectUser
|
|
||||||
claimId: claimId,
|
|
||||||
pageLoadedAt: Date.now(),
|
|
||||||
recsysId: null,
|
|
||||||
recClaimIds: [],
|
|
||||||
recClickedVideoIdx: [],
|
|
||||||
events: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recsys.log('createRecsysEntry', claimId);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send event for claimId
|
|
||||||
* @param claimId
|
|
||||||
* @param isTentative
|
|
||||||
*/
|
|
||||||
sendRecsysEntry: function (claimId, isTentative) {
|
|
||||||
const shareTelemetry =
|
|
||||||
IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data);
|
|
||||||
|
|
||||||
if (recsys.entries[claimId] && shareTelemetry) {
|
|
||||||
const data = JSON.stringify(recsys.entries[claimId]);
|
|
||||||
try {
|
|
||||||
navigator.sendBeacon(recsysEndpoint, data);
|
|
||||||
if (!isTentative) {
|
|
||||||
delete recsys.entries[claimId];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('no beacon for you', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recsys.log('sendRecsysEntry', claimId);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A player event fired. Get the Entry for the claimId, and add the events
|
|
||||||
* @param claimId
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
onRecsysPlayerEvent: function (claimId, event, isEmbedded) {
|
|
||||||
if (!recsys.entries[claimId]) {
|
|
||||||
recsys.createRecsysEntry(claimId);
|
|
||||||
// do something to show it's floating or autoplay
|
|
||||||
}
|
|
||||||
if (isEmbedded) {
|
|
||||||
recsys.entries[claimId]['isEmbed'] = true;
|
|
||||||
}
|
|
||||||
recsys.entries[claimId].events.push(event);
|
|
||||||
recsys.log('onRecsysPlayerEvent', claimId);
|
|
||||||
},
|
|
||||||
log: function (callName, claimId) {
|
|
||||||
if (recsys.debug) {
|
|
||||||
console.log(`Call: ***${callName}***, ClaimId: ${claimId}, Recsys Entries`, Object.assign({}, recsys.entries));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Player closed. Check to see if primaryUri = playingUri
|
|
||||||
* if so, send the Entry.
|
|
||||||
*/
|
|
||||||
onPlayerDispose: function (claimId, isEmbedded) {
|
|
||||||
if (window.store) {
|
|
||||||
const state = window.store.getState();
|
|
||||||
const playingUri = selectPlayingUri(state);
|
|
||||||
const primaryUri = selectPrimaryUri(state);
|
|
||||||
const onFilePage = playingUri === primaryUri;
|
|
||||||
if (!onFilePage || isEmbedded) {
|
|
||||||
if (isEmbedded) {
|
|
||||||
recsys.entries[claimId]['isEmbed'] = true;
|
|
||||||
}
|
|
||||||
recsys.sendRecsysEntry(claimId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recsys.log('PlayerDispose', claimId);
|
|
||||||
},
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * File page unmount or change event
|
|
||||||
// * Check to see if playingUri, floatingEnabled, primaryUri === playingUri
|
|
||||||
// * If not, send the Entry.
|
|
||||||
// * If floating enabled, leaving file page will pop out player, leading to
|
|
||||||
// * more events until player is disposed. Don't send unless floatingPlayer playingUri
|
|
||||||
// */
|
|
||||||
// onLeaveFilePage: function (primaryUri) {
|
|
||||||
// if (window.store) {
|
|
||||||
// const state = window.store.getState();
|
|
||||||
// const claim = makeSelectClaimForUri(primaryUri)(state);
|
|
||||||
// const claimId = claim ? claim.claim_id : null;
|
|
||||||
// const playingUri = selectPlayingUri(state);
|
|
||||||
// const actualPlayingUri = playingUri && playingUri.uri;
|
|
||||||
// // const primaryUri = selectPrimaryUri(state);
|
|
||||||
// const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state);
|
|
||||||
// // When leaving page, if floating player is enabled, play will continue.
|
|
||||||
// if (claimId) {
|
|
||||||
// recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
|
||||||
// }
|
|
||||||
// const shouldSend =
|
|
||||||
// (claimId && floatingPlayer && actualPlayingUri && actualPlayingUri !== primaryUri) || !floatingPlayer || !actualPlayingUri;
|
|
||||||
// if (shouldSend) {
|
|
||||||
// recsys.sendRecsysEntry(claimId);
|
|
||||||
// }
|
|
||||||
// recsys.log('LeaveFile', claimId);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate event
|
|
||||||
* Send all claimIds that aren't currently playing.
|
|
||||||
*/
|
|
||||||
onNavigate: function () {
|
|
||||||
if (window.store) {
|
|
||||||
const state = window.store.getState();
|
|
||||||
const playingUri = selectPlayingUri(state);
|
|
||||||
const actualPlayingUri = playingUri && playingUri.uri;
|
|
||||||
const claim = makeSelectClaimForUri(actualPlayingUri)(state);
|
|
||||||
const playingClaimId = claim ? claim.claim_id : null;
|
|
||||||
// const primaryUri = selectPrimaryUri(state);
|
|
||||||
const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state);
|
|
||||||
// When leaving page, if floating player is enabled, play will continue.
|
|
||||||
Object.keys(recsys.entries).forEach((claimId) => {
|
|
||||||
const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds;
|
|
||||||
if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) {
|
|
||||||
recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
|
||||||
recsys.sendRecsysEntry(claimId);
|
|
||||||
}
|
|
||||||
recsys.log('OnNavigate', claimId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// @if TARGET='web'
|
|
||||||
document.addEventListener('visibilitychange', function logData() {
|
|
||||||
if (document.visibilityState === 'hidden') {
|
|
||||||
Object.keys(recsys.entries).map((claimId) => recsys.sendRecsysEntry(claimId, true));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
history.listen(() => {
|
|
||||||
recsys.onNavigate();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default recsys;
|
|
10
flow-typed/file-data.js
vendored
10
flow-typed/file-data.js
vendored
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
declare type FileData = {
|
|
||||||
file?: Blob,
|
|
||||||
path: string,
|
|
||||||
duration?: number,
|
|
||||||
size?: number,
|
|
||||||
mimeType: string,
|
|
||||||
error?: string,
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lbry",
|
"name": "lbry",
|
||||||
"version": "0.53.9",
|
"version": "0.53.5",
|
||||||
"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.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lbry"
|
"lbry"
|
||||||
|
@ -51,7 +51,6 @@
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ffmpeg-probe": "^1.0.6",
|
|
||||||
"humanize-duration": "^3.27.0",
|
"humanize-duration": "^3.27.0",
|
||||||
"match-sorter": "^6.3.0",
|
"match-sorter": "^6.3.0",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
|
@ -115,7 +114,7 @@
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"dotenv-defaults": "^2.0.1",
|
"dotenv-defaults": "^2.0.1",
|
||||||
"dotenv-webpack": "^1.8.0",
|
"dotenv-webpack": "^1.8.0",
|
||||||
"electron": "17.2.0",
|
"electron": "17.0.0",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
|
@ -193,6 +192,7 @@
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"strip-markdown": "^3.0.3",
|
"strip-markdown": "^3.0.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
|
"tape": "^5.6.0",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"three-full": "^28.0.2",
|
"three-full": "^28.0.2",
|
||||||
"unist-util-visit": "^2.0.3",
|
"unist-util-visit": "^2.0.3",
|
||||||
|
@ -217,7 +217,7 @@
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.113.0",
|
"lbrynetDaemonVersion": "0.110.0",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -2318,9 +2318,29 @@
|
||||||
"Odysee Connect --[Section in Help Page]--": "Odysee Connect",
|
"Odysee Connect --[Section in Help Page]--": "Odysee Connect",
|
||||||
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
|
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
|
||||||
"Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.",
|
"Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.",
|
||||||
"Autoplay Next is on.": "Autoplay Next is on.",
|
"Creator Comment settings": "Creator Comment settings",
|
||||||
"This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.",
|
"Remote Sync Settings": "Remote Sync Settings",
|
||||||
|
"Wallet Sync": "Wallet Sync",
|
||||||
|
"Manage cross device sync for your wallet": "Manage cross device sync for your wallet",
|
||||||
|
"With your email and wallet encryption password, we will register you with sync. If your wallet is not currentlyencrypted, it will be after this. Bitch.": "With your email and wallet encryption password, we will register you with sync. If your wallet is not currentlyencrypted, it will be after this. Bitch.",
|
||||||
|
"Password Again": "Password Again",
|
||||||
|
"Enter Email": "Enter Email",
|
||||||
|
"Sign up": "Sign up",
|
||||||
|
"Let's get you set up. This will require you to encrypt your wallet and remember your password.": "Let's get you set up. This will require you to encrypt your wallet and remember your password.",
|
||||||
|
"Sign in to your sync account. Or %sign_up%.": "Sign in to your sync account. Or %sign_up%.",
|
||||||
|
"Sign up for a sync account. Or %sign_in%.": "Sign up for a sync account. Or %sign_in%.",
|
||||||
|
"Verifying": "Verifying",
|
||||||
|
"We have sent you an email to verify your account.": "We have sent you an email to verify your account.",
|
||||||
|
"Doing Math": "Doing Math",
|
||||||
|
"Hold on, doing some math.": "Hold on, doing some math.",
|
||||||
|
"You are signed in as %email%.": "You are signed in as %email%.",
|
||||||
|
"Sync Provider Url": "Sync Provider Url",
|
||||||
|
"Enter Password": "Enter Password",
|
||||||
|
"Enter a password.": "Enter a password.",
|
||||||
|
"Some sign-in relevant message.": "Some sign-in relevant message.",
|
||||||
|
"Hold on, doing some math. (Registering you)": "Hold on, doing some math. (Registering you)",
|
||||||
|
"Failed to view lbry://@rossmanngroup#a/Day-39-Gotham-City-Solutions#e, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@rossmanngroup#a/Day-39-Gotham-City-Solutions#e, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
||||||
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
||||||
"Your update is now pending. It will take a few minutes to appear for other users.": "Your update is now pending. It will take a few minutes to appear for other users.",
|
"%anonymous%": "%anonymous%",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
168
ui/analytics.js
168
ui/analytics.js
|
@ -1,11 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
/*
|
|
||||||
Removed Watchman (internal view tracking) code.
|
|
||||||
This file may eventually implement cantina
|
|
||||||
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
|
||||||
refer to ___ for removal commit.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||||
|
@ -21,6 +14,9 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
|
||||||
export const SHARE_INTERNAL = 'shareInternal';
|
export const SHARE_INTERNAL = 'shareInternal';
|
||||||
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
||||||
|
|
||||||
|
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
|
||||||
|
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
|
||||||
|
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
ElectronCookies.enable({
|
ElectronCookies.enable({
|
||||||
origin: 'https://lbry.tv',
|
origin: 'https://lbry.tv',
|
||||||
|
@ -55,7 +51,6 @@ type Analytics = {
|
||||||
) => Promise<any>,
|
) => Promise<any>,
|
||||||
emailProvidedEvent: () => void,
|
emailProvidedEvent: () => void,
|
||||||
emailVerifiedEvent: () => void,
|
emailVerifiedEvent: () => void,
|
||||||
rewardEligibleEvent: () => void,
|
|
||||||
startupEvent: () => void,
|
startupEvent: () => void,
|
||||||
purchaseEvent: (number) => void,
|
purchaseEvent: (number) => void,
|
||||||
readyEvent: (number) => void,
|
readyEvent: (number) => void,
|
||||||
|
@ -72,10 +67,114 @@ type LogPublishParams = {
|
||||||
let internalAnalyticsEnabled: boolean = false;
|
let internalAnalyticsEnabled: boolean = false;
|
||||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the mobile device type viewing the data
|
||||||
|
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
||||||
|
*
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function getDeviceType() {
|
||||||
|
return 'dsk';
|
||||||
|
}
|
||||||
|
// variables initialized for watchman
|
||||||
|
let amountOfBufferEvents = 0;
|
||||||
|
let amountOfBufferTimeInMS = 0;
|
||||||
|
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
||||||
|
let lastSentTime;
|
||||||
|
|
||||||
|
// calculate data for backend, send them, and reset buffer data for next interval
|
||||||
|
async function sendAndResetWatchmanData() {
|
||||||
|
if (!userId) {
|
||||||
|
return 'Can only be used with a user id';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoPlayer) {
|
||||||
|
return 'Video player not initialized';
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
||||||
|
lastSentTime = new Date();
|
||||||
|
|
||||||
|
let protocol;
|
||||||
|
if (videoType === 'application/x-mpegURL') {
|
||||||
|
protocol = 'hls';
|
||||||
|
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||||
|
// $FlowFixMe
|
||||||
|
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
||||||
|
} else {
|
||||||
|
protocol = 'stb';
|
||||||
|
}
|
||||||
|
|
||||||
|
// current position in video in MS
|
||||||
|
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
||||||
|
|
||||||
|
// get the duration marking the time in the video for relative position calculation
|
||||||
|
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
||||||
|
|
||||||
|
// build object for watchman backend
|
||||||
|
const objectToSend = {
|
||||||
|
rebuf_count: amountOfBufferEvents,
|
||||||
|
rebuf_duration: amountOfBufferTimeInMS,
|
||||||
|
url: claimUrl.replace('lbry://', ''),
|
||||||
|
device: getDeviceType(),
|
||||||
|
duration: timeSinceLastIntervalSend,
|
||||||
|
protocol,
|
||||||
|
player: playerPoweredBy,
|
||||||
|
user_id: userId.toString(),
|
||||||
|
position: Math.round(positionInVideo),
|
||||||
|
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||||
|
bitrate: bitrateAsBitsPerSecond,
|
||||||
|
bandwidth: undefined,
|
||||||
|
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||||
|
};
|
||||||
|
|
||||||
|
// post to watchman
|
||||||
|
await sendWatchmanData(objectToSend);
|
||||||
|
|
||||||
|
// reset buffer data
|
||||||
|
amountOfBufferEvents = 0;
|
||||||
|
amountOfBufferTimeInMS = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let watchmanInterval;
|
||||||
|
// clear watchman interval and mark it as null (when video paused)
|
||||||
|
function stopWatchmanInterval() {
|
||||||
|
clearInterval(watchmanInterval);
|
||||||
|
watchmanInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates the setInterval that will run send to watchman on recurring basis
|
||||||
|
function startWatchmanIntervalIfNotRunning() {
|
||||||
|
if (!watchmanInterval) {
|
||||||
|
// instantiate the first time to calculate duration from
|
||||||
|
lastSentTime = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// post data to the backend
|
||||||
|
async function sendWatchmanData(body) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||||
videoBufferEvent: async (claim, data) => {
|
videoBufferEvent: async (claim, data) => {
|
||||||
// stub
|
amountOfBufferEvents = amountOfBufferEvents + 1;
|
||||||
|
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
||||||
|
},
|
||||||
|
onDispose: () => {
|
||||||
|
stopWatchmanInterval();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Is told whether video is being started or paused, and adjusts interval accordingly
|
* Is told whether video is being started or paused, and adjusts interval accordingly
|
||||||
|
@ -83,9 +182,40 @@ const analytics: Analytics = {
|
||||||
* @param {object} passedPlayer - VideoJS Player object
|
* @param {object} passedPlayer - VideoJS Player object
|
||||||
*/
|
*/
|
||||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||||
// stub
|
let playerIsSeeking = false;
|
||||||
|
// have to use this because videojs pauses/unpauses during seek
|
||||||
|
// sometimes the seeking function isn't populated yet so check for it as well
|
||||||
|
if (passedPlayer && passedPlayer.seeking) {
|
||||||
|
playerIsSeeking = passedPlayer.seeking();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if being paused, and not seeking, send existing data and stop interval
|
||||||
|
if (!isPlaying && !playerIsSeeking) {
|
||||||
|
sendAndResetWatchmanData();
|
||||||
|
stopWatchmanInterval();
|
||||||
|
// if being told to pause, and seeking, send and restart interval
|
||||||
|
} else if (!isPlaying && playerIsSeeking) {
|
||||||
|
sendAndResetWatchmanData();
|
||||||
|
stopWatchmanInterval();
|
||||||
|
startWatchmanIntervalIfNotRunning();
|
||||||
|
// is being told to play, and seeking, don't do anything,
|
||||||
|
// assume it's been started already from pause
|
||||||
|
} else if (isPlaying && playerIsSeeking) {
|
||||||
|
// start but not a seek, assuming a start from paused content
|
||||||
|
} else if (isPlaying && !playerIsSeeking) {
|
||||||
|
startWatchmanIntervalIfNotRunning();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||||
|
// populate values for watchman when video starts
|
||||||
|
userId = passedUserId;
|
||||||
|
claimUrl = canonicalUrl;
|
||||||
|
playerPoweredBy = poweredBy;
|
||||||
|
|
||||||
|
videoType = passedPlayer.currentSource().type;
|
||||||
|
videoPlayer = passedPlayer;
|
||||||
|
bitrateAsBitsPerSecond = videoBitrate;
|
||||||
|
|
||||||
// sendPromMetric('time_to_start', duration);
|
// sendPromMetric('time_to_start', duration);
|
||||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||||
},
|
},
|
||||||
|
@ -224,9 +354,6 @@ const analytics: Analytics = {
|
||||||
emailVerifiedEvent: () => {
|
emailVerifiedEvent: () => {
|
||||||
sendMatomoEvent('Engagement', 'Email-Verified');
|
sendMatomoEvent('Engagement', 'Email-Verified');
|
||||||
},
|
},
|
||||||
rewardEligibleEvent: () => {
|
|
||||||
sendMatomoEvent('Engagement', 'Reward-Eligible');
|
|
||||||
},
|
|
||||||
openUrlEvent: (url: string) => {
|
openUrlEvent: (url: string) => {
|
||||||
sendMatomoEvent('Engagement', 'Open-Url', url);
|
sendMatomoEvent('Engagement', 'Open-Url', url);
|
||||||
},
|
},
|
||||||
|
@ -251,9 +378,24 @@ function sendMatomoEvent(category, action, name, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prometheus
|
||||||
|
// function sendPromMetric(name: string, value?: number) {
|
||||||
|
// if (IS_WEB) {
|
||||||
|
// let url = new URL(SDK_API_PATH + '/metric/ui');
|
||||||
|
// const params = { name: name, value: value ? value.toString() : '' };
|
||||||
|
// url.search = new URLSearchParams(params).toString();
|
||||||
|
// return fetch(url, { method: 'post' });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const MatomoInstance = new MatomoTracker({
|
const MatomoInstance = new MatomoTracker({
|
||||||
urlBase: MATOMO_URL,
|
urlBase: MATOMO_URL,
|
||||||
siteId: MATOMO_ID, // optional, default value: `1`
|
siteId: MATOMO_ID, // optional, default value: `1`
|
||||||
|
// heartBeat: { // optional, enabled by default
|
||||||
|
// active: true, // optional, default value: true
|
||||||
|
// seconds: 10 // optional, default value: `15
|
||||||
|
// },
|
||||||
|
// linkTracking: false // optional, default value: true
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors/sync';
|
import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors/sync';
|
||||||
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
|
|
||||||
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
|
|
||||||
import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims';
|
import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims';
|
||||||
import { selectMyChannelUrls, selectMyChannelClaimIds } from 'redux/selectors/claims';
|
import { selectMyChannelUrls, selectMyChannelClaimIds } from 'redux/selectors/claims';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
@ -25,7 +22,7 @@ import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings
|
||||||
import { doSyncLoop } from 'redux/actions/sync';
|
import { doSyncLoop } from 'redux/actions/sync';
|
||||||
import {
|
import {
|
||||||
doDownloadUpgradeRequested,
|
doDownloadUpgradeRequested,
|
||||||
doSignIn,
|
doSignIn, // huh
|
||||||
doGetAndPopulatePreferences,
|
doGetAndPopulatePreferences,
|
||||||
doSetActiveChannel,
|
doSetActiveChannel,
|
||||||
doSetIncognito,
|
doSetIncognito,
|
||||||
|
@ -34,8 +31,6 @@ import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/c
|
||||||
import App from './view';
|
import App from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
user: selectUser(state),
|
|
||||||
accessToken: selectAccessToken(state),
|
|
||||||
theme: selectThemePath(state),
|
theme: selectThemePath(state),
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
|
@ -43,8 +38,6 @@ const select = (state) => ({
|
||||||
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
|
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
|
||||||
isUpgradeAvailable: selectIsUpgradeAvailable(state),
|
isUpgradeAvailable: selectIsUpgradeAvailable(state),
|
||||||
syncError: selectGetSyncErrorMessage(state),
|
syncError: selectGetSyncErrorMessage(state),
|
||||||
rewards: selectUnclaimedRewards(state),
|
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
currentModal: selectModal(state),
|
currentModal: selectModal(state),
|
||||||
syncFatalError: selectSyncFatalError(state),
|
syncFatalError: selectSyncFatalError(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
@ -55,7 +48,6 @@ const select = (state) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
|
||||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||||
fetchCollectionListMine: () => dispatch(doFetchCollectionListMine()),
|
fetchCollectionListMine: () => dispatch(doFetchCollectionListMine()),
|
||||||
setLanguage: (language) => dispatch(doSetLanguage(language)),
|
setLanguage: (language) => dispatch(doSetLanguage(language)),
|
||||||
|
@ -64,7 +56,6 @@ const perform = (dispatch) => ({
|
||||||
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
|
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
|
||||||
getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()),
|
getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()),
|
||||||
syncLoop: (noInterval) => dispatch(doSyncLoop(noInterval)),
|
syncLoop: (noInterval) => dispatch(doSyncLoop(noInterval)),
|
||||||
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
|
|
||||||
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
|
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
|
||||||
setIncognito: () => dispatch(doSetIncognito()),
|
setIncognito: () => dispatch(doSetIncognito()),
|
||||||
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
|
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
|
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import analytics from 'analytics';
|
|
||||||
import Router from 'component/router/index';
|
import Router from 'component/router/index';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import { openContextMenu } from 'util/context-menu';
|
import { openContextMenu } from 'util/context-menu';
|
||||||
import useKonamiListener from 'util/enhanced-layout';
|
import useKonamiListener from 'util/enhanced-layout';
|
||||||
import FileRenderFloating from 'component/fileRenderFloating';
|
import FileRenderFloating from 'component/fileRenderFloating';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import usePrevious from 'effects/use-previous';
|
|
||||||
import REWARDS from 'rewards';
|
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import LANGUAGES from 'constants/languages';
|
import LANGUAGES from 'constants/languages';
|
||||||
import useZoom from 'effects/use-zoom';
|
import useZoom from 'effects/use-zoom';
|
||||||
|
@ -46,7 +42,6 @@ type Props = {
|
||||||
length: number,
|
length: number,
|
||||||
push: (string) => void,
|
push: (string) => void,
|
||||||
},
|
},
|
||||||
fetchAccessToken: () => void,
|
|
||||||
fetchChannelListMine: () => void,
|
fetchChannelListMine: () => void,
|
||||||
fetchCollectionListMine: () => void,
|
fetchCollectionListMine: () => void,
|
||||||
signIn: () => void,
|
signIn: () => void,
|
||||||
|
@ -82,27 +77,18 @@ type Props = {
|
||||||
function App(props: Props) {
|
function App(props: Props) {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
user,
|
|
||||||
fetchAccessToken,
|
|
||||||
fetchChannelListMine,
|
fetchChannelListMine,
|
||||||
fetchCollectionListMine,
|
fetchCollectionListMine,
|
||||||
signIn,
|
|
||||||
autoUpdateDownloaded,
|
autoUpdateDownloaded,
|
||||||
isUpgradeAvailable,
|
isUpgradeAvailable,
|
||||||
requestDownloadUpgrade,
|
requestDownloadUpgrade,
|
||||||
uploadCount,
|
uploadCount,
|
||||||
history,
|
history,
|
||||||
syncError,
|
|
||||||
language,
|
language,
|
||||||
languages,
|
languages,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
getWalletSyncPref,
|
getWalletSyncPref,
|
||||||
rewards,
|
|
||||||
setReferrer,
|
|
||||||
isAuthenticated,
|
|
||||||
syncLoop,
|
|
||||||
currentModal,
|
|
||||||
syncFatalError,
|
syncFatalError,
|
||||||
myChannelClaimIds,
|
myChannelClaimIds,
|
||||||
activeChannelId,
|
activeChannelId,
|
||||||
|
@ -117,38 +103,16 @@ function App(props: Props) {
|
||||||
|
|
||||||
const appRef = useRef();
|
const appRef = useRef();
|
||||||
const isEnhancedLayout = useKonamiListener();
|
const isEnhancedLayout = useKonamiListener();
|
||||||
const [hasSignedIn, setHasSignedIn] = useState(false);
|
|
||||||
const [readyForSync, setReadyForSync] = useState(false);
|
|
||||||
const [readyForPrefs, setReadyForPrefs] = useState(false);
|
|
||||||
const hasVerifiedEmail = user && Boolean(user.has_verified_email);
|
|
||||||
const isRewardApproved = user && user.is_reward_approved;
|
|
||||||
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
|
|
||||||
const previousRewardApproved = usePrevious(isRewardApproved);
|
|
||||||
const { pathname, search } = props.location;
|
|
||||||
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
||||||
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
||||||
// const [retryingSync, setRetryingSync] = useState(false);
|
|
||||||
const [langRenderKey, setLangRenderKey] = useState(0);
|
const [langRenderKey, setLangRenderKey] = useState(0);
|
||||||
const [sidebarOpen] = usePersistedState('sidebar', true);
|
const [sidebarOpen] = usePersistedState('sidebar', true);
|
||||||
const showUpgradeButton = (autoUpdateDownloaded || isUpgradeAvailable) && !upgradeNagClosed;
|
const showUpgradeButton = (autoUpdateDownloaded || isUpgradeAvailable) && !upgradeNagClosed;
|
||||||
// referral claiming
|
|
||||||
const referredRewardAvailable = rewards && rewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE);
|
|
||||||
const urlParams = new URLSearchParams(search);
|
|
||||||
const rawReferrerParam = urlParams.get('r');
|
|
||||||
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
|
||||||
const userId = user && user.id;
|
|
||||||
const useCustomScrollbar = !IS_MAC;
|
const useCustomScrollbar = !IS_MAC;
|
||||||
const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
|
const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
|
||||||
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
|
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
|
||||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||||
const hasActiveChannelClaim = activeChannelId !== undefined;
|
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||||
const isPersonalized = hasVerifiedEmail;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId) {
|
|
||||||
analytics.setUser(userId);
|
|
||||||
}
|
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uploadCount) return;
|
if (!uploadCount) return;
|
||||||
|
@ -188,23 +152,10 @@ function App(props: Props) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Enable ctrl +/- zooming on Desktop.
|
// Enable ctrl +/- zooming on Desktop.
|
||||||
// @if TARGET='app'
|
|
||||||
useZoom();
|
useZoom();
|
||||||
// @endif
|
|
||||||
|
|
||||||
// Enable 'Alt + Left/Right' for history navigation on Desktop.
|
// Enable 'Alt + Left/Right' for history navigation on Desktop.
|
||||||
// @if TARGET='app'
|
|
||||||
useHistoryNav(history);
|
useHistoryNav(history);
|
||||||
// @endif
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (referredRewardAvailable && sanitizedReferrerParam && isRewardApproved) {
|
|
||||||
setReferrer(sanitizedReferrerParam, true);
|
|
||||||
} else if (referredRewardAvailable && sanitizedReferrerParam) {
|
|
||||||
setReferrer(sanitizedReferrerParam, false);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [sanitizedReferrerParam, isRewardApproved, referredRewardAvailable]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current: wrapperElement } = appRef;
|
const { current: wrapperElement } = appRef;
|
||||||
|
@ -212,13 +163,9 @@ function App(props: Props) {
|
||||||
ReactModal.setAppElement(wrapperElement);
|
ReactModal.setAppElement(wrapperElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAccessToken();
|
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
fetchChannelListMine(); // This is fetched after a user is signed in on web
|
fetchChannelListMine(); // This is fetched after a user is signed in on web
|
||||||
fetchCollectionListMine();
|
fetchCollectionListMine();
|
||||||
// @endif
|
}, [appRef, fetchChannelListMine, fetchCollectionListMine]);
|
||||||
}, [appRef, fetchAccessToken, fetchChannelListMine, fetchCollectionListMine]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
|
@ -261,71 +208,20 @@ function App(props: Props) {
|
||||||
}, [shouldMigrateLanguage, setLanguage]);
|
}, [shouldMigrateLanguage, setLanguage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check that previousHasVerifiedEmail was not undefined instead of just not truthy
|
if (updatePreferences && getWalletSyncPref) {
|
||||||
// This ensures we don't fire the emailVerified event on the initial user fetch
|
getWalletSyncPref().then(() => updatePreferences());
|
||||||
if (previousHasVerifiedEmail === false && hasVerifiedEmail) {
|
|
||||||
analytics.emailVerifiedEvent();
|
|
||||||
}
|
}
|
||||||
}, [previousHasVerifiedEmail, hasVerifiedEmail, signIn]);
|
}, [updatePreferences, getWalletSyncPref]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (previousRewardApproved === false && isRewardApproved) {
|
|
||||||
analytics.rewardEligibleEvent();
|
|
||||||
}
|
|
||||||
}, [previousRewardApproved, isRewardApproved]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (updatePreferences && getWalletSyncPref && readyForPrefs) {
|
|
||||||
getWalletSyncPref()
|
|
||||||
.then(() => updatePreferences())
|
|
||||||
.then(() => {
|
|
||||||
setReadyForSync(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [updatePreferences, getWalletSyncPref, setReadyForSync, readyForPrefs, hasVerifiedEmail]);
|
|
||||||
|
|
||||||
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.
|
|
||||||
useEffect(() => {
|
|
||||||
// signInSyncPref is cleared after sharedState loop.
|
|
||||||
if (readyForSync && hasVerifiedEmail) {
|
|
||||||
// In case we are syncing.
|
|
||||||
syncLoop();
|
|
||||||
}
|
|
||||||
}, [readyForSync, hasVerifiedEmail, syncLoop]);
|
|
||||||
|
|
||||||
// We know someone is logging in or not when we get their user object
|
|
||||||
// We'll use this to determine when it's time to pull preferences
|
|
||||||
// This will no longer work if desktop users no longer get a user object from lbryinc
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
setReadyForPrefs(true);
|
|
||||||
}
|
|
||||||
}, [user, setReadyForPrefs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (syncError && isAuthenticated && !pathname.includes(PAGES.AUTH_WALLET_PASSWORD) && !currentModal) {
|
|
||||||
history.push(`/$/${PAGES.AUTH_WALLET_PASSWORD}?redirect=${pathname}`);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [syncError, pathname, isAuthenticated]);
|
|
||||||
|
|
||||||
// Keep this at the end to ensure initial setup effects are run first
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasSignedIn && hasVerifiedEmail) {
|
|
||||||
signIn();
|
|
||||||
setHasSignedIn(true);
|
|
||||||
}
|
|
||||||
}, [hasVerifiedEmail, signIn, hasSignedIn]);
|
|
||||||
|
|
||||||
// batch resolve subscriptions to be used by the sideNavigation component.
|
// batch resolve subscriptions to be used by the sideNavigation component.
|
||||||
// add it here so that it only resolves the first time, despite route changes.
|
// add it here so that it only resolves the first time, despite route changes.
|
||||||
// useLayoutEffect because it has to be executed before the sideNavigation component requests them
|
// useLayoutEffect because it has to be executed before the sideNavigation component requests them
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (sidebarOpen && isPersonalized && subscriptions && !resolvedSubscriptions) {
|
if (sidebarOpen && subscriptions && !resolvedSubscriptions) {
|
||||||
setResolvedSubscriptions(true);
|
setResolvedSubscriptions(true);
|
||||||
resolveUris(subscriptions.map((sub) => sub.uri));
|
resolveUris(subscriptions.map((sub) => sub.uri));
|
||||||
}
|
}
|
||||||
}, [sidebarOpen, isPersonalized, resolvedSubscriptions, subscriptions, resolveUris, setResolvedSubscriptions]);
|
}, [sidebarOpen, resolvedSubscriptions, subscriptions, resolveUris, setResolvedSubscriptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// When language is changed or translations are fetched, we render.
|
// When language is changed or translations are fetched, we render.
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import Button from './view';
|
import Button from './view';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
pathname: state.router.location.pathname,
|
pathname: state.router.location.pathname,
|
||||||
emailVerified: selectUserVerifiedEmail(state),
|
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedButton = connect(mapStateToProps)(Button);
|
const ConnectedButton = connect(mapStateToProps)(Button);
|
||||||
|
|
|
@ -32,11 +32,9 @@ type Props = {
|
||||||
onMouseEnter: ?(any) => any,
|
onMouseEnter: ?(any) => any,
|
||||||
onMouseLeave: ?(any) => any,
|
onMouseLeave: ?(any) => any,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
emailVerified: boolean,
|
|
||||||
myref: any,
|
myref: any,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
'aria-label'?: string,
|
'aria-label'?: string,
|
||||||
user: ?User,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// use forwardRef to allow consumers to pass refs to the button content if they want to
|
// use forwardRef to allow consumers to pass refs to the button content if they want to
|
||||||
|
@ -63,11 +61,9 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
iconSize,
|
iconSize,
|
||||||
iconColor,
|
iconColor,
|
||||||
activeClass,
|
activeClass,
|
||||||
emailVerified,
|
|
||||||
myref,
|
myref,
|
||||||
dispatch, // <button> doesn't know what to do with dispatch
|
dispatch, // <button> doesn't know what to do with dispatch
|
||||||
pathname,
|
pathname,
|
||||||
user,
|
|
||||||
authSrc,
|
authSrc,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectUserEmail } from 'redux/selectors/user';
|
|
||||||
import CardVerify from './view';
|
|
||||||
|
|
||||||
const select = state => ({
|
|
||||||
email: selectUserEmail(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = () => ({});
|
|
||||||
|
|
||||||
export default connect(select, perform)(CardVerify);
|
|
|
@ -1,185 +0,0 @@
|
||||||
/* eslint-disable no-undef */
|
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
|
|
||||||
let scriptLoading = false;
|
|
||||||
let scriptLoaded = false;
|
|
||||||
let scriptDidError = false;
|
|
||||||
|
|
||||||
// Flow does not like the way this stripe plugin works
|
|
||||||
// Disabled because it was a huge pain
|
|
||||||
// type Props = {
|
|
||||||
// disabled: boolean,
|
|
||||||
// label: ?string,
|
|
||||||
// email: string,
|
|
||||||
|
|
||||||
// // =====================================================
|
|
||||||
// // Required by stripe
|
|
||||||
// // see Stripe docs for more info:
|
|
||||||
// // https://stripe.com/docs/checkout#integration-custom
|
|
||||||
// // =====================================================
|
|
||||||
|
|
||||||
// // Your publishable key (test or live).
|
|
||||||
// // can't use "key" as a prop in react, so have to change the keyname
|
|
||||||
// stripeKey: string,
|
|
||||||
|
|
||||||
// // The callback to invoke when the Checkout process is complete.
|
|
||||||
// // function(token)
|
|
||||||
// // token is the token object created.
|
|
||||||
// // token.id can be used to create a charge or customer.
|
|
||||||
// // token.email contains the email address entered by the user.
|
|
||||||
// token: string,
|
|
||||||
// };
|
|
||||||
|
|
||||||
class CardVerify extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
open: false,
|
|
||||||
scriptFailedToLoad: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (scriptLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scriptLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptLoading = true;
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://checkout.stripe.com/checkout.js';
|
|
||||||
script.async = true;
|
|
||||||
|
|
||||||
this.loadPromise = (() => {
|
|
||||||
let canceled = false;
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
|
||||||
script.onload = () => {
|
|
||||||
scriptLoaded = true;
|
|
||||||
scriptLoading = false;
|
|
||||||
resolve();
|
|
||||||
this.onScriptLoaded();
|
|
||||||
};
|
|
||||||
script.onerror = event => {
|
|
||||||
scriptDidError = true;
|
|
||||||
scriptLoading = false;
|
|
||||||
reject(event);
|
|
||||||
this.onScriptError(event);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const wrappedPromise = new Promise((resolve, reject) => {
|
|
||||||
promise.then(() => (canceled ? reject({ isCanceled: true }) : resolve()));
|
|
||||||
promise.catch(error => (canceled ? reject({ isCanceled: true }) : reject(error)));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
promise: wrappedPromise,
|
|
||||||
reject() {
|
|
||||||
canceled = true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.loadPromise.promise.then(this.onScriptLoaded).catch(this.onScriptError);
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
document.body.appendChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (!scriptLoading) {
|
|
||||||
this.updateStripeHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.loadPromise) {
|
|
||||||
this.loadPromise.reject();
|
|
||||||
}
|
|
||||||
if (CardVerify.stripeHandler && this.state.open) {
|
|
||||||
CardVerify.stripeHandler.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScriptLoaded = () => {
|
|
||||||
if (!CardVerify.stripeHandler) {
|
|
||||||
CardVerify.stripeHandler = StripeCheckout.configure({
|
|
||||||
key: this.props.stripeKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.hasPendingClick) {
|
|
||||||
this.showStripeDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onScriptError = (...args) => {
|
|
||||||
this.setState({ scriptFailedToLoad: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClosed = () => {
|
|
||||||
this.setState({ open: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
updateStripeHandler() {
|
|
||||||
if (!CardVerify.stripeHandler) {
|
|
||||||
CardVerify.stripeHandler = StripeCheckout.configure({
|
|
||||||
key: this.props.stripeKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showStripeDialog() {
|
|
||||||
this.setState({ open: true });
|
|
||||||
CardVerify.stripeHandler.open({
|
|
||||||
allowRememberMe: false,
|
|
||||||
closed: this.onClosed,
|
|
||||||
description: __('Confirm Identity'),
|
|
||||||
email: this.props.email,
|
|
||||||
locale: 'auto',
|
|
||||||
panelLabel: 'Verify',
|
|
||||||
token: this.props.token,
|
|
||||||
zipCode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick = () => {
|
|
||||||
if (scriptDidError) {
|
|
||||||
try {
|
|
||||||
throw new Error('Tried to call onClick, but StripeCheckout failed to load');
|
|
||||||
} catch (x) {}
|
|
||||||
} else if (CardVerify.stripeHandler) {
|
|
||||||
this.showStripeDialog();
|
|
||||||
} else {
|
|
||||||
this.hasPendingClick = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { scriptFailedToLoad } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{scriptFailedToLoad && (
|
|
||||||
<div className="error__text">There was an error connecting to Stripe. Please try again later.</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={this.props.label}
|
|
||||||
disabled={this.props.disabled || this.state.open || this.hasPendingClick}
|
|
||||||
onClick={this.onClick.bind(this)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CardVerify;
|
|
||||||
/* eslint-enable no-undef */
|
|
||||||
/* eslint-enable react/prop-types */
|
|
|
@ -11,7 +11,6 @@ import { doResolveUris } from 'redux/actions/claims';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
|
|
||||||
import ChannelContent from './view';
|
import ChannelContent from './view';
|
||||||
|
@ -29,7 +28,6 @@ const select = (state, props) => {
|
||||||
channelIsMine: selectClaimIsMine(state, claim),
|
channelIsMine: selectClaimIsMine(state, claim),
|
||||||
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
claim,
|
claim,
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
showMature: selectShowMatureContent(state),
|
showMature: selectShowMatureContent(state),
|
||||||
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
|
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,6 @@ type Props = {
|
||||||
defaultPageSize?: number,
|
defaultPageSize?: number,
|
||||||
defaultInfiniteScroll?: Boolean,
|
defaultInfiniteScroll?: Boolean,
|
||||||
claim: Claim,
|
claim: Claim,
|
||||||
isAuthenticated: boolean,
|
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
viewHiddenChannels: boolean,
|
viewHiddenChannels: boolean,
|
||||||
|
|
|
@ -15,8 +15,6 @@ import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import { doUpdateChannel, doCreateChannel, doClearChannelErrors } from 'redux/actions/claims';
|
import { doUpdateChannel, doCreateChannel, doClearChannelErrors } from 'redux/actions/claims';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
|
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
|
||||||
import { doClaimInitialRewards } from 'redux/actions/rewards';
|
|
||||||
import { selectIsClaimingInitialRewards, selectHasClaimedInitialRewards } from 'redux/selectors/rewards';
|
|
||||||
import ChannelForm from './view';
|
import ChannelForm from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -36,8 +34,6 @@ const select = (state, props) => ({
|
||||||
createError: selectCreateChannelError(state),
|
createError: selectCreateChannelError(state),
|
||||||
creatingChannel: selectCreatingChannel(state),
|
creatingChannel: selectCreatingChannel(state),
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
|
||||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
@ -52,7 +48,6 @@ const perform = (dispatch) => ({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
clearChannelErrors: () => dispatch(doClearChannelErrors()),
|
clearChannelErrors: () => dispatch(doClearChannelErrors()),
|
||||||
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ChannelForm);
|
export default connect(select, perform)(ChannelForm);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import TagsSearch from 'component/tagsSearch';
|
import TagsSearch from 'component/tagsSearch';
|
||||||
import ErrorText from 'component/common/error-text';
|
import ErrorText from 'component/common/error-text';
|
||||||
|
@ -51,7 +51,6 @@ type Props = {
|
||||||
createError: string,
|
createError: string,
|
||||||
creatingChannel: boolean,
|
creatingChannel: boolean,
|
||||||
clearChannelErrors: () => void,
|
clearChannelErrors: () => void,
|
||||||
claimInitialRewards: () => void,
|
|
||||||
onDone: () => void,
|
onDone: () => void,
|
||||||
openModal: (
|
openModal: (
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -59,8 +58,6 @@ type Props = {
|
||||||
) => void,
|
) => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
isClaimingInitialRewards: boolean,
|
|
||||||
hasClaimedInitialRewards: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelForm(props: Props) {
|
function ChannelForm(props: Props) {
|
||||||
|
@ -85,11 +82,8 @@ function ChannelForm(props: Props) {
|
||||||
creatingChannel,
|
creatingChannel,
|
||||||
createError,
|
createError,
|
||||||
clearChannelErrors,
|
clearChannelErrors,
|
||||||
claimInitialRewards,
|
|
||||||
openModal,
|
openModal,
|
||||||
disabled,
|
disabled,
|
||||||
isClaimingInitialRewards,
|
|
||||||
hasClaimedInitialRewards,
|
|
||||||
} = props;
|
} = props;
|
||||||
const [nameError, setNameError] = React.useState(undefined);
|
const [nameError, setNameError] = React.useState(undefined);
|
||||||
const [bidError, setBidError] = React.useState('');
|
const [bidError, setBidError] = React.useState('');
|
||||||
|
@ -107,21 +101,11 @@ function ChannelForm(props: Props) {
|
||||||
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
||||||
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
||||||
const submitLabel = React.useMemo(() => {
|
const submitLabel = React.useMemo(() => {
|
||||||
if (isClaimingInitialRewards) {
|
|
||||||
return __('Claiming credits...');
|
|
||||||
}
|
|
||||||
return creatingChannel || updatingChannel ? __('Submitting...') : __('Submit');
|
return creatingChannel || updatingChannel ? __('Submitting...') : __('Submit');
|
||||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel]);
|
}, [creatingChannel, updatingChannel]);
|
||||||
const submitDisabled = React.useMemo(() => {
|
const submitDisabled = React.useMemo(() => {
|
||||||
return (
|
return creatingChannel || updatingChannel || coverError || bidError || (isNewChannel && !params.name);
|
||||||
isClaimingInitialRewards ||
|
}, [creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params.name]);
|
||||||
creatingChannel ||
|
|
||||||
updatingChannel ||
|
|
||||||
coverError ||
|
|
||||||
bidError ||
|
|
||||||
(isNewChannel && !params.name)
|
|
||||||
);
|
|
||||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params.name]);
|
|
||||||
|
|
||||||
function getChannelParams() {
|
function getChannelParams() {
|
||||||
// fill this in with sdk data
|
// fill this in with sdk data
|
||||||
|
@ -255,12 +239,6 @@ function ChannelForm(props: Props) {
|
||||||
clearChannelErrors();
|
clearChannelErrors();
|
||||||
}, [clearChannelErrors]);
|
}, [clearChannelErrors]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!hasClaimedInitialRewards) {
|
|
||||||
claimInitialRewards();
|
|
||||||
}
|
|
||||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
|
||||||
|
|
||||||
const coverSrc = coverError ? ThumbnailBrokenImage : coverPreview;
|
const coverSrc = coverError ? ThumbnailBrokenImage : coverPreview;
|
||||||
|
|
||||||
let thumbnailPreview;
|
let thumbnailPreview;
|
||||||
|
@ -376,7 +354,7 @@ function ChannelForm(props: Props) {
|
||||||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||||
maxLength={MAX_TITLE_LEN}
|
maxLength={MAX_TITLE_LEN}
|
||||||
/>
|
/>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
||||||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import { handleBidChange } from 'util/publish';
|
import { handleBidChange } from 'util/publish';
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||||
|
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
|
||||||
usePublishFormMode
|
usePublishFormMode
|
||||||
/>
|
/>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doClearPlayingUri } from 'redux/actions/content';
|
import { doClearPlayingUri } from 'redux/actions/content';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import {
|
import {
|
||||||
selectLinkedCommentAncestors,
|
selectLinkedCommentAncestors,
|
||||||
selectOthersReactsForComment,
|
selectOthersReactsForComment,
|
||||||
|
@ -33,7 +32,6 @@ const select = (state, props) => {
|
||||||
claim: makeSelectClaimForUri(uri)(state),
|
claim: makeSelectClaimForUri(uri)(state),
|
||||||
thumbnail: author_uri && selectThumbnailForUri(state, author_uri),
|
thumbnail: author_uri && selectThumbnailForUri(state, author_uri),
|
||||||
channelIsBlocked: author_uri && makeSelectChannelIsMuted(author_uri)(state),
|
channelIsBlocked: author_uri && makeSelectChannelIsMuted(author_uri)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
|
||||||
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
hasChannels: selectHasChannels(state),
|
hasChannels: selectHasChannels(state),
|
||||||
|
|
|
@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import CommentReactions from 'component/commentReactions';
|
import CommentReactions from 'component/commentReactions';
|
||||||
|
@ -49,7 +49,6 @@ type Props = {
|
||||||
linkedCommentId?: string,
|
linkedCommentId?: string,
|
||||||
linkedCommentAncestors: { [string]: Array<string> },
|
linkedCommentAncestors: { [string]: Array<string> },
|
||||||
hasChannels: boolean,
|
hasChannels: boolean,
|
||||||
commentingEnabled: boolean,
|
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
isTopLevel?: boolean,
|
isTopLevel?: boolean,
|
||||||
threadDepth: number,
|
threadDepth: number,
|
||||||
|
@ -82,7 +81,6 @@ function CommentView(props: Props) {
|
||||||
totalReplyPages,
|
totalReplyPages,
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
linkedCommentAncestors,
|
linkedCommentAncestors,
|
||||||
commentingEnabled,
|
|
||||||
hasChannels,
|
hasChannels,
|
||||||
doToast,
|
doToast,
|
||||||
isTopLevel,
|
isTopLevel,
|
||||||
|
@ -319,7 +317,7 @@ function CommentView(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
className="comment__edit-input"
|
className="comment__edit-input"
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
|
@ -368,7 +366,7 @@ function CommentView(props: Props) {
|
||||||
<div className="comment__actions">
|
<div className="comment__actions">
|
||||||
{threadDepth !== 0 && (
|
{threadDepth !== 0 && (
|
||||||
<Button
|
<Button
|
||||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
label={__('Reply')}
|
||||||
className="comment__action"
|
className="comment__action"
|
||||||
onClick={handleCommentReply}
|
onClick={handleCommentReply}
|
||||||
icon={ICONS.REPLY}
|
icon={ICONS.REPLY}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import SelectChannel from 'component/selectChannel';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isReply: boolean,
|
|
||||||
advancedHandler: () => void,
|
|
||||||
advanced: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CommentCreateHeader(props: Props) {
|
|
||||||
const { isReply, advancedHandler, advanced } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="comment-create__header">
|
|
||||||
<div className="comment-create__label-wrapper">
|
|
||||||
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
|
||||||
<SelectChannel tiny />
|
|
||||||
</div>
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
|
|
||||||
onClick={advancedHandler}
|
|
||||||
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
|
||||||
|
|
||||||
import { buildValidSticker } from 'util/comments';
|
import { buildValidSticker } from 'util/comments';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
@ -22,12 +22,13 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SelectChannel from 'component/selectChannel';
|
||||||
import StickerSelector from './sticker-selector';
|
import StickerSelector from './sticker-selector';
|
||||||
import CommentCreateHeader from './comment-create-header';
|
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
const stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
@ -363,6 +364,31 @@ export function CommentCreate(props: Props) {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||||
|
|
||||||
|
// LIVESTREAM ONLY - REMOVE
|
||||||
|
// Handle keyboard shortcut comment creation
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
|
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||||
|
//
|
||||||
|
// if (inputRef && inputRef.current === document.activeElement) {
|
||||||
|
// // $FlowFixMe
|
||||||
|
// const isTyping = e.target.attributes['term'];
|
||||||
|
//
|
||||||
|
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// buttonRef.current.click();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// window.addEventListener('keydown', altEnterListener);
|
||||||
|
//
|
||||||
|
// // removes the listener so it doesn't cause problems elsewhere in the app
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener('keydown', altEnterListener);
|
||||||
|
// };
|
||||||
|
// }, [isLivestream]);
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Render
|
// Render
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -384,11 +410,7 @@ export function CommentCreate(props: Props) {
|
||||||
push(pathPlusRedirect);
|
push(pathPlusRedirect);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormFieldAreaAdvanced
|
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||||
type="textarea"
|
|
||||||
name={'comment_signup_prompt'}
|
|
||||||
placeholder={__('Say something about this...')}
|
|
||||||
/>
|
|
||||||
<div className="section__actions--no-margin">
|
<div className="section__actions--no-margin">
|
||||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -399,22 +421,22 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
className={classnames('comment-create', {
|
className={classnames('commentCreate', {
|
||||||
'comment-create--reply': isReply,
|
'commentCreate--reply': isReply,
|
||||||
'comment-create--nestedReply': isNested,
|
'commentCreate--nestedReply': isNested,
|
||||||
'comment-create--bottom': bottom,
|
'commentCreate--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Input Box/Preview Box */}
|
{/* Input Box/Preview Box */}
|
||||||
{stickerSelector ? (
|
{stickerSelector ? (
|
||||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||||
<div className="comment-create__stickerPreview">
|
<div className="commentCreate__stickerPreview">
|
||||||
<div className="comment-create__stickerPreviewInfo">
|
<div className="commentCreate__stickerPreviewInfo">
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
</div>
|
</div>
|
||||||
<div className="comment-create__stickerPreviewImage">
|
<div className="commentCreate__stickerPreviewImage">
|
||||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
{/* figure out lbc sticker prices */}
|
{/* figure out lbc sticker prices */}
|
||||||
|
@ -426,15 +448,15 @@ export function CommentCreate(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
<div className="comment-create__supportCommentPreview">
|
<div className="commentCreate__supportCommentPreview">
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
className="comment-create__supportCommentPreviewAmount"
|
className="commentCreate__supportCommentPreviewAmount"
|
||||||
isFiat={activeTab === TAB_FIAT}
|
isFiat={activeTab === TAB_FIAT}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
/>
|
/>
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div className="comment-create__supportCommentBody">
|
<div className="commentCreate__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -449,22 +471,23 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
header={
|
label={
|
||||||
<CommentCreateHeader
|
<div className="commentCreate__labelWrapper">
|
||||||
isReply={isReply}
|
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||||
advanced={advancedEditor}
|
<SelectChannel tiny />
|
||||||
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
</div>
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : 'content_description'}
|
||||||
|
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
|
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={onTextareaBlur}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
|
@ -632,7 +655,7 @@ export function CommentCreate(props: Props) {
|
||||||
{/* Help Text */}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{!!minAmount && (
|
{!!minAmount && (
|
||||||
<div className="help--notice comment-create__minAmountNotice">
|
<div className="help--notice commentCreate__minAmountNotice">
|
||||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
|
|
|
@ -23,9 +23,6 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { uri } = props;
|
const { uri } = props;
|
||||||
|
@ -59,19 +56,15 @@ const select = (state, props) => {
|
||||||
myReactsByCommentId: selectMyReacts(state),
|
myReactsByCommentId: selectMyReacts(state),
|
||||||
othersReactsById: selectOthersReacts(state),
|
othersReactsById: selectOthersReacts(state),
|
||||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
|
|
||||||
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = {
|
||||||
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
|
fetchTopLevelComments: doCommentList,
|
||||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
fetchComment: doCommentById,
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
fetchReacts: doCommentReactList,
|
||||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
resetComments: doCommentReset,
|
||||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
doResolveUris,
|
||||||
doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)),
|
};
|
||||||
setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||||
import { ENABLE_COMMENT_REACTIONS, COMMENT_SERVER_API, COMMENT_SERVER_NAME } from 'config';
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import { getCommentsListTitle } from 'util/comments';
|
import { getCommentsListTitle } from 'util/comments';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
@ -15,8 +15,6 @@ import Empty from 'component/common/empty';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { FormField } from 'component/common/form';
|
|
||||||
import Comments from 'comments';
|
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
|
@ -54,9 +52,6 @@ type Props = {
|
||||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||||
resetComments: (claimId: string) => void,
|
resetComments: (claimId: string) => void,
|
||||||
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||||
customCommentServers: Array<CommentServerDetails>,
|
|
||||||
setCommentServer: (string) => void,
|
|
||||||
commentServer: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CommentList(props: Props) {
|
export default function CommentList(props: Props) {
|
||||||
|
@ -85,17 +80,11 @@ export default function CommentList(props: Props) {
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
customCommentServers,
|
|
||||||
setCommentServer,
|
|
||||||
commentServer,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
const isMediumScreen = useIsMediumScreen();
|
||||||
|
|
||||||
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
|
|
||||||
const allServers = [defaultServer, ...(customCommentServers || [])];
|
|
||||||
|
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||||
|
@ -266,16 +255,7 @@ export default function CommentList(props: Props) {
|
||||||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||||
|
|
||||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||||
const actionButtonsProps = {
|
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||||
totalComments,
|
|
||||||
sort,
|
|
||||||
changeSort,
|
|
||||||
setPage,
|
|
||||||
allServers,
|
|
||||||
commentServer,
|
|
||||||
defaultServer,
|
|
||||||
setCommentServer,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -354,21 +334,17 @@ type ActionButtonsProps = {
|
||||||
sort: string,
|
sort: string,
|
||||||
changeSort: (string) => void,
|
changeSort: (string) => void,
|
||||||
setPage: (number) => void,
|
setPage: (number) => void,
|
||||||
allServers: Array<CommentServerDetails>,
|
|
||||||
commentServer: string,
|
|
||||||
setCommentServer: (string) => void,
|
|
||||||
defaultServer: CommentServerDetails,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } =
|
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
||||||
actionButtonsProps;
|
|
||||||
const sortButtonProps = { activeSort: sort, changeSort };
|
const sortButtonProps = { activeSort: sort, changeSort };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'comment__actions-row'}>
|
<>
|
||||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||||
<div className="comment__sort-group">
|
<span className="comment__sort">
|
||||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||||
<SortButton
|
<SortButton
|
||||||
{...sortButtonProps}
|
{...sortButtonProps}
|
||||||
|
@ -377,39 +353,11 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
sortOption={SORT_BY.CONTROVERSY}
|
sortOption={SORT_BY.CONTROVERSY}
|
||||||
/>
|
/>
|
||||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allServers.length >= 2 && (
|
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||||
<div className="button__selected-server">
|
</>
|
||||||
<FormField
|
|
||||||
type="select-tiny"
|
|
||||||
onChange={function (x) {
|
|
||||||
const selectedServer = x.target.value;
|
|
||||||
setPage(0);
|
|
||||||
setCommentServer(selectedServer);
|
|
||||||
if (selectedServer === defaultServer.url) {
|
|
||||||
Comments.setServerUrl(undefined);
|
|
||||||
} else {
|
|
||||||
Comments.setServerUrl(selectedServer);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={commentServer}
|
|
||||||
>
|
|
||||||
{allServers.map(function (server) {
|
|
||||||
return (
|
|
||||||
<option key={server.url} value={server.url}>
|
|
||||||
{server.name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="button_refresh">
|
|
||||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
// @flow
|
|
||||||
import 'easymde/dist/easymde.min.css';
|
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOMServer from 'react-dom/server';
|
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
|
||||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
|
||||||
import type { ElementRef, Node } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
autoFocus?: boolean,
|
|
||||||
blockWrap: boolean,
|
|
||||||
charCount?: number,
|
|
||||||
children?: React$Node,
|
|
||||||
disabled?: boolean,
|
|
||||||
helper?: string | React$Node,
|
|
||||||
hideSuggestions?: boolean,
|
|
||||||
isLivestream?: boolean,
|
|
||||||
label?: string | Node,
|
|
||||||
labelOnLeft: boolean,
|
|
||||||
name: string,
|
|
||||||
noEmojis?: boolean,
|
|
||||||
placeholder?: string | number,
|
|
||||||
quickActionLabel?: string,
|
|
||||||
textAreaMaxLength?: number,
|
|
||||||
type?: string,
|
|
||||||
value?: string | number,
|
|
||||||
onChange?: (any) => any,
|
|
||||||
openEmoteMenu?: () => void,
|
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
render?: () => React$Node,
|
|
||||||
header?: React$Node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
|
|
||||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
|
||||||
|
|
||||||
input: { current: ElementRef<any> };
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.input = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { autoFocus } = this.props;
|
|
||||||
const input = this.input.current;
|
|
||||||
|
|
||||||
if (input && autoFocus) input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
autoFocus,
|
|
||||||
blockWrap,
|
|
||||||
charCount,
|
|
||||||
children,
|
|
||||||
helper,
|
|
||||||
hideSuggestions,
|
|
||||||
isLivestream,
|
|
||||||
label,
|
|
||||||
header,
|
|
||||||
labelOnLeft,
|
|
||||||
name,
|
|
||||||
noEmojis,
|
|
||||||
quickActionLabel,
|
|
||||||
textAreaMaxLength,
|
|
||||||
type,
|
|
||||||
openEmoteMenu,
|
|
||||||
quickActionHandler,
|
|
||||||
render,
|
|
||||||
...inputProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Ideally, the character count should (and can) be appended to the
|
|
||||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
|
||||||
// to pass the current value to it's callback, nor query the current
|
|
||||||
// text length from the callback. So, we'll use our own widget.
|
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const quickAction =
|
|
||||||
quickActionLabel && quickActionHandler ? (
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const input = () => {
|
|
||||||
switch (type) {
|
|
||||||
case 'markdown':
|
|
||||||
const handleEvents = { contextmenu: openEditorMenu };
|
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
|
||||||
// SimpleMDE max char check
|
|
||||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
|
||||||
if (textAreaMaxLength && changes.update) {
|
|
||||||
var str = changes.text.join('\n');
|
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
|
||||||
|
|
||||||
if (delta <= 0) return;
|
|
||||||
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
|
||||||
if (delta > 0) {
|
|
||||||
str = str.substring(0, str.length - delta);
|
|
||||||
changes.update(changes.from, changes.to, str.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
|
||||||
editor.codemirror.on('changes', (instance, changes) => {
|
|
||||||
try {
|
|
||||||
// Grab the last change from the buffered list. I assume the
|
|
||||||
// buffered one ('changes', instead of 'change') is more efficient,
|
|
||||||
// and that "Create Link" will always end up last in the list.
|
|
||||||
const lastChange = changes[changes.length - 1];
|
|
||||||
if (lastChange.origin === '+input') {
|
|
||||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
|
||||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
|
||||||
|
|
||||||
// The URL placeholder is always placed last, so just look at the
|
|
||||||
// last text in the array to also cover the multi-line case:
|
|
||||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
|
||||||
|
|
||||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
|
||||||
const from = lastChange.from;
|
|
||||||
const to = lastChange.to;
|
|
||||||
const isSelectionMultiline = lastChange.text.length > 1;
|
|
||||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
|
||||||
|
|
||||||
// Everything works fine for the [Ctrl-K] case, but for the
|
|
||||||
// [Button] case, this handler happens before the original
|
|
||||||
// code, thus our change got wiped out.
|
|
||||||
// Add a small delay to handle that case.
|
|
||||||
setTimeout(() => {
|
|
||||||
instance.setSelection(
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
|
||||||
);
|
|
||||||
}, 25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {} // Do nothing (revert to original behavior)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
|
||||||
<fieldset-section>
|
|
||||||
{!header && (
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
|
||||||
<SimpleMDE
|
|
||||||
{...inputProps}
|
|
||||||
id={name}
|
|
||||||
type="textarea"
|
|
||||||
events={handleEvents}
|
|
||||||
getMdeInstance={getInstance}
|
|
||||||
options={{
|
|
||||||
spellChecker: true,
|
|
||||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
||||||
previewRender(plainText) {
|
|
||||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
|
||||||
return ReactDOMServer.renderToString(preview);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{countInfo}
|
|
||||||
</fieldset-section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<fieldset-section>
|
|
||||||
{!header && (label || quickAction) && (
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
|
||||||
{hideSuggestions ? (
|
|
||||||
<textarea
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
ref={this.input}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextareaWithSuggestions
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
inputRef={this.input}
|
|
||||||
isLivestream={isLivestream}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="form-field__textarea-info">
|
|
||||||
{!noEmojis && openEmoteMenu && (
|
|
||||||
<Button
|
|
||||||
type="alt"
|
|
||||||
className="button--comment-icons"
|
|
||||||
title="Emotes"
|
|
||||||
onClick={openEmoteMenu}
|
|
||||||
icon={ICONS.EMOJI}
|
|
||||||
iconSize={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{countInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{type && input()}
|
|
||||||
{helper && <div className="form-field__help">{helper}</div>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FormFieldAreaAdvanced;
|
|
|
@ -1,7 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||||
import type { ElementRef, Node } from 'react';
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -14,15 +21,19 @@ type Props = {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
helper?: string | React$Node,
|
helper?: string | React$Node,
|
||||||
|
hideSuggestions?: boolean,
|
||||||
inputButton?: React$Node,
|
inputButton?: React$Node,
|
||||||
|
isLivestream?: boolean,
|
||||||
label?: string | Node,
|
label?: string | Node,
|
||||||
labelOnLeft: boolean,
|
labelOnLeft: boolean,
|
||||||
max?: number,
|
max?: number,
|
||||||
min?: number,
|
min?: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
noEmojis?: boolean,
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
|
quickActionLabel?: string,
|
||||||
range?: number,
|
range?: number,
|
||||||
readOnly?: boolean,
|
readOnly?: boolean,
|
||||||
stretch?: boolean,
|
stretch?: boolean,
|
||||||
|
@ -30,6 +41,8 @@ type Props = {
|
||||||
type?: string,
|
type?: string,
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
onChange?: (any) => any,
|
onChange?: (any) => any,
|
||||||
|
openEmoteMenu?: () => void,
|
||||||
|
quickActionHandler?: (any) => any,
|
||||||
render?: () => React$Node,
|
render?: () => React$Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,15 +72,21 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
children,
|
children,
|
||||||
error,
|
error,
|
||||||
helper,
|
helper,
|
||||||
|
hideSuggestions,
|
||||||
inputButton,
|
inputButton,
|
||||||
|
isLivestream,
|
||||||
label,
|
label,
|
||||||
labelOnLeft,
|
labelOnLeft,
|
||||||
name,
|
name,
|
||||||
|
noEmojis,
|
||||||
postfix,
|
postfix,
|
||||||
prefix,
|
prefix,
|
||||||
|
quickActionLabel,
|
||||||
stretch,
|
stretch,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
type,
|
type,
|
||||||
|
openEmoteMenu,
|
||||||
|
quickActionHandler,
|
||||||
render,
|
render,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -82,10 +101,18 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Wrapper = blockWrap
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
|
||||||
|
const quickAction =
|
||||||
|
quickActionLabel && quickActionHandler ? (
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const inputSimple = (type: string) => (
|
const inputSimple = (type: string) => (
|
||||||
<>
|
<>
|
||||||
<input id={name} type={type} {...inputProps} />
|
<input id={name} type={type} {...inputProps} />
|
||||||
|
@ -116,22 +143,133 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
return inputSelect('');
|
return inputSelect('');
|
||||||
case 'select-tiny':
|
case 'select-tiny':
|
||||||
return inputSelect('select--slim');
|
return inputSelect('select--slim');
|
||||||
|
case 'markdown':
|
||||||
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
|
const getInstance = (editor) => {
|
||||||
|
// SimpleMDE max char check
|
||||||
|
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||||
|
if (textAreaMaxLength && changes.update) {
|
||||||
|
var str = changes.text.join('\n');
|
||||||
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
|
|
||||||
|
if (delta <= 0) return;
|
||||||
|
|
||||||
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
|
if (delta > 0) {
|
||||||
|
str = str.substring(0, str.length - delta);
|
||||||
|
changes.update(changes.from, changes.to, str.split('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||||
|
editor.codemirror.on('changes', (instance, changes) => {
|
||||||
|
try {
|
||||||
|
// Grab the last change from the buffered list. I assume the
|
||||||
|
// buffered one ('changes', instead of 'change') is more efficient,
|
||||||
|
// and that "Create Link" will always end up last in the list.
|
||||||
|
const lastChange = changes[changes.length - 1];
|
||||||
|
if (lastChange.origin === '+input') {
|
||||||
|
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||||
|
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||||
|
|
||||||
|
// The URL placeholder is always placed last, so just look at the
|
||||||
|
// last text in the array to also cover the multi-line case:
|
||||||
|
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||||
|
|
||||||
|
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||||
|
const from = lastChange.from;
|
||||||
|
const to = lastChange.to;
|
||||||
|
const isSelectionMultiline = lastChange.text.length > 1;
|
||||||
|
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||||
|
|
||||||
|
// Everything works fine for the [Ctrl-K] case, but for the
|
||||||
|
// [Button] case, this handler happens before the original
|
||||||
|
// code, thus our change got wiped out.
|
||||||
|
// Add a small delay to handle that case.
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.setSelection(
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||||
|
);
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
|
<fieldset-section>
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
</div>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
<SimpleMDE
|
||||||
|
{...inputProps}
|
||||||
|
id={name}
|
||||||
|
type="textarea"
|
||||||
|
events={handleEvents}
|
||||||
|
getMdeInstance={getInstance}
|
||||||
|
options={{
|
||||||
|
spellChecker: true,
|
||||||
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||||
|
previewRender(plainText) {
|
||||||
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||||
|
return ReactDOMServer.renderToString(preview);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countInfo}
|
||||||
|
</fieldset-section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{label && (
|
{(label || quickAction) && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{quickAction}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<textarea
|
|
||||||
type={type}
|
{hideSuggestions ? (
|
||||||
id={name}
|
<textarea
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
type={type}
|
||||||
ref={this.input}
|
id={name}
|
||||||
{...inputProps}
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
/>
|
ref={this.input}
|
||||||
<div className="form-field__textarea-info">{countInfo}</div>
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextareaWithSuggestions
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
inputRef={this.input}
|
||||||
|
isLivestream={isLivestream}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-field__textarea-info">
|
||||||
|
{!noEmojis && openEmoteMenu && (
|
||||||
|
<Button
|
||||||
|
type="alt"
|
||||||
|
className="button--comment-icons"
|
||||||
|
title="Emotes"
|
||||||
|
onClick={openEmoteMenu}
|
||||||
|
icon={ICONS.EMOJI}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{countInfo}
|
||||||
|
</div>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export { Form } from './form-components/form';
|
export { Form } from './form-components/form';
|
||||||
export { FormField } from './form-components/form-field';
|
export { FormField } from './form-components/form-field';
|
||||||
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
|
||||||
export { FormFieldPrice } from './form-components/form-field-price';
|
export { FormFieldPrice } from './form-components/form-field-price';
|
||||||
export { Submit } from './form-components/submit';
|
export { Submit } from './form-components/submit';
|
||||||
|
|
|
@ -2054,15 +2054,4 @@ export const icons = {
|
||||||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
|
||||||
<g>
|
|
||||||
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
|
||||||
<g>
|
|
||||||
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
|
||||||
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -157,31 +157,6 @@ export default function CreatorAnalytics(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <Card
|
|
||||||
iconColor
|
|
||||||
className="section"
|
|
||||||
title={<span>{__('%lbc_received% LBRY Credits Earned', { lbc_received: stats.AllLBCReceived })}</span>}
|
|
||||||
icon={ICONS.REWARDS}
|
|
||||||
subtitle={
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="card__data-subtitle">
|
|
||||||
<span>
|
|
||||||
{'+'}{' '}
|
|
||||||
{__('%lbc_received_changed% this week', {
|
|
||||||
lbc_received_changed: stats.LBCReceivedChange || 0,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{stats.LBCReceivedChange > 0 && <Icon icon={ICONS.TRENDING} iconColor="green" size={18} />}
|
|
||||||
</div>
|
|
||||||
<p className="help">
|
|
||||||
{__(
|
|
||||||
"Earnings may also include any LBC you've sent yourself or added as support. We are working on making this more accurate. Check your wallet page for the correct total balance."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{stats.VideoURITopNew ? (
|
{stats.VideoURITopNew ? (
|
||||||
<Card
|
<Card
|
||||||
className="section"
|
className="section"
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
|
||||||
import { selectEmailToVerify, selectUser } from 'redux/selectors/user';
|
|
||||||
import FirstRunEmailCollection from './view';
|
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
|
||||||
emailCollectionAcknowledged: makeSelectClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED)(state),
|
|
||||||
email: selectEmailToVerify(state),
|
|
||||||
user: selectUser(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = (dispatch) => () => ({
|
|
||||||
acknowledgeEmail: () => {
|
|
||||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(FirstRunEmailCollection);
|
|
|
@ -1,40 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import UserEmailNew from 'component/userEmailNew';
|
|
||||||
import UserEmailVerify from 'component/userEmailVerify';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
email: string,
|
|
||||||
emailCollectionAcknowledged: boolean,
|
|
||||||
user: ?{ has_verified_email: boolean },
|
|
||||||
completeFirstRun: () => void,
|
|
||||||
acknowledgeEmail: () => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
class FirstRunEmailCollection extends React.PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { completeFirstRun, email, user, emailCollectionAcknowledged, acknowledgeEmail } = this.props;
|
|
||||||
|
|
||||||
// this shouldn't happen
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelButton = <Button button="link" onClick={completeFirstRun} label={__('Not Now')} />;
|
|
||||||
if (user && !user.has_verified_email && !email) {
|
|
||||||
return <UserEmailNew cancelButton={cancelButton} />;
|
|
||||||
} else if (user && !user.has_verified_email) {
|
|
||||||
return <UserEmailVerify cancelButton={cancelButton} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to acknowledge here so users don't see an empty email screen in the first run banner
|
|
||||||
if (!emailCollectionAcknowledged) {
|
|
||||||
acknowledgeEmail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FirstRunEmailCollection;
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri } from 'redux/selectors/claims';
|
||||||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import { doOpenFileInFolder } from 'redux/actions/file';
|
import { doOpenFileInFolder } from 'redux/actions/file';
|
||||||
import FileDetails from './view';
|
import FileDetails from './view';
|
||||||
|
|
||||||
|
@ -10,7 +9,6 @@ const select = (state, props) => ({
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -10,7 +10,6 @@ type Props = {
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
openFolder: (string) => void,
|
openFolder: (string) => void,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
user: ?any,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileDetails extends PureComponent<Props> {
|
class FileDetails extends PureComponent<Props> {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { isURIEqual } from 'util/lbryURI';
|
import { isURIEqual } from 'util/lbryURI';
|
||||||
|
@ -132,7 +132,6 @@ export default function FileRenderFloating(props: Props) {
|
||||||
const playingUriSource = playingUri && playingUri.source;
|
const playingUriSource = playingUri && playingUri.source;
|
||||||
const isComment = playingUriSource === 'comment';
|
const isComment = playingUriSource === 'comment';
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
|
||||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||||
|
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
|
@ -344,8 +343,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
'content__viewer--floating': isFloating,
|
'content__viewer--floating': isFloating,
|
||||||
'content__viewer--inline': !isFloating,
|
'content__viewer--inline': !isFloating,
|
||||||
'content__viewer--secondary': isComment,
|
'content__viewer--secondary': isComment,
|
||||||
'content__viewer--theater-mode':
|
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
||||||
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
|
|
@ -9,7 +9,6 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
|
@ -64,7 +63,6 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||||
const isMediumScreen = useIsMediumScreen();
|
|
||||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||||
const containerRef = React.useRef<any>();
|
const containerRef = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -153,7 +151,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
'content__cover--theater-mode': videoTheaterMode,
|
||||||
'content__cover--text': isText,
|
'content__cover--text': isText,
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
||||||
import { makeSelectClaimWasPurchased } from 'redux/selectors/claims';
|
import { makeSelectClaimWasPurchased } from 'redux/selectors/claims';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
|
||||||
import { makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
|
@ -19,7 +18,6 @@ const select = (state, props) => ({
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(FileRenderInline));
|
export default withRouter(connect(select, perform)(FileRenderInline));
|
||||||
|
|
|
@ -11,23 +11,13 @@ type Props = {
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
streamingUrl?: string,
|
streamingUrl?: string,
|
||||||
triggerAnalyticsView: (string, number) => Promise<any>,
|
triggerAnalyticsView: (string, number) => Promise<any>,
|
||||||
claimRewards: () => void,
|
|
||||||
costInfo: any,
|
costInfo: any,
|
||||||
claimWasPurchased: boolean,
|
claimWasPurchased: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderInline(props: Props) {
|
export default function FileRenderInline(props: Props) {
|
||||||
const {
|
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, renderMode, costInfo, claimWasPurchased } =
|
||||||
isPlaying,
|
props;
|
||||||
fileInfo,
|
|
||||||
uri,
|
|
||||||
streamingUrl,
|
|
||||||
triggerAnalyticsView,
|
|
||||||
claimRewards,
|
|
||||||
renderMode,
|
|
||||||
costInfo,
|
|
||||||
claimWasPurchased,
|
|
||||||
} = props;
|
|
||||||
const [playTime, setPlayTime] = useState();
|
const [playTime, setPlayTime] = useState();
|
||||||
const isFree = !costInfo || (costInfo.cost !== undefined && costInfo.cost === 0);
|
const isFree = !costInfo || (costInfo.cost !== undefined && costInfo.cost === 0);
|
||||||
const isReadyToView = fileInfo && fileInfo.completed;
|
const isReadyToView = fileInfo && fileInfo.completed;
|
||||||
|
@ -55,11 +45,10 @@ export default function FileRenderInline(props: Props) {
|
||||||
const timeToStart = Date.now() - playTime;
|
const timeToStart = Date.now() - playTime;
|
||||||
|
|
||||||
triggerAnalyticsView(uri, timeToStart).then(() => {
|
triggerAnalyticsView(uri, timeToStart).then(() => {
|
||||||
claimRewards();
|
|
||||||
setPlayTime(null);
|
setPlayTime(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [setPlayTime, claimRewards, triggerAnalyticsView, isReadyToPlay, playTime, uri]);
|
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, playTime, uri]);
|
||||||
|
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -13,7 +13,7 @@ const select = (state, props) => {
|
||||||
if (claimUriBeingPlayed) {
|
if (claimUriBeingPlayed) {
|
||||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
||||||
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
|
isBeingPlayed = claim.claim_id === claimBeingPlayed.claim_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,29 +1,25 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user';
|
|
||||||
import { doSignOut } from 'redux/actions/app';
|
import { doSignOut } from 'redux/actions/app';
|
||||||
import { formatCredits } from 'util/format-credits';
|
import { formatCredits } from 'util/format-credits';
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
||||||
import { selectHasNavigated } from 'redux/selectors/app';
|
import { selectHasNavigated } from 'redux/selectors/app';
|
||||||
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
|
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
|
||||||
import { selectEmailToVerify, selectUser } from 'redux/selectors/user';
|
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { doLbrysyncRegister } from 'redux/actions/sync';
|
||||||
import Header from './view';
|
import Header from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
emailToVerify: selectEmailToVerify(state),
|
|
||||||
hasNavigated: selectHasNavigated(state),
|
hasNavigated: selectHasNavigated(state),
|
||||||
hideBalance: selectClientSetting(state, SETTINGS.HIDE_BALANCE),
|
hideBalance: selectClientSetting(state, SETTINGS.HIDE_BALANCE),
|
||||||
roundedBalance: formatCredits(selectTotalBalance(state), 2, true),
|
roundedBalance: formatCredits(selectTotalBalance(state), 2, true),
|
||||||
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
|
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
|
||||||
syncError: selectGetSyncErrorMessage(state),
|
syncError: selectGetSyncErrorMessage(state),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
clearEmailEntry: () => dispatch(doClearEmailEntry()),
|
lbrysyncRegister: (username, password) => dispatch(doLbrysyncRegister(username, password)),
|
||||||
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
|
|
||||||
signOut: () => dispatch(doSignOut()),
|
signOut: () => dispatch(doSignOut()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ type Props = {
|
||||||
simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile
|
simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile
|
||||||
},
|
},
|
||||||
balance: number,
|
balance: number,
|
||||||
emailToVerify?: string,
|
|
||||||
hasNavigated: boolean,
|
hasNavigated: boolean,
|
||||||
hideBalance: boolean,
|
hideBalance: boolean,
|
||||||
hideCancel: boolean,
|
hideCancel: boolean,
|
||||||
|
@ -43,8 +42,6 @@ type Props = {
|
||||||
roundedSpendableBalance: string,
|
roundedSpendableBalance: string,
|
||||||
sidebarOpen: boolean,
|
sidebarOpen: boolean,
|
||||||
syncError: ?string,
|
syncError: ?string,
|
||||||
clearEmailEntry: () => void,
|
|
||||||
clearPasswordEntry: () => void,
|
|
||||||
setSidebarOpen: (boolean) => void,
|
setSidebarOpen: (boolean) => void,
|
||||||
signOut: () => void,
|
signOut: () => void,
|
||||||
};
|
};
|
||||||
|
@ -54,7 +51,6 @@ const Header = (props: Props) => {
|
||||||
authHeader,
|
authHeader,
|
||||||
backout,
|
backout,
|
||||||
balance,
|
balance,
|
||||||
emailToVerify,
|
|
||||||
hideBalance,
|
hideBalance,
|
||||||
hideCancel,
|
hideCancel,
|
||||||
history,
|
history,
|
||||||
|
@ -63,8 +59,6 @@ const Header = (props: Props) => {
|
||||||
roundedSpendableBalance,
|
roundedSpendableBalance,
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
syncError,
|
syncError,
|
||||||
clearEmailEntry,
|
|
||||||
clearPasswordEntry,
|
|
||||||
setSidebarOpen,
|
setSidebarOpen,
|
||||||
signOut,
|
signOut,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -80,7 +74,6 @@ const Header = (props: Props) => {
|
||||||
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
|
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
|
||||||
const isVerifyPage = pathname.includes(PAGES.AUTH_VERIFY);
|
const isVerifyPage = pathname.includes(PAGES.AUTH_VERIFY);
|
||||||
const isSignUpPage = pathname.includes(PAGES.AUTH);
|
const isSignUpPage = pathname.includes(PAGES.AUTH);
|
||||||
const isSignInPage = pathname.includes(PAGES.AUTH_SIGNIN);
|
|
||||||
const isPwdResetPage = pathname.includes(PAGES.AUTH_PASSWORD_RESET);
|
const isPwdResetPage = pathname.includes(PAGES.AUTH_PASSWORD_RESET);
|
||||||
|
|
||||||
// For pages that allow for "backing out", shows a backout option instead of the Home logo
|
// For pages that allow for "backing out", shows a backout option instead of the Home logo
|
||||||
|
@ -236,12 +229,9 @@ const Header = (props: Props) => {
|
||||||
// className="button--header-close"
|
// className="button--header-close"
|
||||||
icon={ICONS.REMOVE}
|
icon={ICONS.REMOVE}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearEmailEntry();
|
|
||||||
clearPasswordEntry();
|
|
||||||
|
|
||||||
if (syncError) signOut();
|
if (syncError) signOut();
|
||||||
|
|
||||||
if ((isSignInPage && !emailToVerify) || isSignUpPage || isPwdResetPage) {
|
if (isSignUpPage || isPwdResetPage) {
|
||||||
goBack();
|
goBack();
|
||||||
} else {
|
} else {
|
||||||
push('/');
|
push('/');
|
||||||
|
|
|
@ -4,13 +4,11 @@ import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import HeaderMenuButtons from './view';
|
import HeaderMenuButtons from './view';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
||||||
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -14,14 +14,13 @@ import Tooltip from 'component/common/tooltip';
|
||||||
type HeaderMenuButtonProps = {
|
type HeaderMenuButtonProps = {
|
||||||
automaticDarkModeEnabled: boolean,
|
automaticDarkModeEnabled: boolean,
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
user: ?User,
|
|
||||||
handleThemeToggle: (boolean, string) => void,
|
handleThemeToggle: (boolean, string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
const { automaticDarkModeEnabled, currentTheme, user, handleThemeToggle } = props;
|
const { automaticDarkModeEnabled, currentTheme, handleThemeToggle } = props;
|
||||||
|
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header__buttons">
|
<div className="header__buttons">
|
||||||
|
@ -35,6 +34,7 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
<MenuList className="menu__list--header">
|
<MenuList className="menu__list--header">
|
||||||
<HeaderMenuLink page={PAGES.UPLOAD} icon={ICONS.PUBLISH} name={__('Upload')} />
|
<HeaderMenuLink page={PAGES.UPLOAD} icon={ICONS.PUBLISH} name={__('Upload')} />
|
||||||
<HeaderMenuLink page={PAGES.CHANNEL_NEW} icon={ICONS.CHANNEL} name={__('New Channel')} />
|
<HeaderMenuLink page={PAGES.CHANNEL_NEW} icon={ICONS.CHANNEL} name={__('New Channel')} />
|
||||||
|
<HeaderMenuLink page={PAGES.SETTINGS_SYNC} icon={ICONS.GAMING} name={__('Sign In')} />
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||||
import { doSeeAllNotifications } from 'redux/actions/notifications';
|
import { doSeeAllNotifications } from 'redux/actions/notifications';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import NotificationHeaderButton from './view';
|
import NotificationHeaderButton from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
unseenCount: selectUnseenNotificationCount(state),
|
unseenCount: selectUnseenNotificationCount(state),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
|
|
|
@ -13,15 +13,14 @@ import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
unseenCount: number,
|
unseenCount: number,
|
||||||
user: ?User,
|
|
||||||
doSeeAllNotifications: () => void,
|
doSeeAllNotifications: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NotificationHeaderButton(props: Props) {
|
export default function NotificationHeaderButton(props: Props) {
|
||||||
const { unseenCount, user, doSeeAllNotifications } = props;
|
const { unseenCount, doSeeAllNotifications } = props;
|
||||||
|
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS;
|
||||||
|
|
||||||
function handleMenuClick() {
|
function handleMenuClick() {
|
||||||
if (unseenCount > 0) doSeeAllNotifications();
|
if (unseenCount > 0) doSeeAllNotifications();
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectUserEmail, selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import * as MODALS from 'constants/modal_types';
|
|
||||||
import HeaderProfileMenuButton from './view';
|
import HeaderProfileMenuButton from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
email: selectUserEmail(state),
|
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
export default connect(select)(HeaderProfileMenuButton);
|
||||||
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(HeaderProfileMenuButton);
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuList, MenuButton } from '@reach/menu-button';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
@ -11,13 +11,10 @@ import React from 'react';
|
||||||
|
|
||||||
type HeaderMenuButtonProps = {
|
type HeaderMenuButtonProps = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
email: ?string,
|
|
||||||
authenticated: boolean,
|
|
||||||
openSignOutModal: () => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
||||||
const { activeChannelClaim, email, openSignOutModal, authenticated } = props;
|
const { activeChannelClaim } = props;
|
||||||
|
|
||||||
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
|
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
|
||||||
|
|
||||||
|
@ -43,8 +40,8 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
||||||
<HeaderMenuLink page={PAGES.UPLOADS} icon={ICONS.PUBLISH} name={__('Uploads')} />
|
<HeaderMenuLink page={PAGES.UPLOADS} icon={ICONS.PUBLISH} name={__('Uploads')} />
|
||||||
<HeaderMenuLink page={PAGES.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
|
<HeaderMenuLink page={PAGES.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
|
||||||
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
|
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
|
||||||
|
{/* No sync button for now
|
||||||
{authenticated ? (
|
{authenticated ? (
|
||||||
<MenuItem onSelect={openSignOutModal}>
|
<MenuItem onSelect={openSignOutModal}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
|
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
|
||||||
|
@ -53,8 +50,9 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
||||||
<span className="menu__link-help">{email}</span>
|
<span className="menu__link-help">{email}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<HeaderMenuLink page={PAGES.AUTH_SIGNIN} icon={ICONS.SIGN_IN} name={__('Cloud Connect')} />
|
<HeaderMenuLink page={PAGES.AUTH_SIGNIN} icon={ICONS.SIGN_IN} name={__('Maybe Sync')} />
|
||||||
)}
|
)}
|
||||||
|
*/}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import NotificationHeaderButton from './view';
|
import NotificationHeaderButton from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
unseenCount: selectUnseenNotificationCount(state),
|
unseenCount: selectUnseenNotificationCount(state),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(NotificationHeaderButton);
|
export default connect(select)(NotificationHeaderButton);
|
||||||
|
|
|
@ -6,12 +6,11 @@ import { ENABLE_UI_NOTIFICATIONS } from 'config';
|
||||||
type Props = {
|
type Props = {
|
||||||
unseenCount: number,
|
unseenCount: number,
|
||||||
inline: boolean,
|
inline: boolean,
|
||||||
user: ?User,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NotificationHeaderButton(props: Props) {
|
export default function NotificationHeaderButton(props: Props) {
|
||||||
const { unseenCount, inline = false, user } = props;
|
const { unseenCount, inline = false } = props;
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS;
|
||||||
|
|
||||||
if (unseenCount === 0 || !notificationsEnabled) {
|
if (unseenCount === 0 || !notificationsEnabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import NudgeFloating from './view';
|
import NudgeFloating from './view';
|
||||||
|
|
||||||
const select = state => ({
|
export default NudgeFloating;
|
||||||
user: selectUser(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select)(NudgeFloating);
|
|
||||||
|
|
|
@ -5,22 +5,20 @@ import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: ?User,
|
|
||||||
name: string,
|
name: string,
|
||||||
text: string,
|
text: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NudgeFloating(props: Props) {
|
export default function NudgeFloating(props: Props) {
|
||||||
const { user, name, text } = props;
|
const { name, text } = props;
|
||||||
const [showNudge, setShowNudge] = React.useState(false);
|
const [showNudge, setShowNudge] = React.useState(false);
|
||||||
const [nudgeAcknowledged, setNudgeAcknowledged] = usePersistedState(name, false);
|
const [nudgeAcknowledged, setNudgeAcknowledged] = usePersistedState(name, false);
|
||||||
const emailVerified = user && user.has_verified_email;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!emailVerified && !nudgeAcknowledged) {
|
if (!nudgeAcknowledged) {
|
||||||
setShowNudge(true);
|
setShowNudge(true);
|
||||||
}
|
}
|
||||||
}, [emailVerified, nudgeAcknowledged]);
|
}, [nudgeAcknowledged]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
showNudge && (
|
showNudge && (
|
||||||
|
|
|
@ -92,7 +92,7 @@ function Page(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('main-wrapper__inner', {
|
className={classnames('main-wrapper__inner', {
|
||||||
'main-wrapper__inner--filepage': isOnFilePage,
|
'main-wrapper__inner--filepage': isOnFilePage,
|
||||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!authPage &&
|
{!authPage &&
|
||||||
|
@ -124,7 +124,7 @@ function Page(props: Props) {
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--settings-page': settingsPage,
|
'main--settings-page': settingsPage,
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type={'markdown'}
|
type={'markdown'}
|
||||||
name="content_post"
|
name="content_post"
|
||||||
label={label}
|
label={label}
|
||||||
|
|
|
@ -1,23 +1,11 @@
|
||||||
import { DOMAIN } from 'config';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSetDaemonSetting } from 'redux/actions/settings';
|
import { doSetDaemonSetting } from 'redux/actions/settings';
|
||||||
import { doSignOut } from 'redux/actions/app';
|
|
||||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { doAuthenticate } from 'redux/actions/user';
|
|
||||||
import { version as appVersion } from 'package.json';
|
|
||||||
|
|
||||||
import PrivacyAgreement from './view';
|
import PrivacyAgreement from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
setShareDataInternal: (share) => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)),
|
setShareDataInternal: (share) => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)),
|
||||||
signOut: () => dispatch(doSignOut()),
|
|
||||||
authenticateIfSharingData: () =>
|
|
||||||
dispatch(doAuthenticate(appVersion, undefined, undefined, true, undefined, undefined, DOMAIN)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(PrivacyAgreement);
|
export default connect(null, perform)(PrivacyAgreement);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
import { FormField } from 'component/common/form-components/form-field';
|
import { FormField } from 'component/common/form-components/form-field';
|
||||||
import { Form } from 'component/common/form-components/form';
|
import { Form } from 'component/common/form-components/form';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
@ -14,13 +13,11 @@ const NONE = 'none';
|
||||||
type Props = {
|
type Props = {
|
||||||
signOut: () => void,
|
signOut: () => void,
|
||||||
setShareDataInternal: (boolean) => void,
|
setShareDataInternal: (boolean) => void,
|
||||||
authenticated: boolean,
|
|
||||||
authenticateIfSharingData: () => void,
|
|
||||||
handleNextPage: () => void,
|
handleNextPage: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PrivacyAgreement(props: Props) {
|
function PrivacyAgreement(props: Props) {
|
||||||
const { setShareDataInternal, authenticated, signOut, authenticateIfSharingData, handleNextPage } = props;
|
const { setShareDataInternal, handleNextPage } = props;
|
||||||
const [share, setShare] = useState(undefined); // preload
|
const [share, setShare] = useState(undefined); // preload
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
@ -30,10 +27,6 @@ function PrivacyAgreement(props: Props) {
|
||||||
setShareDataInternal(false);
|
setShareDataInternal(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (share === LIMITED) {
|
|
||||||
authenticateIfSharingData();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNextPage();
|
handleNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +56,6 @@ function PrivacyAgreement(props: Props) {
|
||||||
onChange={(e) => setShare(LIMITED)}
|
onChange={(e) => setShare(LIMITED)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
disabled={authenticated}
|
|
||||||
name={'shareNot'}
|
name={'shareNot'}
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={share === NONE}
|
checked={share === NONE}
|
||||||
|
@ -77,19 +69,6 @@ function PrivacyAgreement(props: Props) {
|
||||||
)}
|
)}
|
||||||
onChange={(e) => setShare(NONE)}
|
onChange={(e) => setShare(NONE)}
|
||||||
/>
|
/>
|
||||||
{authenticated && (
|
|
||||||
<div className="card--inline section--padded">
|
|
||||||
<p className="help--inline">
|
|
||||||
<I18nMessage
|
|
||||||
tokens={{
|
|
||||||
signout_button: <Button button="link" label={__('Sign Out')} onClick={signOut} />,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
You are signed in and sharing data with your cloud service provider. %signout_button%.
|
|
||||||
</I18nMessage>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className={'card__actions'}>
|
<div className={'card__actions'}>
|
||||||
<Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />
|
<Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />
|
||||||
|
|
|
@ -2,18 +2,13 @@ import { connect } from 'react-redux';
|
||||||
import { selectPublishFormValues } from 'redux/selectors/publish';
|
import { selectPublishFormValues } from 'redux/selectors/publish';
|
||||||
import { doUpdatePublishForm } from 'redux/actions/publish';
|
import { doUpdatePublishForm } from 'redux/actions/publish';
|
||||||
import PublishAdditionalOptions from './view';
|
import PublishAdditionalOptions from './view';
|
||||||
import { selectUser, selectAccessToken } from 'redux/selectors/user';
|
|
||||||
import { doFetchAccessToken } from 'redux/actions/user';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
...selectPublishFormValues(state),
|
...selectPublishFormValues(state),
|
||||||
accessToken: selectAccessToken(state),
|
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(PublishAdditionalOptions);
|
export default connect(select, perform)(PublishAdditionalOptions);
|
||||||
|
|
|
@ -10,14 +10,7 @@ import Card from 'component/common/card';
|
||||||
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||||||
import { sortLanguageMap } from 'util/default-languages';
|
import { sortLanguageMap } from 'util/default-languages';
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
// import ErrorText from 'component/common/error-text';
|
|
||||||
// import { LbryFirst } from 'lbry-redux';
|
|
||||||
// import { ipcRenderer } from 'electron';
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: ?User,
|
|
||||||
language: ?string,
|
language: ?string,
|
||||||
name: ?string,
|
name: ?string,
|
||||||
licenseType: ?string,
|
licenseType: ?string,
|
||||||
|
@ -25,92 +18,16 @@ type Props = {
|
||||||
licenseUrl: ?string,
|
licenseUrl: ?string,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
useLBRYUploader: boolean,
|
|
||||||
needsYTAuth: boolean,
|
|
||||||
fetchAccessToken: () => void,
|
|
||||||
accessToken: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishAdditionalOptions(props: Props) {
|
function PublishAdditionalOptions(props: Props) {
|
||||||
const {
|
const { language, name, licenseType, otherLicenseDescription, licenseUrl, updatePublishForm } = props;
|
||||||
language,
|
|
||||||
name,
|
|
||||||
licenseType,
|
|
||||||
otherLicenseDescription,
|
|
||||||
licenseUrl,
|
|
||||||
updatePublishForm,
|
|
||||||
// user,
|
|
||||||
// useLBRYUploader,
|
|
||||||
// needsYTAuth,
|
|
||||||
// accessToken,
|
|
||||||
// fetchAccessToken,
|
|
||||||
} = props;
|
|
||||||
const [hideSection, setHideSection] = usePersistedState('publish-advanced-options', true);
|
const [hideSection, setHideSection] = usePersistedState('publish-advanced-options', true);
|
||||||
// const [hasLaunchedLbryFirst, setHasLaunchedLbryFirst] = React.useState(false);
|
|
||||||
// const [ytError, setYtError] = React.useState(false);
|
|
||||||
// const isLBRYFirstUser = user && user.lbry_first_approved;
|
|
||||||
// const showLbryFirstCheckbox = !IS_WEB && isLBRYFirstUser && hasLaunchedLbryFirst;
|
|
||||||
|
|
||||||
function toggleHideSection() {
|
function toggleHideSection() {
|
||||||
setHideSection(!hideSection);
|
setHideSection(!hideSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
// function signup() {
|
|
||||||
// updatePublishForm({ ytSignupPending: true });
|
|
||||||
// LbryFirst.ytSignup()
|
|
||||||
// .then(response => {
|
|
||||||
// updatePublishForm({ needsYTAuth: false, ytSignupPending: false });
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// updatePublishForm({ ytSignupPending: false });
|
|
||||||
// setYtError(true);
|
|
||||||
// console.error(error); // eslint-disable-line
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function unlink() {
|
|
||||||
// setYtError(false);
|
|
||||||
|
|
||||||
// LbryFirst.remove()
|
|
||||||
// .then(response => {
|
|
||||||
// updatePublishForm({ needsYTAuth: true });
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// setYtError(true);
|
|
||||||
// console.error(error); // eslint-disable-line
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (!accessToken) {
|
|
||||||
// fetchAccessToken();
|
|
||||||
// }
|
|
||||||
// }, [accessToken, fetchAccessToken]);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (isLBRYFirstUser && !hasLaunchedLbryFirst) {
|
|
||||||
// ipcRenderer.send('launch-lbry-first');
|
|
||||||
// ipcRenderer.on('lbry-first-launched', () => {
|
|
||||||
// setHasLaunchedLbryFirst(true);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [isLBRYFirstUser, hasLaunchedLbryFirst, setHasLaunchedLbryFirst]);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (useLBRYUploader && isLBRYFirstUser && hasLaunchedLbryFirst && accessToken) {
|
|
||||||
// LbryFirst.hasYTAuth(accessToken)
|
|
||||||
// .then(response => {
|
|
||||||
// updatePublishForm({ needsYTAuth: !response.HasAuth });
|
|
||||||
// })
|
|
||||||
// .catch(error => {
|
|
||||||
// setYtError(true);
|
|
||||||
// console.error(error); // eslint-disable-line
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [updatePublishForm, useLBRYUploader, isLBRYFirstUser, hasLaunchedLbryFirst, accessToken]);
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="card--enable-overflow"
|
className="card--enable-overflow"
|
||||||
|
@ -118,41 +35,6 @@ function PublishAdditionalOptions(props: Props) {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!hideSection && (
|
{!hideSection && (
|
||||||
<div className={classnames({ 'card--disabled': !name })}>
|
<div className={classnames({ 'card--disabled': !name })}>
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
{/* {showLbryFirstCheckbox && (
|
|
||||||
<div className="section">
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
checked={useLBRYUploader}
|
|
||||||
type="checkbox"
|
|
||||||
name="use_lbry_uploader_checkbox"
|
|
||||||
onChange={event => updatePublishForm({ useLBRYUploader: !useLBRYUploader })}
|
|
||||||
label={
|
|
||||||
<React.Fragment>
|
|
||||||
{__('Automagically upload to your youtube channel.')}{' '}
|
|
||||||
<Button button="link" href="https://lbry.com/faq/lbry-uploader" label={__('Learn More')} />
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{useLBRYUploader && (
|
|
||||||
<div className="section__actions">
|
|
||||||
{needsYTAuth ? (
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
onClick={signup}
|
|
||||||
label={__('Log In With YouTube')}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button button="alt" onClick={unlink} label={__('Unlink YouTube Channel')} disabled={false} />
|
|
||||||
)}
|
|
||||||
{ytError && <ErrorText>{__('There was an error with LBRY first publishing.')}</ErrorText>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
{/* @endif */}
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<PublishReleaseDate />
|
<PublishReleaseDate />
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
import path from 'path';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
mode: ?string,
|
mode: ?string,
|
||||||
|
@ -99,27 +99,18 @@ function PublishFile(props: Props) {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
async function readSelectedFileDetails() {
|
async function readSelectedFile() {
|
||||||
// Read the file to get the file's duration (if possible)
|
// Read the file to get the file's duration (if possible)
|
||||||
// and offer transcoding it.
|
// and offer transcoding it.
|
||||||
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
|
const readFileContents = true;
|
||||||
let file;
|
const result = await ipcRenderer.invoke('get-file-from-path', filePath, readFileContents);
|
||||||
if (result.buffer) {
|
const file = new File([result.buffer], result.name, {
|
||||||
file = new File([result.buffer], result.name, {
|
type: result.mime,
|
||||||
type: result.mime,
|
});
|
||||||
});
|
const fileWithPath = { file, path: result.path };
|
||||||
}
|
processSelectedFile(fileWithPath);
|
||||||
const fileData: FileData = {
|
|
||||||
path: result.path,
|
|
||||||
name: result.name,
|
|
||||||
mimeType: result.mime || 'application/octet-stream',
|
|
||||||
size: result.size,
|
|
||||||
duration: result.duration,
|
|
||||||
file: file,
|
|
||||||
};
|
|
||||||
processSelectedFile(fileData);
|
|
||||||
}
|
}
|
||||||
readSelectedFileDetails();
|
readSelectedFile();
|
||||||
}, [filePath]);
|
}, [filePath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -228,11 +219,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processSelectedFile(fileData: FileData, clearName = true) {
|
function processSelectedFile(fileWithPath: FileWithPath, clearName = true) {
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!fileData || fileData.error) {
|
if (!fileWithPath) {
|
||||||
if (isStillEditing || !clearName) {
|
if (isStillEditing || !clearName) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,11 +233,8 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
||||||
const file = fileData.file;
|
const file = fileWithPath.file;
|
||||||
// Check to see if it's a video and if mp4
|
const contentType = file.type && file.type.split('/');
|
||||||
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
|
|
||||||
const duration = fileData.duration;
|
|
||||||
const size = fileData.size;
|
|
||||||
const isVideo = contentType && contentType[0] === 'video';
|
const isVideo = contentType && contentType[0] === 'video';
|
||||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||||
|
|
||||||
|
@ -254,25 +242,34 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
if (contentType && contentType[0] === 'text') {
|
if (contentType && contentType[0] === 'text') {
|
||||||
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
||||||
setCurrentFileType(contentType.join('/'));
|
setCurrentFileType(contentType);
|
||||||
} else if (path.parse(fileData.path).ext) {
|
} else if (file.name) {
|
||||||
// If user's machine is missing a valid content type registration
|
// If user's machine is missing a valid content type registration
|
||||||
// for markdown content: text/markdown, file extension will be used instead
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
const extension = path.parse(fileData.path).ext;
|
const extension = file.name.split('.').pop();
|
||||||
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
updateFileInfo(duration || 0, size, isVideo);
|
const video = document.createElement('video');
|
||||||
|
video.preload = 'metadata';
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
updateFileInfo(video.duration, file.size, isVideo);
|
||||||
|
window.URL.revokeObjectURL(video.src);
|
||||||
|
};
|
||||||
|
video.onerror = () => {
|
||||||
|
updateFileInfo(0, file.size, isVideo);
|
||||||
|
};
|
||||||
|
video.src = window.URL.createObjectURL(file);
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(duration || 0, size, isVideo);
|
updateFileInfo(0, file.size, isVideo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, size, isVideo);
|
updateFileInfo(0, file.size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextPost && file) {
|
if (isTextPost) {
|
||||||
// Create reader
|
// Create reader
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// Handler for file reader
|
// Handler for file reader
|
||||||
|
@ -286,7 +283,7 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
// Strip off extension and replace invalid characters
|
// Strip off extension and replace invalid characters
|
||||||
if (!isStillEditing) {
|
if (!isStillEditing) {
|
||||||
const fileWithoutExtension = path.parse(fileData.path).name;
|
const fileWithoutExtension = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
|
||||||
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,6 @@ import {
|
||||||
} from 'redux/selectors/publish';
|
} from 'redux/selectors/publish';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { doClaimInitialRewards } from 'redux/actions/rewards';
|
|
||||||
import {
|
|
||||||
selectUnclaimedRewardValue,
|
|
||||||
selectIsClaimingInitialRewards,
|
|
||||||
selectHasClaimedInitialRewards,
|
|
||||||
} from 'redux/selectors/rewards';
|
|
||||||
import {
|
import {
|
||||||
selectModal,
|
selectModal,
|
||||||
selectActiveChannelClaim,
|
selectActiveChannelClaim,
|
||||||
|
@ -31,7 +25,6 @@ import {
|
||||||
} from 'redux/selectors/app';
|
} from 'redux/selectors/app';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import PublishForm from './view';
|
import PublishForm from './view';
|
||||||
|
|
||||||
const select = (state) => {
|
const select = (state) => {
|
||||||
|
@ -41,7 +34,6 @@ const select = (state) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...selectPublishFormValues(state),
|
...selectPublishFormValues(state),
|
||||||
user: selectUser(state),
|
|
||||||
// The winning claim for a short lbry uri
|
// The winning claim for a short lbry uri
|
||||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
isPostClaim,
|
isPostClaim,
|
||||||
|
@ -55,14 +47,11 @@ const select = (state) => {
|
||||||
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
|
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
|
||||||
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
|
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
|
||||||
isResolvingUri: selectIsResolvingPublishUris(state),
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
|
||||||
modal: selectModal(state),
|
modal: selectModal(state),
|
||||||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
incognito: selectIncognito(state),
|
incognito: selectIncognito(state),
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
|
||||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,7 +63,6 @@ const perform = (dispatch) => ({
|
||||||
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||||
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
|
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
|
||||||
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(PublishForm);
|
export default connect(select, perform)(PublishForm);
|
||||||
|
|
|
@ -62,7 +62,6 @@ type Props = {
|
||||||
licenseType: string,
|
licenseType: string,
|
||||||
otherLicenseDescription: ?string,
|
otherLicenseDescription: ?string,
|
||||||
licenseUrl: ?string,
|
licenseUrl: ?string,
|
||||||
useLBRYUploader: ?boolean,
|
|
||||||
publishing: boolean,
|
publishing: boolean,
|
||||||
publishSuccess: boolean,
|
publishSuccess: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -76,19 +75,14 @@ type Props = {
|
||||||
// Add back type
|
// Add back type
|
||||||
updatePublishForm: (any) => void,
|
updatePublishForm: (any) => void,
|
||||||
checkAvailability: (string) => void,
|
checkAvailability: (string) => void,
|
||||||
ytSignupPending: boolean,
|
|
||||||
modal: { id: string, modalProps: {} },
|
modal: { id: string, modalProps: {} },
|
||||||
enablePublishPreview: boolean,
|
enablePublishPreview: boolean,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
incognito: boolean,
|
incognito: boolean,
|
||||||
user: ?User,
|
|
||||||
activeChannelStakedLevel: number,
|
activeChannelStakedLevel: number,
|
||||||
isPostClaim: boolean,
|
isPostClaim: boolean,
|
||||||
permanentUrl: ?string,
|
permanentUrl: ?string,
|
||||||
remoteUrl: ?string,
|
remoteUrl: ?string,
|
||||||
isClaimingInitialRewards: boolean,
|
|
||||||
claimInitialRewards: () => void,
|
|
||||||
hasClaimedInitialRewards: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishForm(props: Props) {
|
function PublishForm(props: Props) {
|
||||||
|
@ -116,7 +110,6 @@ function PublishForm(props: Props) {
|
||||||
publish,
|
publish,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
checkAvailability,
|
checkAvailability,
|
||||||
ytSignupPending,
|
|
||||||
modal,
|
modal,
|
||||||
enablePublishPreview,
|
enablePublishPreview,
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
|
@ -124,9 +117,6 @@ function PublishForm(props: Props) {
|
||||||
isPostClaim,
|
isPostClaim,
|
||||||
permanentUrl,
|
permanentUrl,
|
||||||
remoteUrl,
|
remoteUrl,
|
||||||
isClaimingInitialRewards,
|
|
||||||
claimInitialRewards,
|
|
||||||
hasClaimedInitialRewards,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const inEditMode = Boolean(editingURI);
|
const inEditMode = Boolean(editingURI);
|
||||||
|
@ -208,6 +198,7 @@ function PublishForm(props: Props) {
|
||||||
isNameValid(name) &&
|
isNameValid(name) &&
|
||||||
title &&
|
title &&
|
||||||
bid &&
|
bid &&
|
||||||
|
thumbnail &&
|
||||||
!bidError &&
|
!bidError &&
|
||||||
!emptyPostError &&
|
!emptyPostError &&
|
||||||
!(thumbnailError && !thumbnailUploaded) &&
|
!(thumbnailError && !thumbnailUploaded) &&
|
||||||
|
@ -223,12 +214,6 @@ function PublishForm(props: Props) {
|
||||||
|
|
||||||
const [previewing, setPreviewing] = React.useState(false);
|
const [previewing, setPreviewing] = React.useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasClaimedInitialRewards) {
|
|
||||||
claimInitialRewards();
|
|
||||||
}
|
|
||||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -239,9 +224,7 @@ function PublishForm(props: Props) {
|
||||||
|
|
||||||
let submitLabel;
|
let submitLabel;
|
||||||
|
|
||||||
if (isClaimingInitialRewards) {
|
if (publishing) {
|
||||||
submitLabel = __('Claiming credits...');
|
|
||||||
} else if (publishing) {
|
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
submitLabel = __('Saving...');
|
submitLabel = __('Saving...');
|
||||||
} else {
|
} else {
|
||||||
|
@ -535,12 +518,7 @@ function PublishForm(props: Props) {
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
label={submitLabel}
|
label={submitLabel}
|
||||||
disabled={
|
disabled={
|
||||||
isClaimingInitialRewards ||
|
formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS || previewing
|
||||||
formDisabled ||
|
|
||||||
!formValid ||
|
|
||||||
uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS ||
|
|
||||||
ytSignupPending ||
|
|
||||||
previewing
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button button="link" onClick={clearPublish} label={__('New --[clears Publish Form]--')} />
|
<Button button="link" onClick={clearPublish} label={__('New --[clears Publish Form]--')} />
|
||||||
|
|
|
@ -6,7 +6,6 @@ import Card from 'component/common/card';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import RecSys from 'recsys';
|
|
||||||
|
|
||||||
const VIEW_ALL_RELATED = 'view_all_related';
|
const VIEW_ALL_RELATED = 'view_all_related';
|
||||||
const VIEW_MORE_FROM = 'view_more_from';
|
const VIEW_MORE_FROM = 'view_more_from';
|
||||||
|
@ -18,48 +17,20 @@ type Props = {
|
||||||
isSearching: boolean,
|
isSearching: boolean,
|
||||||
doFetchRecommendedContent: (string) => void,
|
doFetchRecommendedContent: (string) => void,
|
||||||
claim: ?StreamClaim,
|
claim: ?StreamClaim,
|
||||||
claimId: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
const {
|
const { uri, doFetchRecommendedContent, recommendedContentUris, isSearching, claim } = props;
|
||||||
uri,
|
|
||||||
doFetchRecommendedContent,
|
|
||||||
recommendedContentUris,
|
|
||||||
nextRecommendedUri,
|
|
||||||
isSearching,
|
|
||||||
claim,
|
|
||||||
claimId,
|
|
||||||
} = props;
|
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
const channelName = signingChannel ? signingChannel.name : null;
|
const channelName = signingChannel ? signingChannel.name : null;
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMedium = useIsMediumScreen();
|
const isMedium = useIsMediumScreen();
|
||||||
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
doFetchRecommendedContent(uri);
|
doFetchRecommendedContent(uri);
|
||||||
}, [uri, doFetchRecommendedContent]);
|
}, [uri, doFetchRecommendedContent]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Right now we only want to record the recs if they actually saw them.
|
|
||||||
if (
|
|
||||||
recommendedContentUris &&
|
|
||||||
recommendedContentUris.length &&
|
|
||||||
nextRecommendedUri &&
|
|
||||||
viewMode === VIEW_ALL_RELATED
|
|
||||||
) {
|
|
||||||
onRecommendationsLoaded(claimId, recommendedContentUris);
|
|
||||||
}
|
|
||||||
}, [recommendedContentUris, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]);
|
|
||||||
|
|
||||||
function handleRecommendationClicked(e, clickedClaim) {
|
|
||||||
if (claim) {
|
|
||||||
onRecommendationClicked(claim.claim_id, clickedClaim.claim_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
isBodyList
|
isBodyList
|
||||||
|
@ -96,7 +67,6 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
uris={recommendedContentUris}
|
uris={recommendedContentUris}
|
||||||
hideMenu={isMobile}
|
hideMenu={isMobile}
|
||||||
empty={__('No related content found')}
|
empty={__('No related content found')}
|
||||||
onClick={handleRecommendationClicked}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{viewMode === VIEW_MORE_FROM && signingChannel && (
|
{viewMode === VIEW_MORE_FROM && signingChannel && (
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { selectHasNavigated, selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app';
|
import { selectHasNavigated, selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app';
|
||||||
import { selectHomepageData } from 'redux/selectors/settings';
|
import { selectHomepageData } from 'redux/selectors/settings';
|
||||||
import Router from './view';
|
import Router from './view';
|
||||||
import { normalizeURI } from 'util/lbryURI';
|
import { normalizeURI } from 'util/lbryURI';
|
||||||
import { selectTitleForUri } from 'redux/selectors/claims';
|
import { selectTitleForUri } from 'redux/selectors/claims';
|
||||||
import { doSetHasNavigated } from 'redux/actions/app';
|
import { doSetHasNavigated } from 'redux/actions/app';
|
||||||
import { doUserSetReferrer } from 'redux/actions/user';
|
|
||||||
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
|
|
||||||
|
|
||||||
const select = (state) => {
|
const select = (state) => {
|
||||||
const { pathname, hash } = state.router.location;
|
const { pathname, hash } = state.router.location;
|
||||||
|
@ -30,17 +27,14 @@ const select = (state) => {
|
||||||
uri,
|
uri,
|
||||||
title: selectTitleForUri(state, uri),
|
title: selectTitleForUri(state, uri),
|
||||||
currentScroll: selectScrollStartingPosition(state),
|
currentScroll: selectScrollStartingPosition(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
welcomeVersion: selectWelcomeVersion(state),
|
welcomeVersion: selectWelcomeVersion(state),
|
||||||
hasNavigated: selectHasNavigated(state),
|
hasNavigated: selectHasNavigated(state),
|
||||||
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
|
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
setHasNavigated: () => dispatch(doSetHasNavigated()),
|
setHasNavigated: () => dispatch(doSetHasNavigated()),
|
||||||
setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(Router);
|
export default connect(select, perform)(Router);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { PAGE_TITLE } from 'constants/pageTitles';
|
import { PAGE_TITLE } from 'constants/pageTitles';
|
||||||
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||||
import { parseURI, isURIValid } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import { WELCOME_VERSION } from 'config';
|
import { WELCOME_VERSION } from 'config';
|
||||||
import { GetLinksData } from 'util/buildHomepage';
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||||
|
@ -14,14 +14,9 @@ import HomePage from 'page/home';
|
||||||
import BackupPage from 'page/backup';
|
import BackupPage from 'page/backup';
|
||||||
|
|
||||||
// Chunk: "secondary"
|
// Chunk: "secondary"
|
||||||
import SignInPage from 'page/signIn';
|
|
||||||
import SignInWalletPasswordPage from 'page/signInWalletPassword';
|
import SignInWalletPasswordPage from 'page/signInWalletPassword';
|
||||||
|
|
||||||
import SignUpPage from 'page/signUp';
|
|
||||||
import SignInVerifyPage from 'page/signInVerify';
|
|
||||||
|
|
||||||
// Chunk: "wallet/secondary"
|
// Chunk: "wallet/secondary"
|
||||||
import BuyPage from 'page/buy';
|
|
||||||
import ReceivePage from 'page/receive';
|
import ReceivePage from 'page/receive';
|
||||||
import SendPage from 'page/send';
|
import SendPage from 'page/send';
|
||||||
import WalletPage from 'page/wallet';
|
import WalletPage from 'page/wallet';
|
||||||
|
@ -43,8 +38,6 @@ import ListBlockedPage from 'page/listBlocked';
|
||||||
import ListsPage from 'page/lists';
|
import ListsPage from 'page/lists';
|
||||||
import PlaylistsPage from 'page/playlists';
|
import PlaylistsPage from 'page/playlists';
|
||||||
import OwnComments from 'page/ownComments';
|
import OwnComments from 'page/ownComments';
|
||||||
import PasswordResetPage from 'page/passwordReset';
|
|
||||||
import PasswordSetPage from 'page/passwordSet';
|
|
||||||
import PublishPage from 'page/publish';
|
import PublishPage from 'page/publish';
|
||||||
import ReportContentPage from 'page/reportContent';
|
import ReportContentPage from 'page/reportContent';
|
||||||
import ReportPage from 'page/report';
|
import ReportPage from 'page/report';
|
||||||
|
@ -53,6 +46,7 @@ import SearchPage from 'page/search';
|
||||||
|
|
||||||
import SettingsCreatorPage from 'page/settingsCreator';
|
import SettingsCreatorPage from 'page/settingsCreator';
|
||||||
import SettingsNotificationsPage from 'page/settingsNotifications';
|
import SettingsNotificationsPage from 'page/settingsNotifications';
|
||||||
|
import SettingsSyncPage from 'page/settingsSync';
|
||||||
|
|
||||||
import SettingsPage from 'page/settings';
|
import SettingsPage from 'page/settings';
|
||||||
import ShowPage from 'page/show';
|
import ShowPage from 'page/show';
|
||||||
|
@ -70,7 +64,6 @@ if ('scrollRestoration' in history) {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentScroll: number,
|
currentScroll: number,
|
||||||
isAuthenticated: boolean,
|
|
||||||
location: { pathname: string, search: string, hash: string },
|
location: { pathname: string, search: string, hash: string },
|
||||||
history: {
|
history: {
|
||||||
action: string,
|
action: string,
|
||||||
|
@ -90,14 +83,12 @@ type Props = {
|
||||||
welcomeVersion: number,
|
welcomeVersion: number,
|
||||||
hasNavigated: boolean,
|
hasNavigated: boolean,
|
||||||
setHasNavigated: () => void,
|
setHasNavigated: () => void,
|
||||||
setReferrer: (?string) => void,
|
|
||||||
hasUnclaimedRefereeReward: boolean,
|
|
||||||
homepageData: any,
|
homepageData: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
type PrivateRouteProps = Props & {
|
type PrivateRouteProps = Props & {
|
||||||
component: any,
|
component: any,
|
||||||
isAuthenticated: boolean,
|
isAuthenticated?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PrivateRoute(props: PrivateRouteProps) {
|
function PrivateRoute(props: PrivateRouteProps) {
|
||||||
|
@ -109,15 +100,12 @@ function AppRouter(props: Props) {
|
||||||
const {
|
const {
|
||||||
currentScroll,
|
currentScroll,
|
||||||
location: { pathname, search, hash },
|
location: { pathname, search, hash },
|
||||||
isAuthenticated,
|
|
||||||
history,
|
history,
|
||||||
uri,
|
uri,
|
||||||
title,
|
title,
|
||||||
welcomeVersion,
|
welcomeVersion,
|
||||||
hasNavigated,
|
hasNavigated,
|
||||||
setHasNavigated,
|
setHasNavigated,
|
||||||
hasUnclaimedRefereeReward,
|
|
||||||
setReferrer,
|
|
||||||
homepageData,
|
homepageData,
|
||||||
} = props;
|
} = props;
|
||||||
const { entries, listen, action: historyAction } = history;
|
const { entries, listen, action: historyAction } = history;
|
||||||
|
@ -140,16 +128,6 @@ function AppRouter(props: Props) {
|
||||||
return unlisten;
|
return unlisten;
|
||||||
}, [listen, hasNavigated, setHasNavigated]);
|
}, [listen, hasNavigated, setHasNavigated]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasNavigated && hasUnclaimedRefereeReward && !isAuthenticated) {
|
|
||||||
const valid = isURIValid(uri);
|
|
||||||
if (valid) {
|
|
||||||
const { path } = parseURI(uri);
|
|
||||||
if (path !== 'undefined') setReferrer(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [hasNavigated, uri, hasUnclaimedRefereeReward, setReferrer, isAuthenticated]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getDefaultTitle = (pathname: string) => {
|
const getDefaultTitle = (pathname: string) => {
|
||||||
const title = pathname.startsWith('/$/') ? PAGE_TITLE[pathname.substring(3)] : '';
|
const title = pathname.startsWith('/$/') ? PAGE_TITLE[pathname.substring(3)] : '';
|
||||||
|
@ -175,9 +153,7 @@ function AppRouter(props: Props) {
|
||||||
document.title = getDefaultTitle(pathname);
|
document.title = getDefaultTitle(pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
entries[entryIndex].title = document.title;
|
entries[entryIndex].title = document.title;
|
||||||
// @endif
|
|
||||||
}, [pathname, entries, entryIndex, title, uri]);
|
}, [pathname, entries, entryIndex, title, uri]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -227,19 +203,10 @@ function AppRouter(props: Props) {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Odysee signin */}
|
{/* Odysee signin */}
|
||||||
<Route path={`/$/${PAGES.AUTH_SIGNIN}`} exact component={SignInPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH_PASSWORD_RESET}`} exact component={PasswordResetPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH_PASSWORD_SET}`} exact component={PasswordSetPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH}`} exact component={SignUpPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignUpPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH_VERIFY}`} exact component={SignInVerifyPage} />
|
|
||||||
|
|
||||||
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
|
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
|
||||||
|
<Route path={`/$/${PAGES.SETTINGS_SYNC}`} exact component={SettingsSyncPage} />
|
||||||
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
|
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
<Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} />
|
<Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} />
|
||||||
{/* @endif */}
|
|
||||||
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
||||||
<Route path={`/$/${PAGES.TOP}`} exact component={TopPage} />
|
<Route path={`/$/${PAGES.TOP}`} exact component={TopPage} />
|
||||||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||||
|
@ -270,7 +237,6 @@ function AppRouter(props: Props) {
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||||
|
|
|
@ -3,7 +3,6 @@ import SelectChannel from './view';
|
||||||
import { selectBalance } from 'redux/selectors/wallet';
|
import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims';
|
import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims';
|
||||||
import { doFetchChannelListMine, doCreateChannel } from 'redux/actions/claims';
|
import { doFetchChannelListMine, doCreateChannel } from 'redux/actions/claims';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { doSetActiveChannel } from 'redux/actions/app';
|
import { doSetActiveChannel } from 'redux/actions/app';
|
||||||
|
|
||||||
|
@ -11,7 +10,6 @@ const select = (state) => ({
|
||||||
myChannelClaims: selectMyChannelClaims(state),
|
myChannelClaims: selectMyChannelClaims(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
emailVerified: selectUserVerifiedEmail(state),
|
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ function SelectThumbnail(props: Props) {
|
||||||
__('This will be visible in a few minutes after you submit this form.')}
|
__('This will be visible in a few minutes after you submit this form.')}
|
||||||
<img
|
<img
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
src={thumbnail || ThumbnailMissingImage}
|
src={thumbnail}
|
||||||
alt={__('Thumbnail Preview')}
|
alt={__('Thumbnail Preview')}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
if (updateThumbnailParams) {
|
if (updateThumbnailParams) {
|
||||||
|
|
|
@ -2,14 +2,11 @@ import { connect } from 'react-redux';
|
||||||
import { selectHasChannels } from 'redux/selectors/claims';
|
import { selectHasChannels } from 'redux/selectors/claims';
|
||||||
import { selectWalletIsEncrypted } from 'redux/selectors/wallet';
|
import { selectWalletIsEncrypted } from 'redux/selectors/wallet';
|
||||||
import { doWalletStatus } from 'redux/actions/wallet';
|
import { doWalletStatus } from 'redux/actions/wallet';
|
||||||
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
import SettingAccount from './view';
|
import SettingAccount from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
walletEncrypted: selectWalletIsEncrypted(state),
|
walletEncrypted: selectWalletIsEncrypted(state),
|
||||||
user: selectUser(state),
|
|
||||||
hasChannels: selectHasChannels(state),
|
hasChannels: selectHasChannels(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,27 +6,26 @@ import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import SettingsRow from 'component/settingsRow';
|
import SettingsRow from 'component/settingsRow';
|
||||||
import SyncToggle from 'component/syncToggle';
|
// maybe bring this back
|
||||||
|
// import SyncToggle from 'component/syncToggle';
|
||||||
import { getPasswordFromCookie } from 'util/saved-passwords';
|
import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isAuthenticated: boolean,
|
|
||||||
walletEncrypted: boolean,
|
walletEncrypted: boolean,
|
||||||
user: User,
|
|
||||||
hasChannels: boolean,
|
hasChannels: boolean,
|
||||||
doWalletStatus: () => void,
|
doWalletStatus: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingAccount(props: Props) {
|
export default function SettingAccount(props: Props) {
|
||||||
const { isAuthenticated, walletEncrypted, hasChannels, doWalletStatus } = props;
|
const { hasChannels, doWalletStatus } = props;
|
||||||
const [storedPassword, setStoredPassword] = React.useState(false);
|
// const [storedPassword, setStoredPassword] = React.useState(false);
|
||||||
|
|
||||||
// Determine if password is stored.
|
// Determine if password is stored.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
doWalletStatus();
|
doWalletStatus();
|
||||||
getPasswordFromCookie().then((p) => {
|
getPasswordFromCookie().then((p) => {
|
||||||
if (typeof p === 'string') {
|
if (typeof p === 'string') {
|
||||||
setStoredPassword(true);
|
// get password
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
@ -42,18 +41,8 @@ export default function SettingAccount(props: Props) {
|
||||||
isBodyList
|
isBodyList
|
||||||
body={
|
body={
|
||||||
<>
|
<>
|
||||||
{isAuthenticated && (
|
{/* This will probably start the new sync flow when checked (-> openModal(SYNC_ENABLE) ) */}
|
||||||
<SettingsRow title={__('Password')}>
|
{/* <SyncToggle disabled={true} /> */}
|
||||||
<Button
|
|
||||||
button="inverse"
|
|
||||||
label={__('Manage')}
|
|
||||||
icon={ICONS.ARROW_RIGHT}
|
|
||||||
navigate={`/$/${PAGES.SETTINGS_UPDATE_PWD}`}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
|
|
||||||
|
|
||||||
{hasChannels && (
|
{hasChannels && (
|
||||||
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectUser, selectPasswordSetSuccess, selectPasswordSetError } from 'redux/selectors/user';
|
|
||||||
import { doUserPasswordSet, doClearPasswordEntry } from 'redux/actions/user';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
import UserSignIn from './view';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
|
||||||
user: selectUser(state),
|
|
||||||
passwordSetSuccess: selectPasswordSetSuccess(state),
|
|
||||||
passwordSetError: selectPasswordSetError(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, {
|
|
||||||
doUserPasswordSet,
|
|
||||||
doToast,
|
|
||||||
doClearPasswordEntry,
|
|
||||||
})(UserSignIn);
|
|
|
@ -1,81 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useHistory } from 'react-router';
|
|
||||||
import { FormField, Form } from 'component/common/form';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import ErrorText from 'component/common/error-text';
|
|
||||||
import SettingsRow from 'component/settingsRow';
|
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
user: ?User,
|
|
||||||
doToast: ({ message: string }) => void,
|
|
||||||
doUserPasswordSet: (string, ?string) => void,
|
|
||||||
doClearPasswordEntry: () => void,
|
|
||||||
passwordSetSuccess: boolean,
|
|
||||||
passwordSetError: ?string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SettingAccountPassword(props: Props) {
|
|
||||||
const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props;
|
|
||||||
const [oldPassword, setOldPassword] = useState('');
|
|
||||||
const [newPassword, setNewPassword] = useState('');
|
|
||||||
const hasPassword = user && user.password_set;
|
|
||||||
const { goBack } = useHistory();
|
|
||||||
|
|
||||||
const title = hasPassword ? __('Update Your Password') : __('Add A Password');
|
|
||||||
const subtitle = hasPassword ? '' : __('You do not currently have a password set.');
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
doUserPasswordSet(newPassword, oldPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (passwordSetSuccess) {
|
|
||||||
goBack();
|
|
||||||
doToast({
|
|
||||||
message: __('Password updated successfully.'),
|
|
||||||
});
|
|
||||||
doClearPasswordEntry();
|
|
||||||
setOldPassword('');
|
|
||||||
setNewPassword('');
|
|
||||||
}
|
|
||||||
}, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast, goBack]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsRow title={title} subtitle={subtitle} multirow>
|
|
||||||
<Form onSubmit={handleSubmit} className="section">
|
|
||||||
{hasPassword && (
|
|
||||||
<FormField
|
|
||||||
type="password"
|
|
||||||
name="setting_set_old_password"
|
|
||||||
label={__('Old Password')}
|
|
||||||
value={oldPassword}
|
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormField
|
|
||||||
type="password"
|
|
||||||
name="setting_set_new_password"
|
|
||||||
label={__('New Password')}
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button button="primary" type="submit" label={__('Set Password')} disabled={!newPassword} />
|
|
||||||
{hasPassword ? (
|
|
||||||
<Button button="link" label={__('Forgot Password?')} navigate={`/$/${PAGES.AUTH_PASSWORD_RESET}`} />
|
|
||||||
) : (
|
|
||||||
<Button button="link" label={__('Cancel')} onClick={() => goBack()} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
{passwordSetError && (
|
|
||||||
<div className="section">
|
|
||||||
<ErrorText>{passwordSetError}</ErrorText>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SettingsRow>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,12 +4,10 @@ import * as SETTINGS from 'constants/settings';
|
||||||
import { doSetPlayingUri, clearContentCache } from 'redux/actions/content';
|
import { doSetPlayingUri, clearContentCache } from 'redux/actions/content';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
import SettingContent from './view';
|
import SettingContent from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
autoplayMedia: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
|
autoplayMedia: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
|
||||||
autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state),
|
autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state),
|
||||||
|
|
|
@ -3,7 +3,6 @@ import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { SETTINGS_GRP } from 'constants/settings';
|
import { SETTINGS_GRP } from 'constants/settings';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
@ -18,7 +17,6 @@ type Price = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// --- select ---
|
// --- select ---
|
||||||
isAuthenticated: boolean,
|
|
||||||
floatingPlayer: boolean,
|
floatingPlayer: boolean,
|
||||||
autoplayMedia: boolean,
|
autoplayMedia: boolean,
|
||||||
autoplayNext: boolean,
|
autoplayNext: boolean,
|
||||||
|
@ -37,7 +35,6 @@ type Props = {
|
||||||
|
|
||||||
export default function SettingContent(props: Props) {
|
export default function SettingContent(props: Props) {
|
||||||
const {
|
const {
|
||||||
isAuthenticated,
|
|
||||||
floatingPlayer,
|
floatingPlayer,
|
||||||
autoplayMedia,
|
autoplayMedia,
|
||||||
autoplayNext,
|
autoplayNext,
|
||||||
|
@ -110,10 +107,6 @@ export default function SettingContent(props: Props) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="hide_reposts"
|
name="hide_reposts"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (isAuthenticated) {
|
|
||||||
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
|
|
||||||
Lbryio.call('user_tag', 'edit', param);
|
|
||||||
}
|
|
||||||
setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
|
setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
|
||||||
}}
|
}}
|
||||||
checked={hideReposts}
|
checked={hideReposts}
|
||||||
|
@ -172,7 +165,7 @@ export default function SettingContent(props: Props) {
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
{myChannelUrls && myChannelUrls.length > 0 && (
|
{myChannelUrls && myChannelUrls.length > 0 && (
|
||||||
<SettingsRow title={__('Creator settings')}>
|
<SettingsRow title={__('Creator Comment settings')}>
|
||||||
<Button
|
<Button
|
||||||
button="inverse"
|
button="inverse"
|
||||||
label={__('Manage')}
|
label={__('Manage')}
|
||||||
|
|
|
@ -6,12 +6,12 @@ import {
|
||||||
doNotifyDecryptWallet,
|
doNotifyDecryptWallet,
|
||||||
doNotifyEncryptWallet,
|
doNotifyEncryptWallet,
|
||||||
doNotifyForgetPassword,
|
doNotifyForgetPassword,
|
||||||
|
doOpenModal,
|
||||||
doToggle3PAnalytics,
|
doToggle3PAnalytics,
|
||||||
} from 'redux/actions/app';
|
} from 'redux/actions/app';
|
||||||
import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings';
|
import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings';
|
||||||
import { selectAllowAnalytics } from 'redux/selectors/app';
|
import { selectAllowAnalytics } from 'redux/selectors/app';
|
||||||
import { selectDaemonSettings, selectFfmpegStatus, selectFindingFFmpeg } from 'redux/selectors/settings';
|
import { selectDaemonSettings, selectFfmpegStatus, selectFindingFFmpeg } from 'redux/selectors/settings';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user'; // here
|
|
||||||
|
|
||||||
import SettingSystem from './view';
|
import SettingSystem from './view';
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ const select = (state) => ({
|
||||||
ffmpegStatus: selectFfmpegStatus(state),
|
ffmpegStatus: selectFfmpegStatus(state),
|
||||||
findingFFmpeg: selectFindingFFmpeg(state),
|
findingFFmpeg: selectFindingFFmpeg(state),
|
||||||
walletEncrypted: selectWalletIsEncrypted(state),
|
walletEncrypted: selectWalletIsEncrypted(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
allowAnalytics: selectAllowAnalytics(state),
|
allowAnalytics: selectAllowAnalytics(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,6 +33,7 @@ const perform = (dispatch) => ({
|
||||||
updateWalletStatus: () => dispatch(doWalletStatus()),
|
updateWalletStatus: () => dispatch(doWalletStatus()),
|
||||||
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
|
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
|
||||||
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
|
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
|
||||||
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SettingSystem);
|
export default connect(select, perform)(SettingSystem);
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||||
import SettingEnablePrereleases from 'component/settingEnablePrereleases';
|
import SettingEnablePrereleases from 'component/settingEnablePrereleases';
|
||||||
import SettingDisableAutoUpdates from 'component/settingDisableAutoUpdates';
|
import SettingDisableAutoUpdates from 'component/settingDisableAutoUpdates';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
|
||||||
const IS_MAC = process.platform === 'darwin';
|
const IS_MAC = process.platform === 'darwin';
|
||||||
|
|
||||||
|
@ -44,7 +46,6 @@ type Props = {
|
||||||
ffmpegStatus: { available: boolean, which: string },
|
ffmpegStatus: { available: boolean, which: string },
|
||||||
findingFFmpeg: boolean,
|
findingFFmpeg: boolean,
|
||||||
walletEncrypted: boolean,
|
walletEncrypted: boolean,
|
||||||
isAuthenticated: boolean,
|
|
||||||
allowAnalytics: boolean,
|
allowAnalytics: boolean,
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||||
|
@ -64,7 +65,6 @@ export default function SettingSystem(props: Props) {
|
||||||
ffmpegStatus,
|
ffmpegStatus,
|
||||||
findingFFmpeg,
|
findingFFmpeg,
|
||||||
walletEncrypted,
|
walletEncrypted,
|
||||||
isAuthenticated,
|
|
||||||
allowAnalytics,
|
allowAnalytics,
|
||||||
setDaemonSetting,
|
setDaemonSetting,
|
||||||
clearDaemonSetting,
|
clearDaemonSetting,
|
||||||
|
@ -150,6 +150,14 @@ export default function SettingSystem(props: Props) {
|
||||||
checked={daemonSettings.save_files}
|
checked={daemonSettings.save_files}
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
<SettingsRow title={__('Remote Sync Settings')}>
|
||||||
|
<Button
|
||||||
|
button="inverse"
|
||||||
|
label={__('Manage')}
|
||||||
|
icon={ICONS.ARROW_RIGHT}
|
||||||
|
navigate={`/$/${PAGES.SETTINGS_SYNC}`}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={__('Share usage and diagnostic data')}
|
title={__('Share usage and diagnostic data')}
|
||||||
subtitle={
|
subtitle={
|
||||||
|
@ -168,12 +176,7 @@ export default function SettingSystem(props: Props) {
|
||||||
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
|
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
|
||||||
checked={daemonSettings.share_usage_data}
|
checked={daemonSettings.share_usage_data}
|
||||||
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
|
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
|
||||||
helper={
|
helper={__('Internal sharing is required to participate in rewards programs.')}
|
||||||
isAuthenticated
|
|
||||||
? __('Internal sharing is required while signed in.')
|
|
||||||
: __('Internal sharing is required to participate in rewards programs.')
|
|
||||||
}
|
|
||||||
disabled={isAuthenticated && daemonSettings.share_usage_data}
|
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -276,24 +279,13 @@ export default function SettingSystem(props: Props) {
|
||||||
title={__('Encrypt my wallet with a custom password')}
|
title={__('Encrypt my wallet with a custom password')}
|
||||||
subtitle={
|
subtitle={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<I18nMessage
|
{__('Secure your local wallet data with a custom password.')}{' '}
|
||||||
tokens={{
|
<strong>{__('Lost passwords cannot be recovered.')} </strong>
|
||||||
learn_more: (
|
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />
|
||||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/account-sync" />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Wallet encryption is currently unavailable until it's supported for synced accounts. It will be
|
|
||||||
added back soon. %learn_more%.
|
|
||||||
</I18nMessage>
|
|
||||||
{/* {__('Secure your local wallet data with a custom password.')}{' '}
|
|
||||||
<strong>{__('Lost passwords cannot be recovered.')} </strong>
|
|
||||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. */}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
disabled
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="encrypt_wallet"
|
name="encrypt_wallet"
|
||||||
onChange={() => onChangeEncryptWallet()}
|
onChange={() => onChangeEncryptWallet()}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
|
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
|
||||||
import { selectHomepageData } from 'redux/selectors/settings';
|
import { selectHomepageData } from 'redux/selectors/settings';
|
||||||
import { doSignOut } from 'redux/actions/app';
|
import { doSignOut } from 'redux/actions/app';
|
||||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||||
|
@ -13,10 +12,8 @@ import SideNavigation from './view';
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
subscriptions: selectSubscriptions(state),
|
subscriptions: selectSubscriptions(state),
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
email: selectUserVerifiedEmail(state),
|
|
||||||
purchaseSuccess: selectPurchaseUriSuccess(state),
|
purchaseSuccess: selectPurchaseUriSuccess(state),
|
||||||
unseenCount: selectUnseenNotificationCount(state),
|
unseenCount: selectUnseenNotificationCount(state),
|
||||||
user: selectUser(state),
|
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,11 @@ import * as KEYCODES from 'constants/keycodes';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import NotificationBubble from 'component/notificationBubble';
|
|
||||||
import DebouncedInput from 'component/common/debounced-input';
|
import DebouncedInput from 'component/common/debounced-input';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { useIsMobile, isTouch } from 'effects/use-screensize';
|
import { useIsMobile, isTouch } from 'effects/use-screensize';
|
||||||
import { IS_MAC } from 'component/app/view';
|
import { IS_MAC } from 'component/app/view';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { ENABLE_UI_NOTIFICATIONS } from 'config';
|
|
||||||
|
|
||||||
const FOLLOWED_ITEM_INITIAL_LIMIT = 10;
|
const FOLLOWED_ITEM_INITIAL_LIMIT = 10;
|
||||||
const touch = isTouch();
|
const touch = isTouch();
|
||||||
|
@ -32,7 +30,6 @@ type SideNavLink = {
|
||||||
type Props = {
|
type Props = {
|
||||||
subscriptions: Array<Subscription>,
|
subscriptions: Array<Subscription>,
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
email: ?string,
|
|
||||||
uploadCount: number,
|
uploadCount: number,
|
||||||
doSignOut: () => void,
|
doSignOut: () => void,
|
||||||
sidebarOpen: boolean,
|
sidebarOpen: boolean,
|
||||||
|
@ -42,7 +39,6 @@ type Props = {
|
||||||
unseenCount: number,
|
unseenCount: number,
|
||||||
purchaseSuccess: boolean,
|
purchaseSuccess: boolean,
|
||||||
doClearPurchasedUriSuccess: () => void,
|
doClearPurchasedUriSuccess: () => void,
|
||||||
user: ?User,
|
|
||||||
homepageData: any,
|
homepageData: any,
|
||||||
activeChannelStakedLevel: number,
|
activeChannelStakedLevel: number,
|
||||||
};
|
};
|
||||||
|
@ -51,7 +47,6 @@ function SideNavigation(props: Props) {
|
||||||
const {
|
const {
|
||||||
subscriptions,
|
subscriptions,
|
||||||
doSignOut,
|
doSignOut,
|
||||||
email,
|
|
||||||
purchaseSuccess,
|
purchaseSuccess,
|
||||||
doClearPurchasedUriSuccess,
|
doClearPurchasedUriSuccess,
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
|
@ -59,7 +54,6 @@ function SideNavigation(props: Props) {
|
||||||
isMediumScreen,
|
isMediumScreen,
|
||||||
isOnFilePage,
|
isOnFilePage,
|
||||||
unseenCount,
|
unseenCount,
|
||||||
user,
|
|
||||||
followedTags,
|
followedTags,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -99,13 +93,6 @@ function SideNavigation(props: Props) {
|
||||||
icon: ICONS.PURCHASED,
|
icon: ICONS.PURCHASED,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NOTIFICATIONS = {
|
|
||||||
title: 'Notifications',
|
|
||||||
link: `/$/${PAGES.NOTIFICATIONS}`,
|
|
||||||
icon: ICONS.NOTIFICATION,
|
|
||||||
extra: <NotificationBubble inline />,
|
|
||||||
};
|
|
||||||
|
|
||||||
const PLAYLISTS = {
|
const PLAYLISTS = {
|
||||||
title: 'Lists',
|
title: 'Lists',
|
||||||
link: `/$/${PAGES.LISTS}`,
|
link: `/$/${PAGES.LISTS}`,
|
||||||
|
@ -181,8 +168,6 @@ function SideNavigation(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
|
||||||
|
|
||||||
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||||
const [expandSubscriptions, setExpandSubscriptions] = React.useState(false);
|
const [expandSubscriptions, setExpandSubscriptions] = React.useState(false);
|
||||||
const [expandTags, setExpandTags] = React.useState(false);
|
const [expandTags, setExpandTags] = React.useState(false);
|
||||||
|
@ -241,10 +226,6 @@ function SideNavigation(props: Props) {
|
||||||
const { hideForUnauth, route, link, ...passedProps } = props;
|
const { hideForUnauth, route, link, ...passedProps } = props;
|
||||||
const { title, icon, extra } = passedProps;
|
const { title, icon, extra } = passedProps;
|
||||||
|
|
||||||
if (hideForUnauth && !email) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={route || link || title}>
|
<li key={route || link || title}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -388,15 +369,11 @@ function SideNavigation(props: Props) {
|
||||||
'navigation--push': showPushMenu,
|
'navigation--push': showPushMenu,
|
||||||
'navigation-file-page-and-mobile': hideMenuFromView,
|
'navigation-file-page-and-mobile': hideMenuFromView,
|
||||||
'navigation-touch': touch,
|
'navigation-touch': touch,
|
||||||
// @if TARGET='app'
|
|
||||||
'navigation--mac': IS_MAC,
|
'navigation--mac': IS_MAC,
|
||||||
// @endif
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(!canDisposeMenu || sidebarOpen) && (
|
{(!canDisposeMenu || sidebarOpen) && (
|
||||||
<div className="navigation-inner-container">
|
<div className="navigation-inner-container">
|
||||||
<ul className="navigation-links--absolute mobile-only">{notificationsEnabled && getLink(NOTIFICATIONS)}</ul>
|
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
className={classnames('navigation-links', {
|
className={classnames('navigation-links', {
|
||||||
'navigation-links--micro': showMicroMenu,
|
'navigation-links--micro': showMicroMenu,
|
||||||
|
@ -412,7 +389,7 @@ function SideNavigation(props: Props) {
|
||||||
{getLink(PLAYLISTS)}
|
{getLink(PLAYLISTS)}
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="navigation-links--absolute mobile-only">
|
<ul className="navigation-links--absolute mobile-only">
|
||||||
{email && MOBILE_LINKS.map((linkProps) => getLink(linkProps))}
|
{MOBILE_LINKS.map((linkProps) => getLink(linkProps))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{getSubscriptionSection()}
|
{getSubscriptionSection()}
|
||||||
|
|
|
@ -63,8 +63,6 @@ function SocialShare(props: Props) {
|
||||||
const shareUrl: string = generateShareUrl(
|
const shareUrl: string = generateShareUrl(
|
||||||
shareDomain,
|
shareDomain,
|
||||||
lbryUrl,
|
lbryUrl,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
includeStartTime,
|
includeStartTime,
|
||||||
startTimeSeconds,
|
startTimeSeconds,
|
||||||
includedCollectionId
|
includedCollectionId
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
makeSelectNotificationsDisabled,
|
makeSelectNotificationsDisabled,
|
||||||
} from 'redux/selectors/subscriptions';
|
} from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectPermanentUrlForUri } from 'redux/selectors/claims';
|
import { makeSelectPermanentUrlForUri } from 'redux/selectors/claims';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import SubscribeButton from './view';
|
import SubscribeButton from './view';
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ const select = (state, props) => ({
|
||||||
firstRunCompleted: selectFirstRunCompleted(state),
|
firstRunCompleted: selectFirstRunCompleted(state),
|
||||||
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
|
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
|
||||||
notificationsDisabled: makeSelectNotificationsDisabled(props.uri)(state),
|
notificationsDisabled: makeSelectNotificationsDisabled(props.uri)(state),
|
||||||
user: selectUser(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
|
|
|
@ -21,7 +21,6 @@ type Props = {
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
shrinkOnMobile: boolean,
|
shrinkOnMobile: boolean,
|
||||||
notificationsDisabled: boolean,
|
notificationsDisabled: boolean,
|
||||||
user: ?User,
|
|
||||||
uri: string,
|
uri: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,7 +33,6 @@ export default function SubscribeButton(props: Props) {
|
||||||
doToast,
|
doToast,
|
||||||
shrinkOnMobile = false,
|
shrinkOnMobile = false,
|
||||||
notificationsDisabled,
|
notificationsDisabled,
|
||||||
user,
|
|
||||||
uri,
|
uri,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -42,7 +40,7 @@ export default function SubscribeButton(props: Props) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
let isHovering = useHover(buttonRef);
|
let isHovering = useHover(buttonRef);
|
||||||
isHovering = isMobile ? true : isHovering;
|
isHovering = isMobile ? true : isHovering;
|
||||||
const uiNotificationsEnabled = (user && user.experimental_ui) || ENABLE_UI_NOTIFICATIONS;
|
const uiNotificationsEnabled = ENABLE_UI_NOTIFICATIONS;
|
||||||
|
|
||||||
const { channelName: rawChannelName } = parseURI(uri);
|
const { channelName: rawChannelName } = parseURI(uri);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import {
|
import {
|
||||||
selectGetSyncErrorMessage,
|
selectGetSyncErrorMessage,
|
||||||
selectHasSyncedWallet,
|
selectHasSyncedWallet,
|
||||||
|
@ -17,7 +16,6 @@ const select = (state) => ({
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
hasSyncedWallet: selectHasSyncedWallet(state),
|
hasSyncedWallet: selectHasSyncedWallet(state),
|
||||||
hasSyncChanged: selectHashChanged(state),
|
hasSyncChanged: selectHashChanged(state),
|
||||||
verifiedEmail: selectUserVerifiedEmail(state),
|
|
||||||
getSyncError: selectGetSyncErrorMessage(state),
|
getSyncError: selectGetSyncErrorMessage(state),
|
||||||
getSyncPending: selectGetSyncIsPending(state),
|
getSyncPending: selectGetSyncIsPending(state),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectGetSyncIsPending, selectSyncApplyPasswordError } from 'redux/selectors/sync';
|
import { selectGetSyncIsPending, selectSyncApplyPasswordError } from 'redux/selectors/sync';
|
||||||
import { doGetSyncDesktop } from 'redux/actions/sync';
|
import { doGetSyncDesktop } from 'redux/actions/sync';
|
||||||
import { selectUserEmail } from 'redux/selectors/user';
|
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { doSignOut, doHandleSyncComplete } from 'redux/actions/app';
|
import { doSignOut, doHandleSyncComplete } from 'redux/actions/app';
|
||||||
import SyncPassword from './view';
|
import SyncPassword from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
getSyncIsPending: selectGetSyncIsPending(state),
|
getSyncIsPending: selectGetSyncIsPending(state),
|
||||||
email: selectUserEmail(state),
|
|
||||||
passwordError: selectSyncApplyPasswordError(state),
|
passwordError: selectSyncApplyPasswordError(state),
|
||||||
|
// bring email in from new sync system
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
getSync: (cb, password) => dispatch(doGetSyncDesktop(cb, password)),
|
getSync: (cb, password) => dispatch(doGetSyncDesktop(cb, password)),
|
||||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||||
handleSyncComplete: (error, hasDataChanged) => dispatch(doHandleSyncComplete(error, hasDataChanged)),
|
handleSyncComplete: (error, hasDataChanged) => dispatch(doHandleSyncComplete(error, hasDataChanged)),
|
||||||
|
|
|
@ -12,14 +12,14 @@ import { SITE_HELP_EMAIL } from 'config';
|
||||||
type Props = {
|
type Props = {
|
||||||
getSync: ((any, boolean) => void, ?string) => void,
|
getSync: ((any, boolean) => void, ?string) => void,
|
||||||
getSyncIsPending: boolean,
|
getSyncIsPending: boolean,
|
||||||
email: string,
|
|
||||||
passwordError: boolean,
|
passwordError: boolean,
|
||||||
signOut: () => void,
|
signOut: () => void,
|
||||||
handleSyncComplete: (any, boolean) => void,
|
handleSyncComplete: (any, boolean) => void,
|
||||||
|
email: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SyncPassword(props: Props) {
|
function SyncPassword(props: Props) {
|
||||||
const { getSync, getSyncIsPending, email, signOut, passwordError, handleSyncComplete } = props;
|
const { getSync, getSyncIsPending, signOut, passwordError, handleSyncComplete, email = 'dummy' } = props;
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { search },
|
location: { search },
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doSetWalletSyncPreference } from 'redux/actions/settings';
|
import { doSetWalletSyncPreference } from 'redux/actions/settings';
|
||||||
|
@ -9,7 +8,6 @@ import SyncToggle from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
verifiedEmail: selectUserVerifiedEmail(state),
|
|
||||||
getSyncError: selectGetSyncErrorMessage(state),
|
getSyncError: selectGetSyncErrorMessage(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue