Compare commits

..

6 commits

219 changed files with 2261 additions and 7355 deletions

View file

@ -38,22 +38,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
if: startsWith(runner.os, 'mac')
with:
xcode-version: '13.1.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
xcode-version: '12.4.0'
- name: Download blockchain headers
run: |
@ -73,7 +58,7 @@ jobs:
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_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
# UI

View file

View file

View file

View file

View file

View file

View file

View file

@ -1,53 +1,7 @@
# Changelog
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/).
## [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]
### Added

View file

@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
| | 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) |
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
| 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 | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
## Usage

Binary file not shown.

View file

@ -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();

View file

@ -13,6 +13,7 @@ const config = {
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_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
LBRYSYNC_API: process.env.LBRYSYNC_API,
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
CLOUD_CONNECT_SITE_NAME: process.env.CLOUD_CONNECT_SITE_NAME,
COMMENT_SERVER_API: process.env.COMMENT_SERVER_API,

View file

@ -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);
});
}
}

View file

@ -7,7 +7,6 @@ import https from 'https';
import { app, dialog, ipcMain, session, shell, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import Lbry from 'lbry';
import LbryFirstInstance from './LbryFirstInstance';
import Daemon from './Daemon';
import isDev from 'electron-is-dev';
import createTray from './createTray';
@ -18,14 +17,13 @@ import installDevtools from './installDevtools';
import fs from 'fs';
import path from 'path';
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac } from './sync/sync.js';
const { download } = require('electron-dl');
const mime = require('mime');
const remote = require('@electron/remote/main');
const os = require('os');
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();
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
@ -60,7 +58,6 @@ let rendererWindow;
let tray; // eslint-disable-line
let daemon;
let lbryFirst;
const appState = {};
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
const gotSingleInstanceLock = app.requestSingleInstanceLock();
@ -273,10 +225,6 @@ app.on('will-quit', event => {
daemon.quit();
event.preventDefault();
}
if (lbryFirst) {
lbryFirst.quit();
event.preventDefault();
}
if (rendererWindow) {
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) => {
try {
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', () => {
function formatRc(ver) {
// Adds dash if needed to make RC suffix SemVer friendly
@ -492,15 +439,6 @@ ipcMain.on('version-info-requested', () => {
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 => {
console.log(error);
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
@ -667,3 +605,5 @@ ipcMain.on('upgrade', (event, installerPath) => {
});
app.quit();
});

View 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
View 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
View 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();

View file

@ -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;

View file

@ -1,3 +0,0 @@
import Recsys from './recsys';
export default Recsys;

View file

@ -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;

View file

@ -1,10 +0,0 @@
// @flow
declare type FileData = {
file?: Blob,
path: string,
duration?: number,
size?: number,
mimeType: string,
error?: string,
}

View file

@ -1,6 +1,6 @@
{
"name": "lbry",
"version": "0.53.9",
"version": "0.53.5",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [
"lbry"
@ -51,7 +51,6 @@
"electron-notarize": "^1.0.0",
"electron-updater": "^4.2.4",
"express": "^4.17.1",
"ffmpeg-probe": "^1.0.6",
"humanize-duration": "^3.27.0",
"match-sorter": "^6.3.0",
"mime": "^3.0.0",
@ -115,7 +114,7 @@
"devtron": "^1.4.0",
"dotenv-defaults": "^2.0.1",
"dotenv-webpack": "^1.8.0",
"electron": "17.2.0",
"electron": "17.0.0",
"electron-builder": "^22.10.5",
"electron-devtools-installer": "^3.1.1",
"electron-is-dev": "^0.3.0",
@ -193,6 +192,7 @@
"semver": "^5.3.0",
"strip-markdown": "^3.0.3",
"style-loader": "^0.23.1",
"tape": "^5.6.0",
"terser-webpack-plugin": "^4.2.3",
"three-full": "^28.0.2",
"unist-util-visit": "^2.0.3",
@ -217,7 +217,7 @@
"yarn": "^1.3"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.113.0",
"lbrynetDaemonVersion": "0.110.0",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet"

View file

@ -2318,9 +2318,29 @@
"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 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.",
"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.",
"Creator Comment settings": "Creator Comment settings",
"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",
"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--"
}

View file

@ -1,11 +1,4 @@
// @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 * as Sentry from '@sentry/browser';
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';
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) {
ElectronCookies.enable({
origin: 'https://lbry.tv',
@ -55,7 +51,6 @@ type Analytics = {
) => Promise<any>,
emailProvidedEvent: () => void,
emailVerifiedEvent: () => void,
rewardEligibleEvent: () => void,
startupEvent: () => void,
purchaseEvent: (number) => void,
readyEvent: (number) => void,
@ -72,10 +67,114 @@ type LogPublishParams = {
let internalAnalyticsEnabled: boolean = false;
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 = {
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
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
@ -83,9 +182,40 @@ const analytics: Analytics = {
* @param {object} passedPlayer - VideoJS Player object
*/
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) => {
// 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);
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
},
@ -224,9 +354,6 @@ const analytics: Analytics = {
emailVerifiedEvent: () => {
sendMatomoEvent('Engagement', 'Email-Verified');
},
rewardEligibleEvent: () => {
sendMatomoEvent('Engagement', 'Reward-Eligible');
},
openUrlEvent: (url: string) => {
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({
urlBase: MATOMO_URL,
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));

View file

@ -1,9 +1,6 @@
import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux';
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 { selectMyChannelUrls, selectMyChannelClaimIds } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings';
@ -25,7 +22,7 @@ import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings
import { doSyncLoop } from 'redux/actions/sync';
import {
doDownloadUpgradeRequested,
doSignIn,
doSignIn, // huh
doGetAndPopulatePreferences,
doSetActiveChannel,
doSetIncognito,
@ -34,8 +31,6 @@ import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/c
import App from './view';
const select = (state) => ({
user: selectUser(state),
accessToken: selectAccessToken(state),
theme: selectThemePath(state),
language: selectLanguage(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
@ -43,8 +38,6 @@ const select = (state) => ({
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
isUpgradeAvailable: selectIsUpgradeAvailable(state),
syncError: selectGetSyncErrorMessage(state),
rewards: selectUnclaimedRewards(state),
isAuthenticated: selectUserVerifiedEmail(state),
currentModal: selectModal(state),
syncFatalError: selectSyncFatalError(state),
activeChannelClaim: selectActiveChannelClaim(state),
@ -55,7 +48,6 @@ const select = (state) => ({
});
const perform = (dispatch) => ({
fetchAccessToken: () => dispatch(doFetchAccessToken()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
fetchCollectionListMine: () => dispatch(doFetchCollectionListMine()),
setLanguage: (language) => dispatch(doSetLanguage(language)),
@ -64,7 +56,6 @@ const perform = (dispatch) => ({
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()),
syncLoop: (noInterval) => dispatch(doSyncLoop(noInterval)),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
setIncognito: () => dispatch(doSetIncognito()),
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),

View file

@ -1,16 +1,12 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
import classnames from 'classnames';
import analytics from 'analytics';
import Router from 'component/router/index';
import ReactModal from 'react-modal';
import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import FileRenderFloating from 'component/fileRenderFloating';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import REWARDS from 'rewards';
import usePersistedState from 'effects/use-persisted-state';
import LANGUAGES from 'constants/languages';
import useZoom from 'effects/use-zoom';
@ -46,7 +42,6 @@ type Props = {
length: number,
push: (string) => void,
},
fetchAccessToken: () => void,
fetchChannelListMine: () => void,
fetchCollectionListMine: () => void,
signIn: () => void,
@ -82,27 +77,18 @@ type Props = {
function App(props: Props) {
const {
theme,
user,
fetchAccessToken,
fetchChannelListMine,
fetchCollectionListMine,
signIn,
autoUpdateDownloaded,
isUpgradeAvailable,
requestDownloadUpgrade,
uploadCount,
history,
syncError,
language,
languages,
setLanguage,
updatePreferences,
getWalletSyncPref,
rewards,
setReferrer,
isAuthenticated,
syncLoop,
currentModal,
syncFatalError,
myChannelClaimIds,
activeChannelId,
@ -117,38 +103,16 @@ function App(props: Props) {
const appRef = useRef();
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 [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
// const [retryingSync, setRetryingSync] = useState(false);
const [langRenderKey, setLangRenderKey] = useState(0);
const [sidebarOpen] = usePersistedState('sidebar', true);
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 hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
const hasActiveChannelClaim = activeChannelId !== undefined;
const isPersonalized = hasVerifiedEmail;
useEffect(() => {
if (userId) {
analytics.setUser(userId);
}
}, [userId]);
useEffect(() => {
if (!uploadCount) return;
@ -188,23 +152,10 @@ function App(props: Props) {
}, []);
// Enable ctrl +/- zooming on Desktop.
// @if TARGET='app'
useZoom();
// @endif
// Enable 'Alt + Left/Right' for history navigation on Desktop.
// @if TARGET='app'
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(() => {
const { current: wrapperElement } = appRef;
@ -212,13 +163,9 @@ function App(props: Props) {
ReactModal.setAppElement(wrapperElement);
}
fetchAccessToken();
// @if TARGET='app'
fetchChannelListMine(); // This is fetched after a user is signed in on web
fetchCollectionListMine();
// @endif
}, [appRef, fetchAccessToken, fetchChannelListMine, fetchCollectionListMine]);
}, [appRef, fetchChannelListMine, fetchCollectionListMine]);
useEffect(() => {
// $FlowFixMe
@ -261,71 +208,20 @@ function App(props: Props) {
}, [shouldMigrateLanguage, setLanguage]);
useEffect(() => {
// Check that previousHasVerifiedEmail was not undefined instead of just not truthy
// This ensures we don't fire the emailVerified event on the initial user fetch
if (previousHasVerifiedEmail === false && hasVerifiedEmail) {
analytics.emailVerifiedEvent();
if (updatePreferences && getWalletSyncPref) {
getWalletSyncPref().then(() => updatePreferences());
}
}, [previousHasVerifiedEmail, hasVerifiedEmail, signIn]);
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]);
}, [updatePreferences, getWalletSyncPref]);
// batch resolve subscriptions to be used by the sideNavigation component.
// 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(() => {
if (sidebarOpen && isPersonalized && subscriptions && !resolvedSubscriptions) {
if (sidebarOpen && subscriptions && !resolvedSubscriptions) {
setResolvedSubscriptions(true);
resolveUris(subscriptions.map((sub) => sub.uri));
}
}, [sidebarOpen, isPersonalized, resolvedSubscriptions, subscriptions, resolveUris, setResolvedSubscriptions]);
}, [sidebarOpen, resolvedSubscriptions, subscriptions, resolveUris, setResolvedSubscriptions]);
useEffect(() => {
// When language is changed or translations are fetched, we render.

View file

@ -1,12 +1,9 @@
import Button from './view';
import React, { forwardRef } from 'react';
import { connect } from 'react-redux';
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
const mapStateToProps = (state) => ({
pathname: state.router.location.pathname,
emailVerified: selectUserVerifiedEmail(state),
user: selectUser(state),
});
const ConnectedButton = connect(mapStateToProps)(Button);

View file

@ -32,11 +32,9 @@ type Props = {
onMouseEnter: ?(any) => any,
onMouseLeave: ?(any) => any,
pathname: string,
emailVerified: boolean,
myref: any,
dispatch: any,
'aria-label'?: string,
user: ?User,
};
// 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,
iconColor,
activeClass,
emailVerified,
myref,
dispatch, // <button> doesn't know what to do with dispatch
pathname,
user,
authSrc,
...otherProps
} = props;

View file

@ -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);

View file

@ -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 */

View file

@ -11,7 +11,6 @@ import { doResolveUris } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
import ChannelContent from './view';
@ -29,7 +28,6 @@ const select = (state, props) => {
channelIsMine: selectClaimIsMine(state, claim),
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
claim,
isAuthenticated: selectUserVerifiedEmail(state),
showMature: selectShowMatureContent(state),
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
};

View file

@ -26,7 +26,6 @@ type Props = {
defaultPageSize?: number,
defaultInfiniteScroll?: Boolean,
claim: Claim,
isAuthenticated: boolean,
showMature: boolean,
tileLayout: boolean,
viewHiddenChannels: boolean,

View file

@ -15,8 +15,6 @@ import { selectBalance } from 'redux/selectors/wallet';
import { doUpdateChannel, doCreateChannel, doClearChannelErrors } from 'redux/actions/claims';
import { doOpenModal } from 'redux/actions/app';
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
import { doClaimInitialRewards } from 'redux/actions/rewards';
import { selectIsClaimingInitialRewards, selectHasClaimedInitialRewards } from 'redux/selectors/rewards';
import ChannelForm from './view';
const select = (state, props) => ({
@ -36,8 +34,6 @@ const select = (state, props) => ({
createError: selectCreateChannelError(state),
creatingChannel: selectCreatingChannel(state),
balance: selectBalance(state),
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
});
const perform = (dispatch) => ({
@ -52,7 +48,6 @@ const perform = (dispatch) => ({
);
},
clearChannelErrors: () => dispatch(doClearChannelErrors()),
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
});
export default connect(select, perform)(ChannelForm);

View file

@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
import { FormField } from 'component/common/form';
import Button from 'component/button';
import TagsSearch from 'component/tagsSearch';
import ErrorText from 'component/common/error-text';
@ -51,7 +51,6 @@ type Props = {
createError: string,
creatingChannel: boolean,
clearChannelErrors: () => void,
claimInitialRewards: () => void,
onDone: () => void,
openModal: (
id: string,
@ -59,8 +58,6 @@ type Props = {
) => void,
uri: string,
disabled: boolean,
isClaimingInitialRewards: boolean,
hasClaimedInitialRewards: boolean,
};
function ChannelForm(props: Props) {
@ -85,11 +82,8 @@ function ChannelForm(props: Props) {
creatingChannel,
createError,
clearChannelErrors,
claimInitialRewards,
openModal,
disabled,
isClaimingInitialRewards,
hasClaimedInitialRewards,
} = props;
const [nameError, setNameError] = React.useState(undefined);
const [bidError, setBidError] = React.useState('');
@ -107,21 +101,11 @@ function ChannelForm(props: Props) {
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
const submitLabel = React.useMemo(() => {
if (isClaimingInitialRewards) {
return __('Claiming credits...');
}
return creatingChannel || updatingChannel ? __('Submitting...') : __('Submit');
}, [isClaimingInitialRewards, creatingChannel, updatingChannel]);
}, [creatingChannel, updatingChannel]);
const submitDisabled = React.useMemo(() => {
return (
isClaimingInitialRewards ||
creatingChannel ||
updatingChannel ||
coverError ||
bidError ||
(isNewChannel && !params.name)
);
}, [isClaimingInitialRewards, creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params.name]);
return creatingChannel || updatingChannel || coverError || bidError || (isNewChannel && !params.name);
}, [creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params.name]);
function getChannelParams() {
// fill this in with sdk data
@ -255,12 +239,6 @@ function ChannelForm(props: Props) {
clearChannelErrors();
}, [clearChannelErrors]);
React.useEffect(() => {
if (!hasClaimedInitialRewards) {
claimInitialRewards();
}
}, [hasClaimedInitialRewards, claimInitialRewards]);
const coverSrc = coverError ? ThumbnailBrokenImage : coverPreview;
let thumbnailPreview;
@ -376,7 +354,7 @@ function ChannelForm(props: Props) {
onChange={(e) => setParams({ ...params, title: e.target.value })}
maxLength={MAX_TITLE_LEN}
/>
<FormFieldAreaAdvanced
<FormField
type="markdown"
name="content_description2"
label={__('Description')}

View file

@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
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 { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import { INVALID_NAME_ERROR } from 'constants/claim';
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
usePublishFormMode
/>
</fieldset-section>
<FormFieldAreaAdvanced
<FormField
type="markdown"
name="content_description2"
label={__('Description')}

View file

@ -10,7 +10,6 @@ import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { doToast } from 'redux/actions/notifications';
import { doClearPlayingUri } from 'redux/actions/content';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import {
selectLinkedCommentAncestors,
selectOthersReactsForComment,
@ -33,7 +32,6 @@ const select = (state, props) => {
claim: makeSelectClaimForUri(uri)(state),
thumbnail: author_uri && selectThumbnailForUri(state, author_uri),
channelIsBlocked: author_uri && makeSelectChannelIsMuted(author_uri)(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
othersReacts: selectOthersReactsForComment(state, reactionKey),
activeChannelClaim,
hasChannels: selectHasChannels(state),

View file

@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button';
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 usePersistedState from 'effects/use-persisted-state';
import CommentReactions from 'component/commentReactions';
@ -49,7 +49,6 @@ type Props = {
linkedCommentId?: string,
linkedCommentAncestors: { [string]: Array<string> },
hasChannels: boolean,
commentingEnabled: boolean,
doToast: ({ message: string }) => void,
isTopLevel?: boolean,
threadDepth: number,
@ -82,7 +81,6 @@ function CommentView(props: Props) {
totalReplyPages,
linkedCommentId,
linkedCommentAncestors,
commentingEnabled,
hasChannels,
doToast,
isTopLevel,
@ -319,7 +317,7 @@ function CommentView(props: Props) {
<div>
{isEditing ? (
<Form onSubmit={handleSubmit}>
<FormFieldAreaAdvanced
<FormField
className="comment__edit-input"
type={advancedEditor ? 'markdown' : 'textarea'}
name="editing_comment"
@ -368,7 +366,7 @@ function CommentView(props: Props) {
<div className="comment__actions">
{threadDepth !== 0 && (
<Button
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
label={__('Reply')}
className="comment__action"
onClick={handleCommentReply}
icon={ICONS.REPLY}

View file

@ -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>
);
}

View file

@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
import { buildValidSticker } from 'util/comments';
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 { Lbryio } from 'lbryinc';
import { useHistory } from 'react-router';
@ -22,12 +22,13 @@ import I18nMessage from 'component/i18nMessage';
import Icon from 'component/common/icon';
import OptimizedImage from 'component/optimizedImage';
import React from 'react';
import SelectChannel from 'component/selectChannel';
import StickerSelector from './sticker-selector';
import CommentCreateHeader from './comment-create-header';
import type { ElementRef } from 'react';
import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state';
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
import { getStripeEnvironment } from 'util/stripe';
const stripeEnvironment = getStripeEnvironment();
@ -363,6 +364,31 @@ export function CommentCreate(props: Props) {
.catch(() => {});
}, [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
// **************************************************************************
@ -384,11 +410,7 @@ export function CommentCreate(props: Props) {
push(pathPlusRedirect);
}}
>
<FormFieldAreaAdvanced
type="textarea"
name={'comment_signup_prompt'}
placeholder={__('Say something about this...')}
/>
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
<div className="section__actions--no-margin">
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
</div>
@ -399,22 +421,22 @@ export function CommentCreate(props: Props) {
return (
<Form
onSubmit={() => {}}
className={classnames('comment-create', {
'comment-create--reply': isReply,
'comment-create--nestedReply': isNested,
'comment-create--bottom': bottom,
className={classnames('commentCreate', {
'commentCreate--reply': isReply,
'commentCreate--nestedReply': isNested,
'commentCreate--bottom': bottom,
})}
>
{/* Input Box/Preview Box */}
{stickerSelector ? (
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
<div className="comment-create__stickerPreview">
<div className="comment-create__stickerPreviewInfo">
<div className="commentCreate__stickerPreview">
<div className="commentCreate__stickerPreviewInfo">
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<UriIndicator uri={activeChannelClaim.canonical_url} link />
</div>
<div className="comment-create__stickerPreviewImage">
<div className="commentCreate__stickerPreviewImage">
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
</div>
{/* figure out lbc sticker prices */}
@ -426,15 +448,15 @@ export function CommentCreate(props: Props) {
)}
</div>
) : isReviewingSupportComment && activeChannelClaim ? (
<div className="comment-create__supportCommentPreview">
<div className="commentCreate__supportCommentPreview">
<CreditAmount
amount={tipAmount}
className="comment-create__supportCommentPreviewAmount"
className="commentCreate__supportCommentPreviewAmount"
isFiat={activeTab === TAB_FIAT}
size={activeTab === TAB_LBC ? 18 : 2}
/>
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<div className="comment-create__supportCommentBody">
<div className="commentCreate__supportCommentBody">
<UriIndicator uri={activeChannelClaim.canonical_url} link />
<div>{commentValue}</div>
</div>
@ -449,22 +471,23 @@ export function CommentCreate(props: Props) {
/>
)}
<FormFieldAreaAdvanced
<FormField
autoFocus={isReply}
charCount={charCount}
className={isReply ? 'content_reply' : 'content_comment'}
disabled={isFetchingChannels}
header={
<CommentCreateHeader
isReply={isReply}
advanced={advancedEditor}
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
/>
label={
<div className="commentCreate__labelWrapper">
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
<SelectChannel tiny />
</div>
}
name={isReply ? 'content_reply' : 'content_description'}
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
ref={formFieldRef}
onChange={handleCommentChange}
openEmoteMenu={() => setShowEmotes(!showEmotes)}
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
onFocus={onTextareaFocus}
onBlur={onTextareaBlur}
placeholder={__('Say something about this...')}
@ -632,7 +655,7 @@ export function CommentCreate(props: Props) {
{/* Help Text */}
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
{!!minAmount && (
<div className="help--notice comment-create__minAmountNotice">
<div className="help--notice commentCreate__minAmountNotice">
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
</I18nMessage>

View file

@ -23,9 +23,6 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { getChannelIdFromClaim } from 'util/claim';
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 { uri } = props;
@ -59,19 +56,15 @@ const select = (state, props) => {
myReactsByCommentId: selectMyReacts(state),
othersReactsById: selectOthersReacts(state),
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
};
};
const perform = (dispatch, ownProps) => ({
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)),
setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
});
const perform = {
fetchTopLevelComments: doCommentList,
fetchComment: doCommentById,
fetchReacts: doCommentReactList,
resetComments: doCommentReset,
doResolveUris,
};
export default connect(select, perform)(CommentsList);

View file

@ -1,6 +1,6 @@
// @flow
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 { getCommentsListTitle } from 'util/comments';
import * as ICONS from 'constants/icons';
@ -15,8 +15,6 @@ import Empty from 'component/common/empty';
import React, { useEffect } from 'react';
import Spinner from 'component/spinner';
import usePersistedState from 'effects/use-persisted-state';
import { FormField } from 'component/common/form';
import Comments from 'comments';
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
@ -54,9 +52,6 @@ type Props = {
fetchReacts: (commentIds: Array<string>) => Promise<any>,
resetComments: (claimId: string) => void,
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
customCommentServers: Array<CommentServerDetails>,
setCommentServer: (string) => void,
commentServer: string,
};
export default function CommentList(props: Props) {
@ -85,17 +80,11 @@ export default function CommentList(props: Props) {
fetchReacts,
resetComments,
doResolveUris,
customCommentServers,
setCommentServer,
commentServer,
} = props;
const isMobile = useIsMobile();
const isMediumScreen = useIsMediumScreen();
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
const allServers = [defaultServer, ...(customCommentServers || [])];
const spinnerRef = React.useRef();
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
@ -266,16 +255,7 @@ export default function CommentList(props: Props) {
}, [alreadyResolved, doResolveUris, topLevelComments]);
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
const actionButtonsProps = {
totalComments,
sort,
changeSort,
setPage,
allServers,
commentServer,
defaultServer,
setCommentServer,
};
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
return (
<Card
@ -354,21 +334,17 @@ type ActionButtonsProps = {
sort: string,
changeSort: (string) => void,
setPage: (number) => void,
allServers: Array<CommentServerDetails>,
commentServer: string,
setCommentServer: (string) => void,
defaultServer: CommentServerDetails,
};
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } =
actionButtonsProps;
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
const sortButtonProps = { activeSort: sort, changeSort };
return (
<div className={'comment__actions-row'}>
<>
{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}
@ -377,39 +353,11 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
sortOption={SORT_BY.CONTROVERSY}
/>
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
</div>
</span>
)}
{allServers.length >= 2 && (
<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>
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
</>
);
};

View file

@ -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;

View file

@ -1,7 +1,14 @@
// @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 = {
@ -14,15 +21,19 @@ type Props = {
disabled?: boolean,
error?: string | boolean,
helper?: string | React$Node,
hideSuggestions?: boolean,
inputButton?: React$Node,
isLivestream?: boolean,
label?: string | Node,
labelOnLeft: boolean,
max?: number,
min?: number,
name: string,
noEmojis?: boolean,
placeholder?: string | number,
postfix?: string,
prefix?: string,
quickActionLabel?: string,
range?: number,
readOnly?: boolean,
stretch?: boolean,
@ -30,6 +41,8 @@ type Props = {
type?: string,
value?: string | number,
onChange?: (any) => any,
openEmoteMenu?: () => void,
quickActionHandler?: (any) => any,
render?: () => React$Node,
};
@ -59,15 +72,21 @@ export class FormField extends React.PureComponent<Props> {
children,
error,
helper,
hideSuggestions,
inputButton,
isLivestream,
label,
labelOnLeft,
name,
noEmojis,
postfix,
prefix,
quickActionLabel,
stretch,
textAreaMaxLength,
type,
openEmoteMenu,
quickActionHandler,
render,
...inputProps
} = this.props;
@ -82,10 +101,18 @@ export class FormField extends React.PureComponent<Props> {
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
: ({ 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) => (
<>
<input id={name} type={type} {...inputProps} />
@ -116,22 +143,133 @@ export class FormField extends React.PureComponent<Props> {
return inputSelect('');
case 'select-tiny':
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':
return (
<fieldset-section>
{label && (
{(label || quickAction) && (
<div className="form-field__two-column">
<label htmlFor={name}>{label}</label>
{quickAction}
</div>
)}
<textarea
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
ref={this.input}
{...inputProps}
/>
<div className="form-field__textarea-info">{countInfo}</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>
);
default:

View file

@ -1,5 +1,4 @@
export { Form } from './form-components/form';
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 { Submit } from './form-components/submit';

View file

@ -2054,15 +2054,4 @@ export const icons = {
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
</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>
),
};

View file

@ -157,31 +157,6 @@ export default function CreatorAnalytics(props: Props) {
/>
</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 ? (
<Card
className="section"

View file

@ -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);

View file

@ -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;

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import { selectUser } from 'redux/selectors/user';
import { doOpenFileInFolder } from 'redux/actions/file';
import FileDetails from './view';
@ -10,7 +9,6 @@ const select = (state, props) => ({
contentType: makeSelectContentTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
user: selectUser(state),
});
const perform = (dispatch) => ({

View file

@ -10,7 +10,6 @@ type Props = {
metadata: StreamMetadata,
openFolder: (string) => void,
contentType: string,
user: ?any,
};
class FileDetails extends PureComponent<Props> {

View file

@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen';
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 { useHistory } from 'react-router';
import { isURIEqual } from 'util/lbryURI';
@ -132,7 +132,6 @@ export default function FileRenderFloating(props: Props) {
const playingUriSource = playingUri && playingUri.source;
const isComment = playingUriSource === 'comment';
const isMobile = useIsMobile();
const isMediumScreen = useIsMediumScreen();
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
const [fileViewerRect, setFileViewerRect] = useState();
@ -344,8 +343,7 @@ export default function FileRenderFloating(props: Props) {
'content__viewer--floating': isFloating,
'content__viewer--inline': !isFloating,
'content__viewer--secondary': isComment,
'content__viewer--theater-mode':
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
'content__viewer--disable-click': wasDragging,
})}
style={

View file

@ -9,7 +9,6 @@ import * as PAGES from 'constants/pages';
import * as RENDER_MODES from 'constants/file_render_modes';
import * as KEYCODES from 'constants/keycodes';
import Button from 'component/button';
import { useIsMediumScreen } from 'effects/use-screensize';
import isUserTyping from 'util/detect-typing';
import { getThumbnailCdnUrl } from 'util/thumbnail';
import Nag from 'component/common/nag';
@ -64,7 +63,6 @@ export default function FileRenderInitiator(props: Props) {
const fileStatus = fileInfo && fileInfo.status;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
const isMediumScreen = useIsMediumScreen();
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
const containerRef = React.useRef<any>();
@ -153,7 +151,7 @@ export default function FileRenderInitiator(props: Props) {
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'content__cover--disabled': disabled,
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
'content__cover--theater-mode': videoTheaterMode,
'content__cover--text': isText,
'card__media--nsfw': obscurePreview,
})}

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import { makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
import { makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content';
import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
@ -19,7 +18,6 @@ const select = (state, props) => ({
const perform = (dispatch) => ({
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(connect(select, perform)(FileRenderInline));

View file

@ -11,23 +11,13 @@ type Props = {
renderMode: string,
streamingUrl?: string,
triggerAnalyticsView: (string, number) => Promise<any>,
claimRewards: () => void,
costInfo: any,
claimWasPurchased: boolean,
};
export default function FileRenderInline(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
triggerAnalyticsView,
claimRewards,
renderMode,
costInfo,
claimWasPurchased,
} = props;
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, renderMode, costInfo, claimWasPurchased } =
props;
const [playTime, setPlayTime] = useState();
const isFree = !costInfo || (costInfo.cost !== undefined && costInfo.cost === 0);
const isReadyToView = fileInfo && fileInfo.completed;
@ -55,11 +45,10 @@ export default function FileRenderInline(props: Props) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart).then(() => {
claimRewards();
setPlayTime(null);
});
}
}, [setPlayTime, claimRewards, triggerAnalyticsView, isReadyToPlay, playTime, uri]);
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, playTime, uri]);
if (!isPlaying) {
return null;

View file

@ -13,7 +13,7 @@ const select = (state, props) => {
if (claimUriBeingPlayed) {
const claim = makeSelectClaimForUri(props.uri)(state);
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
isBeingPlayed = claim.claim_id === claimBeingPlayed.claim_id;
}
return {

View file

@ -1,29 +1,25 @@
import { connect } from 'react-redux';
import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user';
import { doSignOut } from 'redux/actions/app';
import { formatCredits } from 'util/format-credits';
import { selectClientSetting } from 'redux/selectors/settings';
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
import { selectHasNavigated } from 'redux/selectors/app';
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
import { selectEmailToVerify, selectUser } from 'redux/selectors/user';
import * as SETTINGS from 'constants/settings';
import { doLbrysyncRegister } from 'redux/actions/sync';
import Header from './view';
const select = (state) => ({
balance: selectBalance(state),
emailToVerify: selectEmailToVerify(state),
hasNavigated: selectHasNavigated(state),
hideBalance: selectClientSetting(state, SETTINGS.HIDE_BALANCE),
roundedBalance: formatCredits(selectTotalBalance(state), 2, true),
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
syncError: selectGetSyncErrorMessage(state),
user: selectUser(state),
});
const perform = (dispatch) => ({
clearEmailEntry: () => dispatch(doClearEmailEntry()),
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
lbrysyncRegister: (username, password) => dispatch(doLbrysyncRegister(username, password)),
signOut: () => dispatch(doSignOut()),
});

View file

@ -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
},
balance: number,
emailToVerify?: string,
hasNavigated: boolean,
hideBalance: boolean,
hideCancel: boolean,
@ -43,8 +42,6 @@ type Props = {
roundedSpendableBalance: string,
sidebarOpen: boolean,
syncError: ?string,
clearEmailEntry: () => void,
clearPasswordEntry: () => void,
setSidebarOpen: (boolean) => void,
signOut: () => void,
};
@ -54,7 +51,6 @@ const Header = (props: Props) => {
authHeader,
backout,
balance,
emailToVerify,
hideBalance,
hideCancel,
history,
@ -63,8 +59,6 @@ const Header = (props: Props) => {
roundedSpendableBalance,
sidebarOpen,
syncError,
clearEmailEntry,
clearPasswordEntry,
setSidebarOpen,
signOut,
} = 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
const isVerifyPage = pathname.includes(PAGES.AUTH_VERIFY);
const isSignUpPage = pathname.includes(PAGES.AUTH);
const isSignInPage = pathname.includes(PAGES.AUTH_SIGNIN);
const isPwdResetPage = pathname.includes(PAGES.AUTH_PASSWORD_RESET);
// 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"
icon={ICONS.REMOVE}
onClick={() => {
clearEmailEntry();
clearPasswordEntry();
if (syncError) signOut();
if ((isSignInPage && !emailToVerify) || isSignUpPage || isPwdResetPage) {
if (isSignUpPage || isPwdResetPage) {
goBack();
} else {
push('/');

View file

@ -4,13 +4,11 @@ import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
import { selectClientSetting } from 'redux/selectors/settings';
import * as SETTINGS from 'constants/settings';
import HeaderMenuButtons from './view';
import { selectUser } from 'redux/selectors/user';
const select = (state) => ({
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
currentTheme: selectClientSetting(state, SETTINGS.THEME),
user: selectUser(state),
});
const perform = (dispatch) => ({

View file

@ -14,14 +14,13 @@ import Tooltip from 'component/common/tooltip';
type HeaderMenuButtonProps = {
automaticDarkModeEnabled: boolean,
currentTheme: string,
user: ?User,
handleThemeToggle: (boolean, string) => void,
};
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 (
<div className="header__buttons">
@ -35,6 +34,7 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
<MenuList className="menu__list--header">
<HeaderMenuLink page={PAGES.UPLOAD} icon={ICONS.PUBLISH} name={__('Upload')} />
<HeaderMenuLink page={PAGES.CHANNEL_NEW} icon={ICONS.CHANNEL} name={__('New Channel')} />
<HeaderMenuLink page={PAGES.SETTINGS_SYNC} icon={ICONS.GAMING} name={__('Sign In')} />
</MenuList>
</Menu>

View file

@ -1,12 +1,10 @@
import { connect } from 'react-redux';
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
import { doSeeAllNotifications } from 'redux/actions/notifications';
import { selectUser } from 'redux/selectors/user';
import NotificationHeaderButton from './view';
const select = (state) => ({
unseenCount: selectUnseenNotificationCount(state),
user: selectUser(state),
});
export default connect(select, {

View file

@ -13,15 +13,14 @@ import Tooltip from 'component/common/tooltip';
type Props = {
unseenCount: number,
user: ?User,
doSeeAllNotifications: () => void,
};
export default function NotificationHeaderButton(props: Props) {
const { unseenCount, user, doSeeAllNotifications } = props;
const { unseenCount, doSeeAllNotifications } = props;
const { push } = useHistory();
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS;
function handleMenuClick() {
if (unseenCount > 0) doSeeAllNotifications();

View file

@ -1,18 +1,9 @@
import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/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';
const select = (state) => ({
activeChannelClaim: selectActiveChannelClaim(state),
email: selectUserEmail(state),
authenticated: selectUserVerifiedEmail(state),
});
const perform = (dispatch) => ({
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
});
export default connect(select, perform)(HeaderProfileMenuButton);
export default connect(select)(HeaderProfileMenuButton);

View file

@ -1,6 +1,6 @@
// @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 PAGES from 'constants/pages';
import ChannelThumbnail from 'component/channelThumbnail';
@ -11,13 +11,10 @@ import React from 'react';
type HeaderMenuButtonProps = {
activeChannelClaim: ?ChannelClaim,
email: ?string,
authenticated: boolean,
openSignOutModal: () => void,
};
export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
const { activeChannelClaim, email, openSignOutModal, authenticated } = props;
const { activeChannelClaim } = props;
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.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
{authenticated ? (
{/* No sync button for now
{authenticated ? (
<MenuItem onSelect={openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
@ -53,8 +50,9 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
<span className="menu__link-help">{email}</span>
</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>
</Menu>
</div>

View file

@ -1,11 +1,9 @@
import { connect } from 'react-redux';
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
import { selectUser } from 'redux/selectors/user';
import NotificationHeaderButton from './view';
const select = (state) => ({
unseenCount: selectUnseenNotificationCount(state),
user: selectUser(state),
});
export default connect(select)(NotificationHeaderButton);

View file

@ -6,12 +6,11 @@ import { ENABLE_UI_NOTIFICATIONS } from 'config';
type Props = {
unseenCount: number,
inline: boolean,
user: ?User,
};
export default function NotificationHeaderButton(props: Props) {
const { unseenCount, inline = false, user } = props;
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
const { unseenCount, inline = false } = props;
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS;
if (unseenCount === 0 || !notificationsEnabled) {
return null;

View file

@ -1,9 +1,3 @@
import { connect } from 'react-redux';
import { selectUser } from 'redux/selectors/user';
import NudgeFloating from './view';
const select = state => ({
user: selectUser(state),
});
export default connect(select)(NudgeFloating);
export default NudgeFloating;

View file

@ -5,22 +5,20 @@ import usePersistedState from 'effects/use-persisted-state';
import Button from 'component/button';
type Props = {
user: ?User,
name: string,
text: string,
};
export default function NudgeFloating(props: Props) {
const { user, name, text } = props;
const { name, text } = props;
const [showNudge, setShowNudge] = React.useState(false);
const [nudgeAcknowledged, setNudgeAcknowledged] = usePersistedState(name, false);
const emailVerified = user && user.has_verified_email;
React.useEffect(() => {
if (!emailVerified && !nudgeAcknowledged) {
if (!nudgeAcknowledged) {
setShowNudge(true);
}
}, [emailVerified, nudgeAcknowledged]);
}, [nudgeAcknowledged]);
return (
showNudge && (

View file

@ -92,7 +92,7 @@ function Page(props: Props) {
<div
className={classnames('main-wrapper__inner', {
'main-wrapper__inner--filepage': isOnFilePage,
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
})}
>
{!authPage &&
@ -124,7 +124,7 @@ function Page(props: Props) {
'main--file-page': filePage,
'main--settings-page': settingsPage,
'main--markdown': isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
})}
>
{children}

View file

@ -1,6 +1,6 @@
// @flow
import React, { useEffect } from 'react';
import { FormFieldAreaAdvanced } from 'component/common/form';
import { FormField } from 'component/common/form';
type Props = {
uri: ?string,
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
]);
return (
<FormFieldAreaAdvanced
<FormField
type={'markdown'}
name="content_post"
label={label}

View file

@ -1,23 +1,11 @@
import { DOMAIN } from 'config';
import { connect } from 'react-redux';
import { doSetDaemonSetting } from 'redux/actions/settings';
import { doSignOut } from 'redux/actions/app';
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';
const select = (state) => ({
authenticated: selectUserVerifiedEmail(state),
});
const perform = (dispatch) => ({
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);

View file

@ -1,7 +1,6 @@
// @flow
import React, { useState } from 'react';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import { FormField } from 'component/common/form-components/form-field';
import { Form } from 'component/common/form-components/form';
import { withRouter } from 'react-router-dom';
@ -14,13 +13,11 @@ const NONE = 'none';
type Props = {
signOut: () => void,
setShareDataInternal: (boolean) => void,
authenticated: boolean,
authenticateIfSharingData: () => void,
handleNextPage: () => void,
};
function PrivacyAgreement(props: Props) {
const { setShareDataInternal, authenticated, signOut, authenticateIfSharingData, handleNextPage } = props;
const { setShareDataInternal, handleNextPage } = props;
const [share, setShare] = useState(undefined); // preload
function handleSubmit() {
@ -30,10 +27,6 @@ function PrivacyAgreement(props: Props) {
setShareDataInternal(false);
}
if (share === LIMITED) {
authenticateIfSharingData();
}
handleNextPage();
}
@ -63,7 +56,6 @@ function PrivacyAgreement(props: Props) {
onChange={(e) => setShare(LIMITED)}
/>
<FormField
disabled={authenticated}
name={'shareNot'}
type="radio"
checked={share === NONE}
@ -77,19 +69,6 @@ function PrivacyAgreement(props: Props) {
)}
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>
<div className={'card__actions'}>
<Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />

View file

@ -2,18 +2,13 @@ import { connect } from 'react-redux';
import { selectPublishFormValues } from 'redux/selectors/publish';
import { doUpdatePublishForm } from 'redux/actions/publish';
import PublishAdditionalOptions from './view';
import { selectUser, selectAccessToken } from 'redux/selectors/user';
import { doFetchAccessToken } from 'redux/actions/user';
const select = (state) => ({
...selectPublishFormValues(state),
accessToken: selectAccessToken(state),
user: selectUser(state),
});
const perform = (dispatch) => ({
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
fetchAccessToken: () => dispatch(doFetchAccessToken()),
});
export default connect(select, perform)(PublishAdditionalOptions);

View file

@ -10,14 +10,7 @@ import Card from 'component/common/card';
import SUPPORTED_LANGUAGES from 'constants/supported_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 = {
user: ?User,
language: ?string,
name: ?string,
licenseType: ?string,
@ -25,92 +18,16 @@ type Props = {
licenseUrl: ?string,
disabled: boolean,
updatePublishForm: ({}) => void,
useLBRYUploader: boolean,
needsYTAuth: boolean,
fetchAccessToken: () => void,
accessToken: string,
};
function PublishAdditionalOptions(props: Props) {
const {
language,
name,
licenseType,
otherLicenseDescription,
licenseUrl,
updatePublishForm,
// user,
// useLBRYUploader,
// needsYTAuth,
// accessToken,
// fetchAccessToken,
} = props;
const { language, name, licenseType, otherLicenseDescription, licenseUrl, updatePublishForm } = props;
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() {
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 (
<Card
className="card--enable-overflow"
@ -118,41 +35,6 @@ function PublishAdditionalOptions(props: Props) {
<React.Fragment>
{!hideSection && (
<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">
<PublishReleaseDate />

View file

@ -1,7 +1,7 @@
// @flow
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import React from 'react';
import { FormFieldAreaAdvanced } from 'component/common/form';
import { FormField } from 'component/common/form';
import usePersistedState from 'effects/use-persisted-state';
import Card from 'component/common/card';
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
return (
<Card
actions={
<FormFieldAreaAdvanced
<FormField
type={advancedEditor ? 'markdown' : 'textarea'}
name="content_description"
label={__('Description')}

View file

@ -14,7 +14,7 @@ import I18nMessage from 'component/i18nMessage';
import usePersistedState from 'effects/use-persisted-state';
import * as PUBLISH_MODES from 'constants/publish_types';
import PublishName from 'component/publishName';
import path from 'path';
type Props = {
uri: ?string,
mode: ?string,
@ -99,27 +99,18 @@ function PublishFile(props: Props) {
if (!filePath) {
return;
}
async function readSelectedFileDetails() {
async function readSelectedFile() {
// Read the file to get the file's duration (if possible)
// and offer transcoding it.
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
let file;
if (result.buffer) {
file = new File([result.buffer], result.name, {
type: result.mime,
});
}
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);
const readFileContents = true;
const result = await ipcRenderer.invoke('get-file-from-path', filePath, readFileContents);
const file = new File([result.buffer], result.name, {
type: result.mime,
});
const fileWithPath = { file, path: result.path };
processSelectedFile(fileWithPath);
}
readSelectedFileDetails();
readSelectedFile();
}, [filePath]);
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;
// select file, start to select a new one, then cancel
if (!fileData || fileData.error) {
if (!fileWithPath) {
if (isStillEditing || !clearName) {
updatePublishForm({ filePath: '' });
} else {
@ -242,11 +233,8 @@ function PublishFile(props: Props) {
}
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
const file = fileData.file;
// Check to see if it's a video and if mp4
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
const duration = fileData.duration;
const size = fileData.size;
const file = fileWithPath.file;
const contentType = file.type && file.type.split('/');
const isVideo = contentType && contentType[0] === 'video';
const isMp4 = contentType && contentType[1] === 'mp4';
@ -254,25 +242,34 @@ function PublishFile(props: Props) {
if (contentType && contentType[0] === 'text') {
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
setCurrentFileType(contentType.join('/'));
} else if (path.parse(fileData.path).ext) {
setCurrentFileType(contentType);
} else if (file.name) {
// If user's machine is missing a valid content type registration
// 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);
}
if (isVideo) {
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 {
updateFileInfo(duration || 0, size, isVideo);
updateFileInfo(0, file.size, isVideo);
}
} else {
updateFileInfo(0, size, isVideo);
updateFileInfo(0, file.size, isVideo);
}
if (isTextPost && file) {
if (isTextPost) {
// Create reader
const reader = new FileReader();
// Handler for file reader
@ -286,7 +283,7 @@ function PublishFile(props: Props) {
// Strip off extension and replace invalid characters
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) });
}
}

View file

@ -17,12 +17,6 @@ import {
} from 'redux/selectors/publish';
import * as RENDER_MODES from 'constants/file_render_modes';
import * as SETTINGS from 'constants/settings';
import { doClaimInitialRewards } from 'redux/actions/rewards';
import {
selectUnclaimedRewardValue,
selectIsClaimingInitialRewards,
selectHasClaimedInitialRewards,
} from 'redux/selectors/rewards';
import {
selectModal,
selectActiveChannelClaim,
@ -31,7 +25,6 @@ import {
} from 'redux/selectors/app';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import { selectUser } from 'redux/selectors/user';
import PublishForm from './view';
const select = (state) => {
@ -41,7 +34,6 @@ const select = (state) => {
return {
...selectPublishFormValues(state),
user: selectUser(state),
// The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state),
isPostClaim,
@ -55,14 +47,11 @@ const select = (state) => {
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
isResolvingUri: selectIsResolvingPublishUris(state),
totalRewardValue: selectUnclaimedRewardValue(state),
modal: selectModal(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
};
};
@ -74,7 +63,6 @@ const perform = (dispatch) => ({
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
});
export default connect(select, perform)(PublishForm);

View file

@ -62,7 +62,6 @@ type Props = {
licenseType: string,
otherLicenseDescription: ?string,
licenseUrl: ?string,
useLBRYUploader: ?boolean,
publishing: boolean,
publishSuccess: boolean,
balance: number,
@ -76,19 +75,14 @@ type Props = {
// Add back type
updatePublishForm: (any) => void,
checkAvailability: (string) => void,
ytSignupPending: boolean,
modal: { id: string, modalProps: {} },
enablePublishPreview: boolean,
activeChannelClaim: ?ChannelClaim,
incognito: boolean,
user: ?User,
activeChannelStakedLevel: number,
isPostClaim: boolean,
permanentUrl: ?string,
remoteUrl: ?string,
isClaimingInitialRewards: boolean,
claimInitialRewards: () => void,
hasClaimedInitialRewards: boolean,
};
function PublishForm(props: Props) {
@ -116,7 +110,6 @@ function PublishForm(props: Props) {
publish,
disabled = false,
checkAvailability,
ytSignupPending,
modal,
enablePublishPreview,
activeChannelClaim,
@ -124,9 +117,6 @@ function PublishForm(props: Props) {
isPostClaim,
permanentUrl,
remoteUrl,
isClaimingInitialRewards,
claimInitialRewards,
hasClaimedInitialRewards,
} = props;
const inEditMode = Boolean(editingURI);
@ -208,6 +198,7 @@ function PublishForm(props: Props) {
isNameValid(name) &&
title &&
bid &&
thumbnail &&
!bidError &&
!emptyPostError &&
!(thumbnailError && !thumbnailUploaded) &&
@ -223,12 +214,6 @@ function PublishForm(props: Props) {
const [previewing, setPreviewing] = React.useState(false);
useEffect(() => {
if (!hasClaimedInitialRewards) {
claimInitialRewards();
}
}, [hasClaimedInitialRewards, claimInitialRewards]);
useEffect(() => {
if (!modal) {
setTimeout(() => {
@ -239,9 +224,7 @@ function PublishForm(props: Props) {
let submitLabel;
if (isClaimingInitialRewards) {
submitLabel = __('Claiming credits...');
} else if (publishing) {
if (publishing) {
if (isStillEditing) {
submitLabel = __('Saving...');
} else {
@ -535,12 +518,7 @@ function PublishForm(props: Props) {
onClick={handlePublish}
label={submitLabel}
disabled={
isClaimingInitialRewards ||
formDisabled ||
!formValid ||
uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS ||
ytSignupPending ||
previewing
formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS || previewing
}
/>
<Button button="link" onClick={clearPublish} label={__('New --[clears Publish Form]--')} />

View file

@ -6,7 +6,6 @@ import Card from 'component/common/card';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import Button from 'component/button';
import classnames from 'classnames';
import RecSys from 'recsys';
const VIEW_ALL_RELATED = 'view_all_related';
const VIEW_MORE_FROM = 'view_more_from';
@ -18,48 +17,20 @@ type Props = {
isSearching: boolean,
doFetchRecommendedContent: (string) => void,
claim: ?StreamClaim,
claimId: string,
};
export default React.memo<Props>(function RecommendedContent(props: Props) {
const {
uri,
doFetchRecommendedContent,
recommendedContentUris,
nextRecommendedUri,
isSearching,
claim,
claimId,
} = props;
const { uri, doFetchRecommendedContent, recommendedContentUris, isSearching, claim } = props;
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
const signingChannel = claim && claim.signing_channel;
const channelName = signingChannel ? signingChannel.name : null;
const isMobile = useIsMobile();
const isMedium = useIsMediumScreen();
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
React.useEffect(() => {
doFetchRecommendedContent(uri);
}, [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 (
<Card
isBodyList
@ -96,7 +67,6 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
uris={recommendedContentUris}
hideMenu={isMobile}
empty={__('No related content found')}
onClick={handleRecommendationClicked}
/>
)}
{viewMode === VIEW_MORE_FROM && signingChannel && (

View file

@ -1,13 +1,10 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectHasNavigated, selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app';
import { selectHomepageData } from 'redux/selectors/settings';
import Router from './view';
import { normalizeURI } from 'util/lbryURI';
import { selectTitleForUri } from 'redux/selectors/claims';
import { doSetHasNavigated } from 'redux/actions/app';
import { doUserSetReferrer } from 'redux/actions/user';
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
const select = (state) => {
const { pathname, hash } = state.router.location;
@ -30,17 +27,14 @@ const select = (state) => {
uri,
title: selectTitleForUri(state, uri),
currentScroll: selectScrollStartingPosition(state),
isAuthenticated: selectUserVerifiedEmail(state),
welcomeVersion: selectWelcomeVersion(state),
hasNavigated: selectHasNavigated(state),
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
homepageData: selectHomepageData(state),
};
};
const perform = (dispatch) => ({
setHasNavigated: () => dispatch(doSetHasNavigated()),
setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
});
export default connect(select, perform)(Router);

View file

@ -5,7 +5,7 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
import * as PAGES from 'constants/pages';
import { PAGE_TITLE } from 'constants/pageTitles';
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 { GetLinksData } from 'util/buildHomepage';
import { useIsLargeScreen } from 'effects/use-screensize';
@ -14,14 +14,9 @@ import HomePage from 'page/home';
import BackupPage from 'page/backup';
// Chunk: "secondary"
import SignInPage from 'page/signIn';
import SignInWalletPasswordPage from 'page/signInWalletPassword';
import SignUpPage from 'page/signUp';
import SignInVerifyPage from 'page/signInVerify';
// Chunk: "wallet/secondary"
import BuyPage from 'page/buy';
import ReceivePage from 'page/receive';
import SendPage from 'page/send';
import WalletPage from 'page/wallet';
@ -43,8 +38,6 @@ import ListBlockedPage from 'page/listBlocked';
import ListsPage from 'page/lists';
import PlaylistsPage from 'page/playlists';
import OwnComments from 'page/ownComments';
import PasswordResetPage from 'page/passwordReset';
import PasswordSetPage from 'page/passwordSet';
import PublishPage from 'page/publish';
import ReportContentPage from 'page/reportContent';
import ReportPage from 'page/report';
@ -53,6 +46,7 @@ import SearchPage from 'page/search';
import SettingsCreatorPage from 'page/settingsCreator';
import SettingsNotificationsPage from 'page/settingsNotifications';
import SettingsSyncPage from 'page/settingsSync';
import SettingsPage from 'page/settings';
import ShowPage from 'page/show';
@ -70,7 +64,6 @@ if ('scrollRestoration' in history) {
type Props = {
currentScroll: number,
isAuthenticated: boolean,
location: { pathname: string, search: string, hash: string },
history: {
action: string,
@ -90,14 +83,12 @@ type Props = {
welcomeVersion: number,
hasNavigated: boolean,
setHasNavigated: () => void,
setReferrer: (?string) => void,
hasUnclaimedRefereeReward: boolean,
homepageData: any,
};
type PrivateRouteProps = Props & {
component: any,
isAuthenticated: boolean,
isAuthenticated?: boolean,
};
function PrivateRoute(props: PrivateRouteProps) {
@ -109,15 +100,12 @@ function AppRouter(props: Props) {
const {
currentScroll,
location: { pathname, search, hash },
isAuthenticated,
history,
uri,
title,
welcomeVersion,
hasNavigated,
setHasNavigated,
hasUnclaimedRefereeReward,
setReferrer,
homepageData,
} = props;
const { entries, listen, action: historyAction } = history;
@ -140,16 +128,6 @@ function AppRouter(props: Props) {
return unlisten;
}, [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(() => {
const getDefaultTitle = (pathname: string) => {
const title = pathname.startsWith('/$/') ? PAGE_TITLE[pathname.substring(3)] : '';
@ -175,9 +153,7 @@ function AppRouter(props: Props) {
document.title = getDefaultTitle(pathname);
}
// @if TARGET='app'
entries[entryIndex].title = document.title;
// @endif
}, [pathname, entries, entryIndex, title, uri]);
useEffect(() => {
@ -227,19 +203,10 @@ function AppRouter(props: Props) {
))}
{/* 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.SETTINGS_SYNC}`} exact component={SettingsSyncPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
{/* @if TARGET='app' */}
<Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} />
{/* @endif */}
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.TOP}`} exact component={TopPage} />
<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.WALLET}`} exact component={WalletPage} />
<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.SEND}`} component={SendPage} />
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />

View file

@ -3,7 +3,6 @@ import SelectChannel from './view';
import { selectBalance } from 'redux/selectors/wallet';
import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims';
import { doFetchChannelListMine, doCreateChannel } from 'redux/actions/claims';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
@ -11,7 +10,6 @@ const select = (state) => ({
myChannelClaims: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
balance: selectBalance(state),
emailVerified: selectUserVerifiedEmail(state),
activeChannelClaim: selectActiveChannelClaim(state),
});

View file

@ -106,7 +106,7 @@ function SelectThumbnail(props: Props) {
__('This will be visible in a few minutes after you submit this form.')}
<img
style={{ display: 'none' }}
src={thumbnail || ThumbnailMissingImage}
src={thumbnail}
alt={__('Thumbnail Preview')}
onError={() => {
if (updateThumbnailParams) {

View file

@ -2,14 +2,11 @@ import { connect } from 'react-redux';
import { selectHasChannels } from 'redux/selectors/claims';
import { selectWalletIsEncrypted } from 'redux/selectors/wallet';
import { doWalletStatus } from 'redux/actions/wallet';
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
import SettingAccount from './view';
const select = (state) => ({
isAuthenticated: selectUserVerifiedEmail(state),
walletEncrypted: selectWalletIsEncrypted(state),
user: selectUser(state),
hasChannels: selectHasChannels(state),
});

View file

@ -6,27 +6,26 @@ import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
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';
type Props = {
isAuthenticated: boolean,
walletEncrypted: boolean,
user: User,
hasChannels: boolean,
doWalletStatus: () => void,
};
export default function SettingAccount(props: Props) {
const { isAuthenticated, walletEncrypted, hasChannels, doWalletStatus } = props;
const [storedPassword, setStoredPassword] = React.useState(false);
const { hasChannels, doWalletStatus } = props;
// const [storedPassword, setStoredPassword] = React.useState(false);
// Determine if password is stored.
React.useEffect(() => {
doWalletStatus();
getPasswordFromCookie().then((p) => {
if (typeof p === 'string') {
setStoredPassword(true);
// get password
}
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
@ -42,18 +41,8 @@ export default function SettingAccount(props: Props) {
isBodyList
body={
<>
{isAuthenticated && (
<SettingsRow title={__('Password')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_UPDATE_PWD}`}
/>
</SettingsRow>
)}
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
{/* This will probably start the new sync flow when checked (-> openModal(SYNC_ENABLE) ) */}
{/* <SyncToggle disabled={true} /> */}
{hasChannels && (
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>

View file

@ -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);

View file

@ -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>
);
}

View file

@ -4,12 +4,10 @@ import * as SETTINGS from 'constants/settings';
import { doSetPlayingUri, clearContentCache } from 'redux/actions/content';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import SettingContent from './view';
const select = (state) => ({
isAuthenticated: selectUserVerifiedEmail(state),
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
autoplayMedia: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state),

View file

@ -3,7 +3,6 @@ import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import React from 'react';
import * as SETTINGS from 'constants/settings';
import { Lbryio } from 'lbryinc';
import { SETTINGS_GRP } from 'constants/settings';
import Button from 'component/button';
import Card from 'component/common/card';
@ -18,7 +17,6 @@ type Price = {
type Props = {
// --- select ---
isAuthenticated: boolean,
floatingPlayer: boolean,
autoplayMedia: boolean,
autoplayNext: boolean,
@ -37,7 +35,6 @@ type Props = {
export default function SettingContent(props: Props) {
const {
isAuthenticated,
floatingPlayer,
autoplayMedia,
autoplayNext,
@ -110,10 +107,6 @@ export default function SettingContent(props: Props) {
type="checkbox"
name="hide_reposts"
onChange={(e) => {
if (isAuthenticated) {
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
Lbryio.call('user_tag', 'edit', param);
}
setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
}}
checked={hideReposts}
@ -172,7 +165,7 @@ export default function SettingContent(props: Props) {
</SettingsRow>
{myChannelUrls && myChannelUrls.length > 0 && (
<SettingsRow title={__('Creator settings')}>
<SettingsRow title={__('Creator Comment settings')}>
<Button
button="inverse"
label={__('Manage')}

View file

@ -6,12 +6,12 @@ import {
doNotifyDecryptWallet,
doNotifyEncryptWallet,
doNotifyForgetPassword,
doOpenModal,
doToggle3PAnalytics,
} from 'redux/actions/app';
import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings';
import { selectAllowAnalytics } from 'redux/selectors/app';
import { selectDaemonSettings, selectFfmpegStatus, selectFindingFFmpeg } from 'redux/selectors/settings';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; // here
import SettingSystem from './view';
@ -20,7 +20,6 @@ const select = (state) => ({
ffmpegStatus: selectFfmpegStatus(state),
findingFFmpeg: selectFindingFFmpeg(state),
walletEncrypted: selectWalletIsEncrypted(state),
isAuthenticated: selectUserVerifiedEmail(state),
allowAnalytics: selectAllowAnalytics(state),
});
@ -34,6 +33,7 @@ const perform = (dispatch) => ({
updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
});
export default connect(select, perform)(SettingSystem);

View file

@ -18,6 +18,8 @@ import { getPasswordFromCookie } from 'util/saved-passwords';
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
import SettingEnablePrereleases from 'component/settingEnablePrereleases';
import SettingDisableAutoUpdates from 'component/settingDisableAutoUpdates';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
const IS_MAC = process.platform === 'darwin';
@ -44,7 +46,6 @@ type Props = {
ffmpegStatus: { available: boolean, which: string },
findingFFmpeg: boolean,
walletEncrypted: boolean,
isAuthenticated: boolean,
allowAnalytics: boolean,
// --- perform ---
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
@ -64,7 +65,6 @@ export default function SettingSystem(props: Props) {
ffmpegStatus,
findingFFmpeg,
walletEncrypted,
isAuthenticated,
allowAnalytics,
setDaemonSetting,
clearDaemonSetting,
@ -150,6 +150,14 @@ export default function SettingSystem(props: Props) {
checked={daemonSettings.save_files}
/>
</SettingsRow>
<SettingsRow title={__('Remote Sync Settings')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_SYNC}`}
/>
</SettingsRow>
<SettingsRow
title={__('Share usage and diagnostic data')}
subtitle={
@ -168,12 +176,7 @@ export default function SettingSystem(props: Props) {
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
checked={daemonSettings.share_usage_data}
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
helper={
isAuthenticated
? __('Internal sharing is required while signed in.')
: __('Internal sharing is required to participate in rewards programs.')
}
disabled={isAuthenticated && daemonSettings.share_usage_data}
helper={__('Internal sharing is required to participate in rewards programs.')}
/>
<FormField
type="checkbox"
@ -276,24 +279,13 @@ export default function SettingSystem(props: Props) {
title={__('Encrypt my wallet with a custom password')}
subtitle={
<React.Fragment>
<I18nMessage
tokens={{
learn_more: (
<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" />. */}
{__('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>
}
>
<FormField
disabled
type="checkbox"
name="encrypt_wallet"
onChange={() => onChangeEncryptWallet()}

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
import { selectFollowedTags } from 'redux/selectors/tags';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
import { selectHomepageData } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app';
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
@ -13,10 +12,8 @@ import SideNavigation from './view';
const select = (state) => ({
subscriptions: selectSubscriptions(state),
followedTags: selectFollowedTags(state),
email: selectUserVerifiedEmail(state),
purchaseSuccess: selectPurchaseUriSuccess(state),
unseenCount: selectUnseenNotificationCount(state),
user: selectUser(state),
homepageData: selectHomepageData(state),
});

View file

@ -6,13 +6,11 @@ import * as KEYCODES from 'constants/keycodes';
import React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import NotificationBubble from 'component/notificationBubble';
import DebouncedInput from 'component/common/debounced-input';
import ChannelThumbnail from 'component/channelThumbnail';
import { useIsMobile, isTouch } from 'effects/use-screensize';
import { IS_MAC } from 'component/app/view';
import { useHistory } from 'react-router';
import { ENABLE_UI_NOTIFICATIONS } from 'config';
const FOLLOWED_ITEM_INITIAL_LIMIT = 10;
const touch = isTouch();
@ -32,7 +30,6 @@ type SideNavLink = {
type Props = {
subscriptions: Array<Subscription>,
followedTags: Array<Tag>,
email: ?string,
uploadCount: number,
doSignOut: () => void,
sidebarOpen: boolean,
@ -42,7 +39,6 @@ type Props = {
unseenCount: number,
purchaseSuccess: boolean,
doClearPurchasedUriSuccess: () => void,
user: ?User,
homepageData: any,
activeChannelStakedLevel: number,
};
@ -51,7 +47,6 @@ function SideNavigation(props: Props) {
const {
subscriptions,
doSignOut,
email,
purchaseSuccess,
doClearPurchasedUriSuccess,
sidebarOpen,
@ -59,7 +54,6 @@ function SideNavigation(props: Props) {
isMediumScreen,
isOnFilePage,
unseenCount,
user,
followedTags,
} = props;
@ -99,13 +93,6 @@ function SideNavigation(props: Props) {
icon: ICONS.PURCHASED,
};
const NOTIFICATIONS = {
title: 'Notifications',
link: `/$/${PAGES.NOTIFICATIONS}`,
icon: ICONS.NOTIFICATION,
extra: <NotificationBubble inline />,
};
const PLAYLISTS = {
title: '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 [expandSubscriptions, setExpandSubscriptions] = React.useState(false);
const [expandTags, setExpandTags] = React.useState(false);
@ -241,10 +226,6 @@ function SideNavigation(props: Props) {
const { hideForUnauth, route, link, ...passedProps } = props;
const { title, icon, extra } = passedProps;
if (hideForUnauth && !email) {
return null;
}
return (
<li key={route || link || title}>
<Button
@ -388,15 +369,11 @@ function SideNavigation(props: Props) {
'navigation--push': showPushMenu,
'navigation-file-page-and-mobile': hideMenuFromView,
'navigation-touch': touch,
// @if TARGET='app'
'navigation--mac': IS_MAC,
// @endif
})}
>
{(!canDisposeMenu || sidebarOpen) && (
<div className="navigation-inner-container">
<ul className="navigation-links--absolute mobile-only">{notificationsEnabled && getLink(NOTIFICATIONS)}</ul>
<ul
className={classnames('navigation-links', {
'navigation-links--micro': showMicroMenu,
@ -412,7 +389,7 @@ function SideNavigation(props: Props) {
{getLink(PLAYLISTS)}
</ul>
<ul className="navigation-links--absolute mobile-only">
{email && MOBILE_LINKS.map((linkProps) => getLink(linkProps))}
{MOBILE_LINKS.map((linkProps) => getLink(linkProps))}
</ul>
{getSubscriptionSection()}

View file

@ -63,8 +63,6 @@ function SocialShare(props: Props) {
const shareUrl: string = generateShareUrl(
shareDomain,
lbryUrl,
null,
null,
includeStartTime,
startTimeSeconds,
includedCollectionId

View file

@ -6,7 +6,6 @@ import {
makeSelectNotificationsDisabled,
} from 'redux/selectors/subscriptions';
import { makeSelectPermanentUrlForUri } from 'redux/selectors/claims';
import { selectUser } from 'redux/selectors/user';
import { doToast } from 'redux/actions/notifications';
import SubscribeButton from './view';
@ -15,7 +14,6 @@ const select = (state, props) => ({
firstRunCompleted: selectFirstRunCompleted(state),
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
notificationsDisabled: makeSelectNotificationsDisabled(props.uri)(state),
user: selectUser(state),
});
export default connect(select, {

View file

@ -21,7 +21,6 @@ type Props = {
doToast: ({ message: string }) => void,
shrinkOnMobile: boolean,
notificationsDisabled: boolean,
user: ?User,
uri: string,
};
@ -34,7 +33,6 @@ export default function SubscribeButton(props: Props) {
doToast,
shrinkOnMobile = false,
notificationsDisabled,
user,
uri,
} = props;
@ -42,7 +40,7 @@ export default function SubscribeButton(props: Props) {
const isMobile = useIsMobile();
let isHovering = useHover(buttonRef);
isHovering = isMobile ? true : isHovering;
const uiNotificationsEnabled = (user && user.experimental_ui) || ENABLE_UI_NOTIFICATIONS;
const uiNotificationsEnabled = ENABLE_UI_NOTIFICATIONS;
const { channelName: rawChannelName } = parseURI(uri);

View file

@ -1,6 +1,5 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import {
selectGetSyncErrorMessage,
selectHasSyncedWallet,
@ -17,7 +16,6 @@ const select = (state) => ({
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
hasSyncedWallet: selectHasSyncedWallet(state),
hasSyncChanged: selectHashChanged(state),
verifiedEmail: selectUserVerifiedEmail(state),
getSyncError: selectGetSyncErrorMessage(state),
getSyncPending: selectGetSyncIsPending(state),
});

View file

@ -1,18 +1,17 @@
import { connect } from 'react-redux';
import { selectGetSyncIsPending, selectSyncApplyPasswordError } from 'redux/selectors/sync';
import { doGetSyncDesktop } from 'redux/actions/sync';
import { selectUserEmail } from 'redux/selectors/user';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSignOut, doHandleSyncComplete } from 'redux/actions/app';
import SyncPassword from './view';
const select = state => ({
const select = (state) => ({
getSyncIsPending: selectGetSyncIsPending(state),
email: selectUserEmail(state),
passwordError: selectSyncApplyPasswordError(state),
// bring email in from new sync system
});
const perform = dispatch => ({
const perform = (dispatch) => ({
getSync: (cb, password) => dispatch(doGetSyncDesktop(cb, password)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
handleSyncComplete: (error, hasDataChanged) => dispatch(doHandleSyncComplete(error, hasDataChanged)),

View file

@ -12,14 +12,14 @@ import { SITE_HELP_EMAIL } from 'config';
type Props = {
getSync: ((any, boolean) => void, ?string) => void,
getSyncIsPending: boolean,
email: string,
passwordError: boolean,
signOut: () => void,
handleSyncComplete: (any, boolean) => void,
email: string,
};
function SyncPassword(props: Props) {
const { getSync, getSyncIsPending, email, signOut, passwordError, handleSyncComplete } = props;
const { getSync, getSyncIsPending, signOut, passwordError, handleSyncComplete, email = 'dummy' } = props;
const {
push,
location: { search },

View file

@ -1,6 +1,5 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetWalletSyncPreference } from 'redux/actions/settings';
@ -9,7 +8,6 @@ import SyncToggle from './view';
const select = (state) => ({
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
verifiedEmail: selectUserVerifiedEmail(state),
getSyncError: selectGetSyncErrorMessage(state),
});

Some files were not shown because too many files have changed in this diff Show more