Compare commits
9 commits
master
...
Comment-se
Author | SHA1 | Date | |
---|---|---|---|
|
1038181386 | ||
|
d2e147f911 | ||
|
b6e71a880e | ||
|
5baa12ec3d | ||
|
d724676daf | ||
|
250e9eeebc | ||
|
fd2d817ba7 | ||
|
ef27cb5a9b | ||
|
1c9c0a4cbf |
109 changed files with 2889 additions and 1804 deletions
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,22 +38,7 @@ jobs:
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
if: startsWith(runner.os, 'mac')
|
if: startsWith(runner.os, 'mac')
|
||||||
with:
|
with:
|
||||||
xcode-version: '13.1.0'
|
xcode-version: '12.4.0'
|
||||||
# This is gonna be hacky.
|
|
||||||
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
|
||||||
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
|
||||||
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
|
||||||
# So for now we'll install python2 on mac and make sure it can find it.
|
|
||||||
# Remove this after successfully upgrading electron-builder.
|
|
||||||
# HACK part 1
|
|
||||||
- uses: Homebrew/actions/setup-homebrew@master
|
|
||||||
if: startsWith(runner.os, 'mac')
|
|
||||||
# HACK part 2
|
|
||||||
- name: Install Python2
|
|
||||||
if: startsWith(runner.os, 'mac')
|
|
||||||
run: |
|
|
||||||
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
|
||||||
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Download blockchain headers
|
- name: Download blockchain headers
|
||||||
run: |
|
run: |
|
||||||
|
@ -73,7 +58,7 @@ jobs:
|
||||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx
|
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
|
||||||
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
0
.yarn/versions/5bc94294.yml
vendored
0
.yarn/versions/5bc94294.yml
vendored
0
.yarn/versions/5f1212ad.yml
vendored
0
.yarn/versions/5f1212ad.yml
vendored
0
.yarn/versions/6b35c994.yml
vendored
0
.yarn/versions/6b35c994.yml
vendored
0
.yarn/versions/6be5ab70.yml
vendored
0
.yarn/versions/6be5ab70.yml
vendored
0
.yarn/versions/8e384637.yml
vendored
0
.yarn/versions/8e384637.yml
vendored
0
.yarn/versions/c6e2b914.yml
vendored
0
.yarn/versions/c6e2b914.yml
vendored
0
.yarn/versions/d1a18cef.yml
vendored
0
.yarn/versions/d1a18cef.yml
vendored
0
.yarn/versions/ec3a9ddf.yml
vendored
0
.yarn/versions/ec3a9ddf.yml
vendored
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -1,73 +1,22 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
## [Unreleased for desktop]
|
||||||
## [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
|
### Added
|
||||||
- Checkbox to disable background wallpaper ([#7630](https://github.com/lbryio/lbry-desktop/pull/7630))
|
- Checkbox to disable background wallpaper ([#7630](https://github.com/lbryio/lbry-desktop/pull/7630))
|
||||||
- Handle content blocking from hub ([#7665](https://github.com/lbryio/lbry-desktop/pull/7665))
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Better handle decimals liquidating supports ([#7648](https://github.com/lbryio/lbry-desktop/pull/7648))
|
|
||||||
- Better handle cover uploads ([#7647](https://github.com/lbryio/lbry-desktop/pull/7647))
|
- Better handle cover uploads ([#7647](https://github.com/lbryio/lbry-desktop/pull/7647))
|
||||||
- Use default path when first choosing file on windows ([#7625](https://github.com/lbryio/lbry-desktop/pull/7625))
|
- Use default path when first choosing file on windows ([#7625](https://github.com/lbryio/lbry-desktop/pull/7625))
|
||||||
- Emoji button hover ([#7620](https://github.com/lbryio/lbry-desktop/pull/7620))
|
- Emoji button hover ([#7620](https://github.com/lbryio/lbry-desktop/pull/7620))
|
||||||
- Prevent infinite retries on thumbs ([#7618](https://github.com/lbryio/lbry-desktop/pull/7618))
|
- Prevent infinite retries on thumbs ([#7618](https://github.com/lbryio/lbry-desktop/pull/7618))
|
||||||
- Double splash/error on app startup ([#7615](https://github.com/lbryio/lbry-desktop/pull/7615))
|
- Double splash/error on app startup ([#7615](https://github.com/lbryio/lbry-desktop/pull/7615))
|
||||||
- App updates are now more coherent, also debs work. ([#7502](https://github.com/lbryio/lbry-desktop/pull/7502))
|
- App updates are now more coherent, also debs work. ([#7502](https://github.com/lbryio/lbry-desktop/pull/7502))
|
||||||
- Better handle many channels moderation calls at startup ([#7674](https://github.com/lbryio/lbry-desktop/pull/7674))
|
|
||||||
- Fix mobile floating viewer position ([#7677](https://github.com/lbryio/lbry-desktop/pull/7677))
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Upgraded Electron to v15.5.5 ([#7614](https://github.com/lbryio/lbry-desktop/pull/7614))
|
- Upgraded Electron to v15.5.5 ([#7614](https://github.com/lbryio/lbry-desktop/pull/7614))
|
||||||
- Upgraded to lbrynet v0.110.0 ([#7680](https://github.com/lbryio/lbry-desktop/pull/7680))
|
|
||||||
|
|
||||||
|
|
||||||
## [0.53.4] - [2022-06-10]
|
## [0.53.4] - [2022-06-10]
|
||||||
|
|
|
@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
|
||||||
|
|
||||||
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
||||||
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||||
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ Start the installed application to interact with the LBRY network.
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [Node.js](https://nodejs.org/en/download/) (v16 required)
|
- [Node.js](https://nodejs.org/en/download/) (v14 required)
|
||||||
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
|
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
|
||||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -20,12 +20,9 @@ import path from 'path';
|
||||||
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
||||||
|
|
||||||
const { download } = require('electron-dl');
|
const { download } = require('electron-dl');
|
||||||
const mime = require('mime');
|
|
||||||
const remote = require('@electron/remote/main');
|
const remote = require('@electron/remote/main');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const sudo = require('sudo-prompt');
|
const sudo = require('sudo-prompt');
|
||||||
const probe = require('ffmpeg-probe');
|
|
||||||
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
|
|
||||||
|
|
||||||
remote.initialize();
|
remote.initialize();
|
||||||
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
||||||
|
@ -302,96 +299,6 @@ app.on('before-quit', () => {
|
||||||
appState.isQuitting = true;
|
appState.isQuitting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the content of a file as a raw buffer of bytes.
|
|
||||||
// Useful to convert a file path to a File instance.
|
|
||||||
// Example:
|
|
||||||
// const result = await ipcMain.invoke('get-file-from-path', 'path/to/file');
|
|
||||||
// const file = new File([result.buffer], result.name);
|
|
||||||
// NOTE: if path points to a folder, an empty
|
|
||||||
// file will be given.
|
|
||||||
ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.stat(path, (error, stats) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Separate folders considering "\" and "/"
|
|
||||||
// as separators (cross platform)
|
|
||||||
const folders = path.split(/[\\/]/);
|
|
||||||
const name = folders[folders.length - 1];
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
resolve({
|
|
||||||
name,
|
|
||||||
mime: undefined,
|
|
||||||
path,
|
|
||||||
buffer: new ArrayBuffer(0),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!readContents) {
|
|
||||||
resolve({
|
|
||||||
name,
|
|
||||||
mime: mime.getType(name) || undefined,
|
|
||||||
path,
|
|
||||||
buffer: new ArrayBuffer(0),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Encoding null ensures data results in a Buffer.
|
|
||||||
fs.readFile(path, { encoding: null }, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve({
|
|
||||||
name,
|
|
||||||
mime: mime.getType(name) || undefined,
|
|
||||||
path,
|
|
||||||
buffer: data,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-file-details-from-path', async (event, path) => {
|
|
||||||
const isFfMp4 = (ffprobeResults) => {
|
|
||||||
return ffprobeResults &&
|
|
||||||
ffprobeResults.format &&
|
|
||||||
ffprobeResults.format.format_name &&
|
|
||||||
ffprobeResults.format.format_name.includes('mp4');
|
|
||||||
};
|
|
||||||
const folders = path.split(/[\\/]/);
|
|
||||||
const name = folders[folders.length - 1];
|
|
||||||
let duration = 0, size = 0, mimeType;
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(path);
|
|
||||||
let ffprobeResults;
|
|
||||||
try {
|
|
||||||
ffprobeResults = await probe(path);
|
|
||||||
duration = ffprobeResults.format.duration;
|
|
||||||
size = ffprobeResults.format.size;
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
let fileReadResult;
|
|
||||||
if (size < MAX_IPC_SEND_BUFFER_SIZE) {
|
|
||||||
try {
|
|
||||||
fileReadResult = await fs.promises.readFile(path);
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: use mmmagic to inspect file and get mime type
|
|
||||||
mimeType = isFfMp4(ffprobeResults) ? 'video/mp4' : mime.getType(name);
|
|
||||||
const fileData = {name, mime: mimeType || undefined, path, duration: duration, size, buffer: fileReadResult };
|
|
||||||
return fileData;
|
|
||||||
} catch (e) {
|
|
||||||
// no stat
|
|
||||||
return { error: 'no file' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('get-disk-space', async (event) => {
|
ipcMain.on('get-disk-space', async (event) => {
|
||||||
try {
|
try {
|
||||||
const { data_dir } = await Lbry.settings_get();
|
const { data_dir } = await Lbry.settings_get();
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// involve moving it from 'extras' to 'ui' (big change).
|
// involve moving it from 'extras' to 'ui' (big change).
|
||||||
|
|
||||||
import { createCachedSelector } from 're-reselect';
|
import { createCachedSelector } from 're-reselect';
|
||||||
import { selectClaimForUri, makeSelectIsBlacklisted } from 'redux/selectors/claims';
|
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||||
|
@ -18,8 +18,7 @@ export const selectBanStateForUri = createCachedSelector(
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
selectMutedChannels,
|
selectMutedChannels,
|
||||||
selectModerationBlockList,
|
selectModerationBlockList,
|
||||||
(state, uri) => makeSelectIsBlacklisted(uri)(state),
|
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist) => {
|
||||||
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist, isBlacklisted) => {
|
|
||||||
const banState = {};
|
const banState = {};
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
|
@ -28,10 +27,6 @@ export const selectBanStateForUri = createCachedSelector(
|
||||||
|
|
||||||
const channelClaim = getChannelFromClaim(claim);
|
const channelClaim = getChannelFromClaim(claim);
|
||||||
|
|
||||||
if (isBlacklisted) {
|
|
||||||
banState['blacklisted'] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will be replaced once blocking is done at the wallet server level.
|
// This will be replaced once blocking is done at the wallet server level.
|
||||||
if (blackListedOutpointMap) {
|
if (blackListedOutpointMap) {
|
||||||
if (
|
if (
|
||||||
|
|
37
flow-typed/Claim.js
vendored
37
flow-typed/Claim.js
vendored
|
@ -145,49 +145,12 @@ declare type PurchaseReceipt = {
|
||||||
type: 'purchase',
|
type: 'purchase',
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type ClaimErrorCensor = {
|
|
||||||
address: string,
|
|
||||||
amount: string,
|
|
||||||
canonical_url: string,
|
|
||||||
claim_id: string,
|
|
||||||
claim_op: string,
|
|
||||||
confirmations: number,
|
|
||||||
has_signing_key: boolean,
|
|
||||||
height: number,
|
|
||||||
meta: {
|
|
||||||
activation_height: number,
|
|
||||||
claims_in_channel: number,
|
|
||||||
creation_height: number,
|
|
||||||
creation_timestamp: number,
|
|
||||||
effective_amount: string,
|
|
||||||
expiration_height: number,
|
|
||||||
is_controlling: boolean,
|
|
||||||
reposted: number,
|
|
||||||
support_amount: string,
|
|
||||||
take_over_height: number,
|
|
||||||
},
|
|
||||||
name: string,
|
|
||||||
normalized_name: string,
|
|
||||||
nout: number,
|
|
||||||
permanent_url: string,
|
|
||||||
short_url: string,
|
|
||||||
timestamp: number,
|
|
||||||
txid: string,
|
|
||||||
type: string,
|
|
||||||
value: {
|
|
||||||
public_key: string,
|
|
||||||
public_key_id: string,
|
|
||||||
},
|
|
||||||
value_type: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
declare type ClaimActionResolveInfo = {
|
declare type ClaimActionResolveInfo = {
|
||||||
[string]: {
|
[string]: {
|
||||||
stream: ?StreamClaim,
|
stream: ?StreamClaim,
|
||||||
channel: ?ChannelClaim,
|
channel: ?ChannelClaim,
|
||||||
claimsInChannel: ?number,
|
claimsInChannel: ?number,
|
||||||
collection: ?CollectionClaim,
|
collection: ?CollectionClaim,
|
||||||
errorCensor: ?ClaimErrorCensor,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
flow-typed/file-data.js
vendored
10
flow-typed/file-data.js
vendored
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
declare type FileData = {
|
|
||||||
file?: Blob,
|
|
||||||
path: string,
|
|
||||||
duration?: number,
|
|
||||||
size?: number,
|
|
||||||
mimeType: string,
|
|
||||||
error?: string,
|
|
||||||
}
|
|
9
flow-typed/file-with-path.js
vendored
9
flow-typed/file-with-path.js
vendored
|
@ -1,9 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
declare type FileWithPath = {
|
|
||||||
file: File,
|
|
||||||
// The full path will only be available in
|
|
||||||
// the application. For browser, the name
|
|
||||||
// of the file will be used.
|
|
||||||
path: string,
|
|
||||||
}
|
|
6
flow-typed/web-file.js
vendored
Normal file
6
flow-typed/web-file.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type WebFile = File & {
|
||||||
|
path?: string,
|
||||||
|
title?: string,
|
||||||
|
}
|
41
package.json
41
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lbry",
|
"name": "lbry",
|
||||||
"version": "0.53.9",
|
"version": "0.53.4",
|
||||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lbry"
|
"lbry"
|
||||||
|
@ -41,29 +41,32 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.1",
|
"@electron/remote": "^2.0.1",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.6.0",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.6.0",
|
||||||
"@mui/material": "^5.2.1",
|
"@mui/material": "^5.2.1",
|
||||||
"@ungap/from-entries": "^0.2.1",
|
"@ungap/from-entries": "^0.2.1",
|
||||||
"auto-launch": "^5.0.5",
|
"auto-launch": "^5.0.5",
|
||||||
"electron-dl": "^3.2.0",
|
"electron-dl": "^3.2.0",
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^2.2.12",
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ffmpeg-probe": "^1.0.6",
|
|
||||||
"humanize-duration": "^3.27.0",
|
"humanize-duration": "^3.27.0",
|
||||||
|
"if-env": "^1.0.4",
|
||||||
"match-sorter": "^6.3.0",
|
"match-sorter": "^6.3.0",
|
||||||
"mime": "^3.0.0",
|
|
||||||
"node-html-parser": "^5.1.0",
|
"node-html-parser": "^5.1.0",
|
||||||
"parse-duration": "^1.0.0",
|
"parse-duration": "^1.0.0",
|
||||||
"proxy-polyfill": "0.1.6",
|
"proxy-polyfill": "0.1.6",
|
||||||
"re-reselect": "^4.0.0",
|
"re-reselect": "^4.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
|
"react-color": "^2.19.3",
|
||||||
"react-datetime-picker": "^3.4.3",
|
"react-datetime-picker": "^3.4.3",
|
||||||
|
"remove-markdown": "^0.3.0",
|
||||||
|
"rss": "^1.2.2",
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tempy": "^0.6.0"
|
"tempy": "^0.6.0",
|
||||||
|
"videojs-logo": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
|
@ -74,7 +77,7 @@
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.3",
|
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||||
"@babel/polyfill": "^7.12.1",
|
"@babel/polyfill": "^7.2.5",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-flow": "^7.12.1",
|
"@babel/preset-flow": "^7.12.1",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
@ -82,6 +85,7 @@
|
||||||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||||
"@hot-loader/react-dom": "^16.13",
|
"@hot-loader/react-dom": "^16.13",
|
||||||
"@meetfranz/electron-cookies": "^3.0.2",
|
"@meetfranz/electron-cookies": "^3.0.2",
|
||||||
|
"@reach/auto-id": "^0.13.0",
|
||||||
"@reach/combobox": "^0.12.1",
|
"@reach/combobox": "^0.12.1",
|
||||||
"@reach/menu-button": "0.8.6",
|
"@reach/menu-button": "0.8.6",
|
||||||
"@reach/rect": "^0.16.0",
|
"@reach/rect": "^0.16.0",
|
||||||
|
@ -92,17 +96,21 @@
|
||||||
"@sentry/webpack-plugin": "^1.10.0",
|
"@sentry/webpack-plugin": "^1.10.0",
|
||||||
"@types/three": "^0.103.2",
|
"@types/three": "^0.103.2",
|
||||||
"adm-zip": "^0.4.13",
|
"adm-zip": "^0.4.13",
|
||||||
|
"async-exit-hook": "^2.0.1",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"babel-plugin-add-module-exports": "^1.0.4",
|
"babel-plugin-add-module-exports": "^1.0.4",
|
||||||
"babel-plugin-import-glob": "^2.0.0",
|
"babel-plugin-import-glob": "^2.0.0",
|
||||||
"babel-plugin-transform-imports": "^1.5.1",
|
"babel-plugin-transform-imports": "^1.5.1",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
|
"bluebird": "^3.5.1",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"codemirror": "^5.39.2",
|
"codemirror": "^5.39.2",
|
||||||
|
"concurrently": "^4.1.2",
|
||||||
"connected-react-router": "^6.8.0",
|
"connected-react-router": "^6.8.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
|
"copyfiles": "^2.4.1",
|
||||||
"country-data": "^0.0.31",
|
"country-data": "^0.0.31",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
|
@ -113,9 +121,10 @@
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
|
"dom-scroll-into-view": "^1.2.1",
|
||||||
"dotenv-defaults": "^2.0.1",
|
"dotenv-defaults": "^2.0.1",
|
||||||
"dotenv-webpack": "^1.8.0",
|
"dotenv-webpack": "^1.8.0",
|
||||||
"electron": "17.2.0",
|
"electron": "15.5.5",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
|
@ -150,7 +159,10 @@
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mammoth": "^1.4.16",
|
"mammoth": "^1.4.16",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.2",
|
||||||
|
"node-abi": "^2.5.1",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
|
"node-html-parser": "^5.1.0",
|
||||||
|
"node-libs-browser": "^2.1.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-wget": "^0.4.3",
|
"node-wget": "^0.4.3",
|
||||||
"nodemon": "^1.19.1",
|
"nodemon": "^1.19.1",
|
||||||
|
@ -165,6 +177,7 @@
|
||||||
"rc-progress": "^2.0.6",
|
"rc-progress": "^2.0.6",
|
||||||
"react": "^16.8.2",
|
"react": "^16.8.2",
|
||||||
"react-awesome-lightbox": "^1.7.3",
|
"react-awesome-lightbox": "^1.7.3",
|
||||||
|
"react-confetti": "^4.0.1",
|
||||||
"react-dom": "^16.8.2",
|
"react-dom": "^16.8.2",
|
||||||
"react-draggable": "^3.3.0",
|
"react-draggable": "^3.3.0",
|
||||||
"react-google-recaptcha": "^2.0.1",
|
"react-google-recaptcha": "^2.0.1",
|
||||||
|
@ -175,6 +188,7 @@
|
||||||
"react-router": "^5.1.0",
|
"react-router": "^5.1.0",
|
||||||
"react-router-dom": "^5.1.0",
|
"react-router-dom": "^5.1.0",
|
||||||
"react-simplemde-editor": "^4.1.3",
|
"react-simplemde-editor": "^4.1.3",
|
||||||
|
"react-spring": "^8.0.20",
|
||||||
"reakit": "^1.0.0-beta.13",
|
"reakit": "^1.0.0-beta.13",
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-persist": "^5.10.0",
|
"redux-persist": "^5.10.0",
|
||||||
|
@ -191,16 +205,20 @@
|
||||||
"sass": "^1.29.0",
|
"sass": "^1.29.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
"stream-to-blob-url": "^2.1.1",
|
||||||
"strip-markdown": "^3.0.3",
|
"strip-markdown": "^3.0.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"three-full": "^28.0.2",
|
"three-full": "^28.0.2",
|
||||||
|
"tiny-relative-date": "^1.3.0",
|
||||||
|
"tree-kill": "^1.1.0",
|
||||||
"unist-util-visit": "^2.0.3",
|
"unist-util-visit": "^2.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"video.js": "^7.14.3",
|
"video.js": "^7.14.3",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-event-tracking": "^1.0.1",
|
"videojs-event-tracking": "^1.0.1",
|
||||||
"villain-react": "^1.0.9",
|
"villain-react": "^1.0.9",
|
||||||
|
"wavesurfer.js": "^2.2.1",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-bundle-analyzer": "^3.1.0",
|
"webpack-bundle-analyzer": "^3.1.0",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
|
@ -210,14 +228,15 @@
|
||||||
"webpack-hot-middleware": "^2.24.3",
|
"webpack-hot-middleware": "^2.24.3",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
|
"y18n": "^4.0.1",
|
||||||
"yarnhook": "^0.2.0"
|
"yarnhook": "^0.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.13",
|
"node": ">=7",
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.113.0",
|
"lbrynetDaemonVersion": "0.107.2",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -2316,11 +2316,5 @@
|
||||||
"Installing, please wait...": "Installing, please wait...",
|
"Installing, please wait...": "Installing, please wait...",
|
||||||
"There was an error during installation. Please, try again.": "There was an error during installation. Please, try again.",
|
"There was an error during installation. Please, try again.": "There was an error during installation. Please, try again.",
|
||||||
"Odysee Connect --[Section in Help Page]--": "Odysee Connect",
|
"Odysee Connect --[Section in Help Page]--": "Odysee Connect",
|
||||||
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
|
|
||||||
"Your hub has blocked 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.",
|
|
||||||
"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.",
|
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
164
ui/analytics.js
164
ui/analytics.js
|
@ -1,11 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
/*
|
|
||||||
Removed Watchman (internal view tracking) code.
|
|
||||||
This file may eventually implement cantina
|
|
||||||
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
|
||||||
refer to ___ for removal commit.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||||
|
@ -21,6 +14,9 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
|
||||||
export const SHARE_INTERNAL = 'shareInternal';
|
export const SHARE_INTERNAL = 'shareInternal';
|
||||||
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
||||||
|
|
||||||
|
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
|
||||||
|
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
|
||||||
|
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
ElectronCookies.enable({
|
ElectronCookies.enable({
|
||||||
origin: 'https://lbry.tv',
|
origin: 'https://lbry.tv',
|
||||||
|
@ -72,10 +68,114 @@ type LogPublishParams = {
|
||||||
let internalAnalyticsEnabled: boolean = false;
|
let internalAnalyticsEnabled: boolean = false;
|
||||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the mobile device type viewing the data
|
||||||
|
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
||||||
|
*
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function getDeviceType() {
|
||||||
|
return 'dsk';
|
||||||
|
}
|
||||||
|
// variables initialized for watchman
|
||||||
|
let amountOfBufferEvents = 0;
|
||||||
|
let amountOfBufferTimeInMS = 0;
|
||||||
|
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
||||||
|
let lastSentTime;
|
||||||
|
|
||||||
|
// calculate data for backend, send them, and reset buffer data for next interval
|
||||||
|
async function sendAndResetWatchmanData() {
|
||||||
|
if (!userId) {
|
||||||
|
return 'Can only be used with a user id';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoPlayer) {
|
||||||
|
return 'Video player not initialized';
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
||||||
|
lastSentTime = new Date();
|
||||||
|
|
||||||
|
let protocol;
|
||||||
|
if (videoType === 'application/x-mpegURL') {
|
||||||
|
protocol = 'hls';
|
||||||
|
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||||
|
// $FlowFixMe
|
||||||
|
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
||||||
|
} else {
|
||||||
|
protocol = 'stb';
|
||||||
|
}
|
||||||
|
|
||||||
|
// current position in video in MS
|
||||||
|
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
||||||
|
|
||||||
|
// get the duration marking the time in the video for relative position calculation
|
||||||
|
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
||||||
|
|
||||||
|
// build object for watchman backend
|
||||||
|
const objectToSend = {
|
||||||
|
rebuf_count: amountOfBufferEvents,
|
||||||
|
rebuf_duration: amountOfBufferTimeInMS,
|
||||||
|
url: claimUrl.replace('lbry://', ''),
|
||||||
|
device: getDeviceType(),
|
||||||
|
duration: timeSinceLastIntervalSend,
|
||||||
|
protocol,
|
||||||
|
player: playerPoweredBy,
|
||||||
|
user_id: userId.toString(),
|
||||||
|
position: Math.round(positionInVideo),
|
||||||
|
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||||
|
bitrate: bitrateAsBitsPerSecond,
|
||||||
|
bandwidth: undefined,
|
||||||
|
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||||
|
};
|
||||||
|
|
||||||
|
// post to watchman
|
||||||
|
await sendWatchmanData(objectToSend);
|
||||||
|
|
||||||
|
// reset buffer data
|
||||||
|
amountOfBufferEvents = 0;
|
||||||
|
amountOfBufferTimeInMS = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let watchmanInterval;
|
||||||
|
// clear watchman interval and mark it as null (when video paused)
|
||||||
|
function stopWatchmanInterval() {
|
||||||
|
clearInterval(watchmanInterval);
|
||||||
|
watchmanInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates the setInterval that will run send to watchman on recurring basis
|
||||||
|
function startWatchmanIntervalIfNotRunning() {
|
||||||
|
if (!watchmanInterval) {
|
||||||
|
// instantiate the first time to calculate duration from
|
||||||
|
lastSentTime = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// post data to the backend
|
||||||
|
async function sendWatchmanData(body) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||||
videoBufferEvent: async (claim, data) => {
|
videoBufferEvent: async (claim, data) => {
|
||||||
// stub
|
amountOfBufferEvents = amountOfBufferEvents + 1;
|
||||||
|
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
||||||
|
},
|
||||||
|
onDispose: () => {
|
||||||
|
stopWatchmanInterval();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Is told whether video is being started or paused, and adjusts interval accordingly
|
* Is told whether video is being started or paused, and adjusts interval accordingly
|
||||||
|
@ -83,9 +183,40 @@ const analytics: Analytics = {
|
||||||
* @param {object} passedPlayer - VideoJS Player object
|
* @param {object} passedPlayer - VideoJS Player object
|
||||||
*/
|
*/
|
||||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||||
// stub
|
let playerIsSeeking = false;
|
||||||
|
// have to use this because videojs pauses/unpauses during seek
|
||||||
|
// sometimes the seeking function isn't populated yet so check for it as well
|
||||||
|
if (passedPlayer && passedPlayer.seeking) {
|
||||||
|
playerIsSeeking = passedPlayer.seeking();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if being paused, and not seeking, send existing data and stop interval
|
||||||
|
if (!isPlaying && !playerIsSeeking) {
|
||||||
|
sendAndResetWatchmanData();
|
||||||
|
stopWatchmanInterval();
|
||||||
|
// if being told to pause, and seeking, send and restart interval
|
||||||
|
} else if (!isPlaying && playerIsSeeking) {
|
||||||
|
sendAndResetWatchmanData();
|
||||||
|
stopWatchmanInterval();
|
||||||
|
startWatchmanIntervalIfNotRunning();
|
||||||
|
// is being told to play, and seeking, don't do anything,
|
||||||
|
// assume it's been started already from pause
|
||||||
|
} else if (isPlaying && playerIsSeeking) {
|
||||||
|
// start but not a seek, assuming a start from paused content
|
||||||
|
} else if (isPlaying && !playerIsSeeking) {
|
||||||
|
startWatchmanIntervalIfNotRunning();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||||
|
// populate values for watchman when video starts
|
||||||
|
userId = passedUserId;
|
||||||
|
claimUrl = canonicalUrl;
|
||||||
|
playerPoweredBy = poweredBy;
|
||||||
|
|
||||||
|
videoType = passedPlayer.currentSource().type;
|
||||||
|
videoPlayer = passedPlayer;
|
||||||
|
bitrateAsBitsPerSecond = videoBitrate;
|
||||||
|
|
||||||
// sendPromMetric('time_to_start', duration);
|
// sendPromMetric('time_to_start', duration);
|
||||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||||
},
|
},
|
||||||
|
@ -251,9 +382,24 @@ function sendMatomoEvent(category, action, name, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prometheus
|
||||||
|
// function sendPromMetric(name: string, value?: number) {
|
||||||
|
// if (IS_WEB) {
|
||||||
|
// let url = new URL(SDK_API_PATH + '/metric/ui');
|
||||||
|
// const params = { name: name, value: value ? value.toString() : '' };
|
||||||
|
// url.search = new URLSearchParams(params).toString();
|
||||||
|
// return fetch(url, { method: 'post' });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const MatomoInstance = new MatomoTracker({
|
const MatomoInstance = new MatomoTracker({
|
||||||
urlBase: MATOMO_URL,
|
urlBase: MATOMO_URL,
|
||||||
siteId: MATOMO_ID, // optional, default value: `1`
|
siteId: MATOMO_ID, // optional, default value: `1`
|
||||||
|
// heartBeat: { // optional, enabled by default
|
||||||
|
// active: true, // optional, default value: true
|
||||||
|
// seconds: 10 // optional, default value: `15
|
||||||
|
// },
|
||||||
|
// linkTracking: false // optional, default value: true
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Icon from 'component/common/icon';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
import useCombinedRefs from 'effects/use-combined-refs';
|
import useCombinedRefs from 'effects/use-combined-refs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -33,6 +34,7 @@ type Props = {
|
||||||
onMouseLeave: ?(any) => any,
|
onMouseLeave: ?(any) => any,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
emailVerified: boolean,
|
emailVerified: boolean,
|
||||||
|
requiresAuth: ?boolean,
|
||||||
myref: any,
|
myref: any,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
'aria-label'?: string,
|
'aria-label'?: string,
|
||||||
|
@ -64,6 +66,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
iconColor,
|
iconColor,
|
||||||
activeClass,
|
activeClass,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
|
requiresAuth,
|
||||||
myref,
|
myref,
|
||||||
dispatch, // <button> doesn't know what to do with dispatch
|
dispatch, // <button> doesn't know what to do with dispatch
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -72,7 +75,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const disable = disabled;
|
const disable = disabled || (user === null && requiresAuth);
|
||||||
|
|
||||||
const combinedClassName = classnames(
|
const combinedClassName = classnames(
|
||||||
'button',
|
'button',
|
||||||
|
@ -180,6 +183,31 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requiresAuth && !emailVerified) {
|
||||||
|
let redirectUrl = `/$/${PAGES.AUTH}?redirect=${pathname}`;
|
||||||
|
|
||||||
|
if (authSrc) {
|
||||||
|
redirectUrl += `&src=${authSrc}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
exact
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
to={redirectUrl}
|
||||||
|
title={title || defaultTooltip}
|
||||||
|
disabled={disable}
|
||||||
|
className={combinedClassName}
|
||||||
|
activeClassName={activeClass}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return path ? (
|
return path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
exact
|
exact
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import TagsSearch from 'component/tagsSearch';
|
import TagsSearch from 'component/tagsSearch';
|
||||||
import ErrorText from 'component/common/error-text';
|
import ErrorText from 'component/common/error-text';
|
||||||
|
@ -325,6 +325,7 @@ function ChannelForm(props: Props) {
|
||||||
uri={uri}
|
uri={uri}
|
||||||
thumbnailPreview={thumbnailPreview}
|
thumbnailPreview={thumbnailPreview}
|
||||||
allowGifs
|
allowGifs
|
||||||
|
showDelayedMessage={isUpload.thumbnail}
|
||||||
setThumbUploadError={setThumbError}
|
setThumbUploadError={setThumbError}
|
||||||
thumbUploadError={thumbError}
|
thumbUploadError={thumbError}
|
||||||
/>
|
/>
|
||||||
|
@ -376,7 +377,7 @@ function ChannelForm(props: Props) {
|
||||||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||||
maxLength={MAX_TITLE_LEN}
|
maxLength={MAX_TITLE_LEN}
|
||||||
/>
|
/>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
||||||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import { handleBidChange } from 'util/publish';
|
import { handleBidChange } from 'util/publish';
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||||
|
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
|
||||||
usePublishFormMode
|
usePublishFormMode
|
||||||
/>
|
/>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import CommentReactions from 'component/commentReactions';
|
import CommentReactions from 'component/commentReactions';
|
||||||
|
@ -319,7 +319,7 @@ function CommentView(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
className="comment__edit-input"
|
className="comment__edit-input"
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import SelectChannel from 'component/selectChannel';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isReply: boolean,
|
|
||||||
advancedHandler: () => void,
|
|
||||||
advanced: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CommentCreateHeader(props: Props) {
|
|
||||||
const { isReply, advancedHandler, advanced } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="comment-create__header">
|
|
||||||
<div className="comment-create__label-wrapper">
|
|
||||||
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
|
||||||
<SelectChannel tiny />
|
|
||||||
</div>
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
|
|
||||||
onClick={advancedHandler}
|
|
||||||
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
|
||||||
|
|
||||||
import { buildValidSticker } from 'util/comments';
|
import { buildValidSticker } from 'util/comments';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
@ -22,8 +22,8 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SelectChannel from 'component/selectChannel';
|
||||||
import StickerSelector from './sticker-selector';
|
import StickerSelector from './sticker-selector';
|
||||||
import CommentCreateHeader from './comment-create-header';
|
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
@ -363,6 +363,31 @@ export function CommentCreate(props: Props) {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||||
|
|
||||||
|
// LIVESTREAM ONLY - REMOVE
|
||||||
|
// Handle keyboard shortcut comment creation
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
|
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||||
|
//
|
||||||
|
// if (inputRef && inputRef.current === document.activeElement) {
|
||||||
|
// // $FlowFixMe
|
||||||
|
// const isTyping = e.target.attributes['term'];
|
||||||
|
//
|
||||||
|
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// buttonRef.current.click();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// window.addEventListener('keydown', altEnterListener);
|
||||||
|
//
|
||||||
|
// // removes the listener so it doesn't cause problems elsewhere in the app
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener('keydown', altEnterListener);
|
||||||
|
// };
|
||||||
|
// }, [isLivestream]);
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Render
|
// Render
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -384,11 +409,7 @@ export function CommentCreate(props: Props) {
|
||||||
push(pathPlusRedirect);
|
push(pathPlusRedirect);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormFieldAreaAdvanced
|
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||||
type="textarea"
|
|
||||||
name={'comment_signup_prompt'}
|
|
||||||
placeholder={__('Say something about this...')}
|
|
||||||
/>
|
|
||||||
<div className="section__actions--no-margin">
|
<div className="section__actions--no-margin">
|
||||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -399,22 +420,22 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
className={classnames('comment-create', {
|
className={classnames('commentCreate', {
|
||||||
'comment-create--reply': isReply,
|
'commentCreate--reply': isReply,
|
||||||
'comment-create--nestedReply': isNested,
|
'commentCreate--nestedReply': isNested,
|
||||||
'comment-create--bottom': bottom,
|
'commentCreate--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Input Box/Preview Box */}
|
{/* Input Box/Preview Box */}
|
||||||
{stickerSelector ? (
|
{stickerSelector ? (
|
||||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||||
<div className="comment-create__stickerPreview">
|
<div className="commentCreate__stickerPreview">
|
||||||
<div className="comment-create__stickerPreviewInfo">
|
<div className="commentCreate__stickerPreviewInfo">
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
</div>
|
</div>
|
||||||
<div className="comment-create__stickerPreviewImage">
|
<div className="commentCreate__stickerPreviewImage">
|
||||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
{/* figure out lbc sticker prices */}
|
{/* figure out lbc sticker prices */}
|
||||||
|
@ -426,15 +447,15 @@ export function CommentCreate(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
<div className="comment-create__supportCommentPreview">
|
<div className="commentCreate__supportCommentPreview">
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
className="comment-create__supportCommentPreviewAmount"
|
className="commentCreate__supportCommentPreviewAmount"
|
||||||
isFiat={activeTab === TAB_FIAT}
|
isFiat={activeTab === TAB_FIAT}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
/>
|
/>
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div className="comment-create__supportCommentBody">
|
<div className="commentCreate__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -449,22 +470,23 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
header={
|
label={
|
||||||
<CommentCreateHeader
|
<div className="commentCreate__labelWrapper">
|
||||||
isReply={isReply}
|
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||||
advanced={advancedEditor}
|
<SelectChannel tiny />
|
||||||
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
</div>
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : 'content_description'}
|
||||||
|
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
|
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={onTextareaBlur}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
|
@ -632,7 +654,7 @@ export function CommentCreate(props: Props) {
|
||||||
{/* Help Text */}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{!!minAmount && (
|
{!!minAmount && (
|
||||||
<div className="help--notice comment-create__minAmountNotice">
|
<div className="help--notice commentCreate__minAmountNotice">
|
||||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
|
|
|
@ -366,9 +366,9 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
const sortButtonProps = { activeSort: sort, changeSort };
|
const sortButtonProps = { activeSort: sort, changeSort };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'comment__actions-row'}>
|
<>
|
||||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||||
<div className="comment__sort-group">
|
<span className="comment__sort">
|
||||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||||
<SortButton
|
<SortButton
|
||||||
{...sortButtonProps}
|
{...sortButtonProps}
|
||||||
|
@ -377,16 +377,17 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
sortOption={SORT_BY.CONTROVERSY}
|
sortOption={SORT_BY.CONTROVERSY}
|
||||||
/>
|
/>
|
||||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<div className="button_refresh">
|
||||||
|
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||||
|
</div>
|
||||||
{allServers.length >= 2 && (
|
{allServers.length >= 2 && (
|
||||||
<div className="button__selected-server">
|
<div className="button_selectedServer">
|
||||||
<FormField
|
<FormField
|
||||||
type="select-tiny"
|
type="select-tiny"
|
||||||
onChange={function (x) {
|
onChange={function (x) {
|
||||||
const selectedServer = x.target.value;
|
const selectedServer = x.target.value;
|
||||||
setPage(0);
|
|
||||||
setCommentServer(selectedServer);
|
setCommentServer(selectedServer);
|
||||||
if (selectedServer === defaultServer.url) {
|
if (selectedServer === defaultServer.url) {
|
||||||
Comments.setServerUrl(undefined);
|
Comments.setServerUrl(undefined);
|
||||||
|
@ -406,10 +407,7 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="button_refresh">
|
</>
|
||||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
||||||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
files: Array<File>,
|
files: Array<WebFile>,
|
||||||
onChange: (File | void) => void,
|
onChange: (WebFile | void) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type RadioProps = {
|
type RadioProps = {
|
||||||
|
@ -26,16 +26,16 @@ function FileList(props: Props) {
|
||||||
|
|
||||||
const getFile = (value?: string) => {
|
const getFile = (value?: string) => {
|
||||||
if (files && files.length) {
|
if (files && files.length) {
|
||||||
return files.find((file: File) => file.name === value);
|
return files.find((file: WebFile) => file.name === value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (radio.items.length) {
|
if (radio.stops.length) {
|
||||||
if (!radio.currentId) {
|
if (!radio.currentId) {
|
||||||
radio.first();
|
radio.first();
|
||||||
} else {
|
} else {
|
||||||
const first = radio.items[0].ref.current;
|
const first = radio.stops[0].ref.current;
|
||||||
// First auto-selection
|
// First auto-selection
|
||||||
if (first && first.id === radio.currentId && !radio.state) {
|
if (first && first.id === radio.currentId && !radio.state) {
|
||||||
const file = getFile(first.value);
|
const file = getFile(first.value);
|
||||||
|
@ -46,12 +46,12 @@ function FileList(props: Props) {
|
||||||
|
|
||||||
if (radio.state) {
|
if (radio.state) {
|
||||||
// Find selected element
|
// Find selected element
|
||||||
const stop = radio.items.find((item) => item.id === radio.currentId);
|
const stop = radio.stops.find(item => item.id === radio.currentId);
|
||||||
const element = stop && stop.ref.current;
|
const element = stop && stop.ref.current;
|
||||||
// Only update state if new item is selected
|
// Only update state if new item is selected
|
||||||
if (element && element.value !== radio.state) {
|
if (element && element.value !== radio.state) {
|
||||||
const file = getFile(element.value);
|
const file = getFile(element.value);
|
||||||
// Select new file and update state
|
// Sselect new file and update state
|
||||||
onChange(file);
|
onChange(file);
|
||||||
radio.setState(element.value);
|
radio.setState(element.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,25 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as remote from '@electron/remote';
|
import * as remote from '@electron/remote';
|
||||||
import { ipcRenderer } from 'electron';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: string,
|
type: string,
|
||||||
currentPath?: ?string,
|
currentPath?: ?string,
|
||||||
onFileChosen: (FileWithPath) => void,
|
onFileChosen: (WebFile) => void,
|
||||||
label?: string,
|
label?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
accept?: string,
|
accept?: string,
|
||||||
error?: string,
|
error?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
filters?: Array<{ name: string, extension: string[] }>,
|
|
||||||
readFile?: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileSelector extends React.PureComponent<Props> {
|
class FileSelector extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
readFile: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fileInput: React.ElementRef<any>;
|
fileInput: React.ElementRef<any>;
|
||||||
|
@ -45,7 +41,7 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
if (this.props.onFileChosen) {
|
if (this.props.onFileChosen) {
|
||||||
this.props.onFileChosen({ file, path: file.path || file.name });
|
this.props.onFileChosen(file);
|
||||||
}
|
}
|
||||||
this.fileInput.current.value = null; // clear the file input
|
this.fileInput.current.value = null; // clear the file input
|
||||||
};
|
};
|
||||||
|
@ -68,27 +64,13 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
properties = ['openDirectory'];
|
properties = ['openDirectory'];
|
||||||
}
|
}
|
||||||
|
|
||||||
remote.dialog
|
remote.dialog.showOpenDialog({ properties, defaultPath }).then((result) => {
|
||||||
.showOpenDialog({
|
const path = result && result.filePaths[0];
|
||||||
properties,
|
if (path) {
|
||||||
defaultPath,
|
// $FlowFixMe
|
||||||
filters: this.props.filters,
|
this.props.onFileChosen({ path });
|
||||||
})
|
}
|
||||||
.then((result) => {
|
});
|
||||||
const path = result && result.filePaths[0];
|
|
||||||
if (path) {
|
|
||||||
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file = new File([result.buffer], result.name, {
|
|
||||||
type: result.mime,
|
|
||||||
});
|
|
||||||
this.props.onFileChosen({ file, path: result.path });
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fileInputButton = () => {
|
fileInputButton = () => {
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
// @flow
|
|
||||||
import 'easymde/dist/easymde.min.css';
|
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOMServer from 'react-dom/server';
|
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
|
||||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
|
||||||
import type { ElementRef, Node } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
autoFocus?: boolean,
|
|
||||||
blockWrap: boolean,
|
|
||||||
charCount?: number,
|
|
||||||
children?: React$Node,
|
|
||||||
disabled?: boolean,
|
|
||||||
helper?: string | React$Node,
|
|
||||||
hideSuggestions?: boolean,
|
|
||||||
isLivestream?: boolean,
|
|
||||||
label?: string | Node,
|
|
||||||
labelOnLeft: boolean,
|
|
||||||
name: string,
|
|
||||||
noEmojis?: boolean,
|
|
||||||
placeholder?: string | number,
|
|
||||||
quickActionLabel?: string,
|
|
||||||
textAreaMaxLength?: number,
|
|
||||||
type?: string,
|
|
||||||
value?: string | number,
|
|
||||||
onChange?: (any) => any,
|
|
||||||
openEmoteMenu?: () => void,
|
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
render?: () => React$Node,
|
|
||||||
header?: React$Node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
|
|
||||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
|
||||||
|
|
||||||
input: { current: ElementRef<any> };
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.input = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { autoFocus } = this.props;
|
|
||||||
const input = this.input.current;
|
|
||||||
|
|
||||||
if (input && autoFocus) input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
autoFocus,
|
|
||||||
blockWrap,
|
|
||||||
charCount,
|
|
||||||
children,
|
|
||||||
helper,
|
|
||||||
hideSuggestions,
|
|
||||||
isLivestream,
|
|
||||||
label,
|
|
||||||
header,
|
|
||||||
labelOnLeft,
|
|
||||||
name,
|
|
||||||
noEmojis,
|
|
||||||
quickActionLabel,
|
|
||||||
textAreaMaxLength,
|
|
||||||
type,
|
|
||||||
openEmoteMenu,
|
|
||||||
quickActionHandler,
|
|
||||||
render,
|
|
||||||
...inputProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Ideally, the character count should (and can) be appended to the
|
|
||||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
|
||||||
// to pass the current value to it's callback, nor query the current
|
|
||||||
// text length from the callback. So, we'll use our own widget.
|
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const quickAction =
|
|
||||||
quickActionLabel && quickActionHandler ? (
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const input = () => {
|
|
||||||
switch (type) {
|
|
||||||
case 'markdown':
|
|
||||||
const handleEvents = { contextmenu: openEditorMenu };
|
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
|
||||||
// SimpleMDE max char check
|
|
||||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
|
||||||
if (textAreaMaxLength && changes.update) {
|
|
||||||
var str = changes.text.join('\n');
|
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
|
||||||
|
|
||||||
if (delta <= 0) return;
|
|
||||||
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
|
||||||
if (delta > 0) {
|
|
||||||
str = str.substring(0, str.length - delta);
|
|
||||||
changes.update(changes.from, changes.to, str.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
|
||||||
editor.codemirror.on('changes', (instance, changes) => {
|
|
||||||
try {
|
|
||||||
// Grab the last change from the buffered list. I assume the
|
|
||||||
// buffered one ('changes', instead of 'change') is more efficient,
|
|
||||||
// and that "Create Link" will always end up last in the list.
|
|
||||||
const lastChange = changes[changes.length - 1];
|
|
||||||
if (lastChange.origin === '+input') {
|
|
||||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
|
||||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
|
||||||
|
|
||||||
// The URL placeholder is always placed last, so just look at the
|
|
||||||
// last text in the array to also cover the multi-line case:
|
|
||||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
|
||||||
|
|
||||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
|
||||||
const from = lastChange.from;
|
|
||||||
const to = lastChange.to;
|
|
||||||
const isSelectionMultiline = lastChange.text.length > 1;
|
|
||||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
|
||||||
|
|
||||||
// Everything works fine for the [Ctrl-K] case, but for the
|
|
||||||
// [Button] case, this handler happens before the original
|
|
||||||
// code, thus our change got wiped out.
|
|
||||||
// Add a small delay to handle that case.
|
|
||||||
setTimeout(() => {
|
|
||||||
instance.setSelection(
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
|
||||||
);
|
|
||||||
}, 25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {} // Do nothing (revert to original behavior)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
|
||||||
<fieldset-section>
|
|
||||||
{!header && (
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
|
||||||
<SimpleMDE
|
|
||||||
{...inputProps}
|
|
||||||
id={name}
|
|
||||||
type="textarea"
|
|
||||||
events={handleEvents}
|
|
||||||
getMdeInstance={getInstance}
|
|
||||||
options={{
|
|
||||||
spellChecker: true,
|
|
||||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
||||||
previewRender(plainText) {
|
|
||||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
|
||||||
return ReactDOMServer.renderToString(preview);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{countInfo}
|
|
||||||
</fieldset-section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<fieldset-section>
|
|
||||||
{!header && (label || quickAction) && (
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
|
||||||
{hideSuggestions ? (
|
|
||||||
<textarea
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
ref={this.input}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextareaWithSuggestions
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
inputRef={this.input}
|
|
||||||
isLivestream={isLivestream}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="form-field__textarea-info">
|
|
||||||
{!noEmojis && openEmoteMenu && (
|
|
||||||
<Button
|
|
||||||
type="alt"
|
|
||||||
className="button--comment-icons"
|
|
||||||
title="Emotes"
|
|
||||||
onClick={openEmoteMenu}
|
|
||||||
icon={ICONS.EMOJI}
|
|
||||||
iconSize={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{countInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{type && input()}
|
|
||||||
{helper && <div className="form-field__help">{helper}</div>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FormFieldAreaAdvanced;
|
|
|
@ -1,7 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||||
import type { ElementRef, Node } from 'react';
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -14,15 +21,19 @@ type Props = {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
helper?: string | React$Node,
|
helper?: string | React$Node,
|
||||||
|
hideSuggestions?: boolean,
|
||||||
inputButton?: React$Node,
|
inputButton?: React$Node,
|
||||||
|
isLivestream?: boolean,
|
||||||
label?: string | Node,
|
label?: string | Node,
|
||||||
labelOnLeft: boolean,
|
labelOnLeft: boolean,
|
||||||
max?: number,
|
max?: number,
|
||||||
min?: number,
|
min?: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
noEmojis?: boolean,
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
|
quickActionLabel?: string,
|
||||||
range?: number,
|
range?: number,
|
||||||
readOnly?: boolean,
|
readOnly?: boolean,
|
||||||
stretch?: boolean,
|
stretch?: boolean,
|
||||||
|
@ -30,6 +41,8 @@ type Props = {
|
||||||
type?: string,
|
type?: string,
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
onChange?: (any) => any,
|
onChange?: (any) => any,
|
||||||
|
openEmoteMenu?: () => void,
|
||||||
|
quickActionHandler?: (any) => any,
|
||||||
render?: () => React$Node,
|
render?: () => React$Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,15 +72,21 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
children,
|
children,
|
||||||
error,
|
error,
|
||||||
helper,
|
helper,
|
||||||
|
hideSuggestions,
|
||||||
inputButton,
|
inputButton,
|
||||||
|
isLivestream,
|
||||||
label,
|
label,
|
||||||
labelOnLeft,
|
labelOnLeft,
|
||||||
name,
|
name,
|
||||||
|
noEmojis,
|
||||||
postfix,
|
postfix,
|
||||||
prefix,
|
prefix,
|
||||||
|
quickActionLabel,
|
||||||
stretch,
|
stretch,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
type,
|
type,
|
||||||
|
openEmoteMenu,
|
||||||
|
quickActionHandler,
|
||||||
render,
|
render,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -82,10 +101,18 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Wrapper = blockWrap
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
|
||||||
|
const quickAction =
|
||||||
|
quickActionLabel && quickActionHandler ? (
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const inputSimple = (type: string) => (
|
const inputSimple = (type: string) => (
|
||||||
<>
|
<>
|
||||||
<input id={name} type={type} {...inputProps} />
|
<input id={name} type={type} {...inputProps} />
|
||||||
|
@ -116,22 +143,133 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
return inputSelect('');
|
return inputSelect('');
|
||||||
case 'select-tiny':
|
case 'select-tiny':
|
||||||
return inputSelect('select--slim');
|
return inputSelect('select--slim');
|
||||||
|
case 'markdown':
|
||||||
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
|
const getInstance = (editor) => {
|
||||||
|
// SimpleMDE max char check
|
||||||
|
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||||
|
if (textAreaMaxLength && changes.update) {
|
||||||
|
var str = changes.text.join('\n');
|
||||||
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
|
|
||||||
|
if (delta <= 0) return;
|
||||||
|
|
||||||
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
|
if (delta > 0) {
|
||||||
|
str = str.substring(0, str.length - delta);
|
||||||
|
changes.update(changes.from, changes.to, str.split('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||||
|
editor.codemirror.on('changes', (instance, changes) => {
|
||||||
|
try {
|
||||||
|
// Grab the last change from the buffered list. I assume the
|
||||||
|
// buffered one ('changes', instead of 'change') is more efficient,
|
||||||
|
// and that "Create Link" will always end up last in the list.
|
||||||
|
const lastChange = changes[changes.length - 1];
|
||||||
|
if (lastChange.origin === '+input') {
|
||||||
|
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||||
|
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||||
|
|
||||||
|
// The URL placeholder is always placed last, so just look at the
|
||||||
|
// last text in the array to also cover the multi-line case:
|
||||||
|
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||||
|
|
||||||
|
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||||
|
const from = lastChange.from;
|
||||||
|
const to = lastChange.to;
|
||||||
|
const isSelectionMultiline = lastChange.text.length > 1;
|
||||||
|
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||||
|
|
||||||
|
// Everything works fine for the [Ctrl-K] case, but for the
|
||||||
|
// [Button] case, this handler happens before the original
|
||||||
|
// code, thus our change got wiped out.
|
||||||
|
// Add a small delay to handle that case.
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.setSelection(
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||||
|
);
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
|
<fieldset-section>
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
</div>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
<SimpleMDE
|
||||||
|
{...inputProps}
|
||||||
|
id={name}
|
||||||
|
type="textarea"
|
||||||
|
events={handleEvents}
|
||||||
|
getMdeInstance={getInstance}
|
||||||
|
options={{
|
||||||
|
spellChecker: true,
|
||||||
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||||
|
previewRender(plainText) {
|
||||||
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||||
|
return ReactDOMServer.renderToString(preview);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countInfo}
|
||||||
|
</fieldset-section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{label && (
|
{(label || quickAction) && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{quickAction}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<textarea
|
|
||||||
type={type}
|
{hideSuggestions ? (
|
||||||
id={name}
|
<textarea
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
type={type}
|
||||||
ref={this.input}
|
id={name}
|
||||||
{...inputProps}
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
/>
|
ref={this.input}
|
||||||
<div className="form-field__textarea-info">{countInfo}</div>
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextareaWithSuggestions
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
inputRef={this.input}
|
||||||
|
isLivestream={isLivestream}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-field__textarea-info">
|
||||||
|
{!noEmojis && openEmoteMenu && (
|
||||||
|
<Button
|
||||||
|
type="alt"
|
||||||
|
className="button--comment-icons"
|
||||||
|
title="Emotes"
|
||||||
|
onClick={openEmoteMenu}
|
||||||
|
icon={ICONS.EMOJI}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{countInfo}
|
||||||
|
</div>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export { Form } from './form-components/form';
|
export { Form } from './form-components/form';
|
||||||
export { FormField } from './form-components/form-field';
|
export { FormField } from './form-components/form-field';
|
||||||
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
|
||||||
export { FormFieldPrice } from './form-components/form-field-price';
|
export { FormFieldPrice } from './form-components/form-field-price';
|
||||||
export { Submit } from './form-components/submit';
|
export { Submit } from './form-components/submit';
|
||||||
|
|
|
@ -2054,15 +2054,4 @@ export const icons = {
|
||||||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
|
||||||
<g>
|
|
||||||
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
|
||||||
<g>
|
|
||||||
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
|
||||||
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,10 +11,10 @@ import Icon from 'component/common/icon';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modal: { id: string, modalProps: {} },
|
modal: { id: string, modalProps: {} },
|
||||||
filePath: ?string,
|
filePath: string | WebFile,
|
||||||
clearPublish: () => void,
|
clearPublish: () => void,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
openModal: (id: string, { files: Array<File> }) => void,
|
openModal: (id: string, { files: Array<WebFile> }) => void,
|
||||||
// React router
|
// React router
|
||||||
history: {
|
history: {
|
||||||
entities: {}[],
|
entities: {}[],
|
||||||
|
@ -37,7 +37,7 @@ function FileDrop(props: Props) {
|
||||||
const { drag, dropData } = useDragDrop();
|
const { drag, dropData } = useDragDrop();
|
||||||
const [files, setFiles] = React.useState([]);
|
const [files, setFiles] = React.useState([]);
|
||||||
const [error, setError] = React.useState(false);
|
const [error, setError] = React.useState(false);
|
||||||
const [target, setTarget] = React.useState<?File>(null);
|
const [target, setTarget] = React.useState<?WebFile>(null);
|
||||||
const hideTimer = React.useRef(null);
|
const hideTimer = React.useRef(null);
|
||||||
const targetTimer = React.useRef(null);
|
const targetTimer = React.useRef(null);
|
||||||
const navigationTimer = React.useRef(null);
|
const navigationTimer = React.useRef(null);
|
||||||
|
@ -65,26 +65,24 @@ function FileDrop(props: Props) {
|
||||||
}
|
}
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
|
// Delay hide and navigation for a smooth transition
|
||||||
|
const hideDropArea = React.useCallback(() => {
|
||||||
|
hideTimer.current = setTimeout(() => {
|
||||||
|
setFiles([]);
|
||||||
|
// Navigate to publish area
|
||||||
|
navigationTimer.current = setTimeout(() => {
|
||||||
|
navigateToPublish();
|
||||||
|
}, NAVIGATE_TIME_OUT);
|
||||||
|
}, HIDE_TIME_OUT);
|
||||||
|
}, [navigateToPublish]);
|
||||||
|
|
||||||
// Handle file selection
|
// Handle file selection
|
||||||
const handleFileSelected = React.useCallback(
|
const handleFileSelected = React.useCallback(
|
||||||
(selectedFile) => {
|
(selectedFile) => {
|
||||||
// Delay hide and navigation for a smooth transition
|
updatePublishForm({ filePath: selectedFile });
|
||||||
hideTimer.current = setTimeout(() => {
|
hideDropArea();
|
||||||
setFiles([]);
|
|
||||||
// Navigate to publish area
|
|
||||||
navigationTimer.current = setTimeout(() => {
|
|
||||||
// Navigate first, THEN assign filePath, otherwise
|
|
||||||
// the file selected will get reset (that's how the
|
|
||||||
// publish file view works, when the user switches to
|
|
||||||
// publish a file, the pathFile value gets reset to undefined)
|
|
||||||
navigateToPublish();
|
|
||||||
updatePublishForm({
|
|
||||||
filePath: selectedFile.path || selectedFile.name,
|
|
||||||
});
|
|
||||||
}, NAVIGATE_TIME_OUT);
|
|
||||||
}, HIDE_TIME_OUT);
|
|
||||||
},
|
},
|
||||||
[setFiles, navigateToPublish, updatePublishForm]
|
[updatePublishForm, hideDropArea]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear timers when unmounted
|
// Clear timers when unmounted
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { isURIEqual } from 'util/lbryURI';
|
import { isURIEqual } from 'util/lbryURI';
|
||||||
|
@ -21,6 +21,7 @@ import AutoplayCountdown from 'component/autoplayCountdown';
|
||||||
// scss/init/vars.scss
|
// scss/init/vars.scss
|
||||||
// --header-height
|
// --header-height
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
|
const HEADER_HEIGHT_MOBILE = 60;
|
||||||
|
|
||||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
||||||
|
@ -132,7 +133,6 @@ export default function FileRenderFloating(props: Props) {
|
||||||
const playingUriSource = playingUri && playingUri.source;
|
const playingUriSource = playingUri && playingUri.source;
|
||||||
const isComment = playingUriSource === 'comment';
|
const isComment = playingUriSource === 'comment';
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
|
||||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||||
|
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
|
@ -344,8 +344,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
'content__viewer--floating': isFloating,
|
'content__viewer--floating': isFloating,
|
||||||
'content__viewer--inline': !isFloating,
|
'content__viewer--inline': !isFloating,
|
||||||
'content__viewer--secondary': isComment,
|
'content__viewer--secondary': isComment,
|
||||||
'content__viewer--theater-mode':
|
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
||||||
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
@ -357,7 +356,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
top:
|
top:
|
||||||
fileViewerRect.windowOffset +
|
fileViewerRect.windowOffset +
|
||||||
fileViewerRect.top -
|
fileViewerRect.top -
|
||||||
(isMobile ? 0 : HEADER_HEIGHT) -
|
(isMobile ? HEADER_HEIGHT_MOBILE : HEADER_HEIGHT) -
|
||||||
(IS_DESKTOP_MAC ? 24 : 0),
|
(IS_DESKTOP_MAC ? 24 : 0),
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
|
@ -64,7 +63,6 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||||
const isMediumScreen = useIsMediumScreen();
|
|
||||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||||
const containerRef = React.useRef<any>();
|
const containerRef = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -153,7 +151,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
'content__cover--theater-mode': videoTheaterMode,
|
||||||
'content__cover--text': isText,
|
'content__cover--text': isText,
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -13,7 +13,7 @@ const select = (state, props) => {
|
||||||
if (claimUriBeingPlayed) {
|
if (claimUriBeingPlayed) {
|
||||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
||||||
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
|
isBeingPlayed = claim.claim_id === claimBeingPlayed.claim_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -92,7 +92,7 @@ function Page(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('main-wrapper__inner', {
|
className={classnames('main-wrapper__inner', {
|
||||||
'main-wrapper__inner--filepage': isOnFilePage,
|
'main-wrapper__inner--filepage': isOnFilePage,
|
||||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!authPage &&
|
{!authPage &&
|
||||||
|
@ -124,7 +124,7 @@ function Page(props: Props) {
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--settings-page': settingsPage,
|
'main--settings-page': settingsPage,
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
label: ?string,
|
label: ?string,
|
||||||
disabled: ?boolean,
|
disabled: ?boolean,
|
||||||
filePath: File,
|
filePath: string | WebFile,
|
||||||
fileText: ?string,
|
fileText: ?string,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type={'markdown'}
|
type={'markdown'}
|
||||||
name="content_post"
|
name="content_post"
|
||||||
label={label}
|
label={label}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
<FormFieldAreaAdvanced
|
<FormField
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ipcRenderer } from 'electron';
|
|
||||||
import { regexInvalidURI } from 'util/lbryURI';
|
import { regexInvalidURI } from 'util/lbryURI';
|
||||||
import PostEditor from 'component/postEditor';
|
import PostEditor from 'component/postEditor';
|
||||||
import FileSelector from 'component/common/file-selector';
|
import FileSelector from 'component/common/file-selector';
|
||||||
|
@ -14,13 +13,13 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
import path from 'path';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
mode: ?string,
|
mode: ?string,
|
||||||
name: ?string,
|
name: ?string,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
filePath: ?string,
|
filePath: string | WebFile,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -78,7 +77,7 @@ function PublishFile(props: Props) {
|
||||||
const sizeInMB = Number(size) / 1000000;
|
const sizeInMB = Number(size) / 1000000;
|
||||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||||
const ffmpegAvail = ffmpegStatus.available;
|
const ffmpegAvail = ffmpegStatus.available;
|
||||||
const currentFile = filePath;
|
const [currentFile, setCurrentFile] = useState(null);
|
||||||
const [currentFileType, setCurrentFileType] = useState(null);
|
const [currentFileType, setCurrentFileType] = useState(null);
|
||||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||||
|
@ -92,35 +91,17 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
||||||
|
|
||||||
// Since the filePath can be updated from outside this component
|
|
||||||
// (for instance, when the user drags & drops a file), we need
|
|
||||||
// to check for changes in the selected file using an effect.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filePath) {
|
if (!filePath || filePath === '') {
|
||||||
return;
|
setCurrentFile('');
|
||||||
}
|
updateFileInfo(0, 0, false);
|
||||||
async function readSelectedFileDetails() {
|
} else if (typeof filePath !== 'string') {
|
||||||
// Read the file to get the file's duration (if possible)
|
// Update currentFile file
|
||||||
// and offer transcoding it.
|
if (filePath.name !== currentFile && filePath.path !== currentFile) {
|
||||||
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
|
handleFileChange(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);
|
|
||||||
}
|
}
|
||||||
readSelectedFileDetails();
|
}, [filePath, currentFile, handleFileChange, updateFileInfo]);
|
||||||
}, [filePath]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
||||||
|
@ -228,11 +209,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processSelectedFile(fileData: FileData, clearName = true) {
|
function handleFileChange(file: WebFile, clearName = true) {
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!fileData || fileData.error) {
|
if (!file) {
|
||||||
if (isStillEditing || !clearName) {
|
if (isStillEditing || !clearName) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -241,12 +222,8 @@ function PublishFile(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
// if video, extract duration so we can warn about bitrateif (typeof file !== 'string') {
|
||||||
const file = fileData.file;
|
const contentType = file.type && file.type.split('/');
|
||||||
// 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 isVideo = contentType && contentType[0] === 'video';
|
const isVideo = contentType && contentType[0] === 'video';
|
||||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||||
|
|
||||||
|
@ -254,25 +231,34 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
if (contentType && contentType[0] === 'text') {
|
if (contentType && contentType[0] === 'text') {
|
||||||
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
||||||
setCurrentFileType(contentType.join('/'));
|
setCurrentFileType(contentType);
|
||||||
} else if (path.parse(fileData.path).ext) {
|
} else if (file.name) {
|
||||||
// If user's machine is missing a valid content type registration
|
// If user's machine is missign a valid content type registration
|
||||||
// for markdown content: text/markdown, file extension will be used instead
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
const extension = path.parse(fileData.path).ext;
|
const extension = file.name.split('.').pop();
|
||||||
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
updateFileInfo(duration || 0, size, isVideo);
|
const video = document.createElement('video');
|
||||||
|
video.preload = 'metadata';
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
updateFileInfo(video.duration, file.size, isVideo);
|
||||||
|
window.URL.revokeObjectURL(video.src);
|
||||||
|
};
|
||||||
|
video.onerror = () => {
|
||||||
|
updateFileInfo(0, file.size, isVideo);
|
||||||
|
};
|
||||||
|
video.src = window.URL.createObjectURL(file);
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(duration || 0, size, isVideo);
|
updateFileInfo(0, file.size, isVideo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, size, isVideo);
|
updateFileInfo(0, file.size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextPost && file) {
|
if (isTextPost) {
|
||||||
// Create reader
|
// Create reader
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// Handler for file reader
|
// Handler for file reader
|
||||||
|
@ -284,17 +270,21 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode(PUBLISH_MODES.FILE);
|
setPublishMode(PUBLISH_MODES.FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip off extension and replace invalid characters
|
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
||||||
if (!isStillEditing) {
|
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
||||||
const fileWithoutExtension = path.parse(fileData.path).name;
|
// File.path will be undefined from web due to browser security, so it will default to the File Object.
|
||||||
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
filePath: file.path || file,
|
||||||
}
|
};
|
||||||
}
|
// Strip off extention and replace invalid characters
|
||||||
|
let fileName = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
|
||||||
|
|
||||||
function handleFileChange(fileWithPath: FileWithPath) {
|
if (!isStillEditing) {
|
||||||
if (fileWithPath) {
|
publishFormParams.name = parseName(fileName);
|
||||||
updatePublishForm({ filePath: fileWithPath.path });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File path is not supported on web for security reasons so we use the name instead.
|
||||||
|
setCurrentFile(file.path || file.name);
|
||||||
|
updatePublishForm(publishFormParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
||||||
|
@ -342,7 +332,6 @@ function PublishFile(props: Props) {
|
||||||
onFileChosen={handleFileChange}
|
onFileChosen={handleFileChange}
|
||||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
placeholder={__('Select file to upload')}
|
placeholder={__('Select file to upload')}
|
||||||
readFile={false}
|
|
||||||
/>
|
/>
|
||||||
{getUploadMessage()}
|
{getUploadMessage()}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -35,8 +35,8 @@ import tempy from 'tempy';
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
tags: Array<Tag>,
|
tags: Array<Tag>,
|
||||||
publish: (source: ?File, ?boolean) => void,
|
publish: (source?: string | File, ?boolean) => void,
|
||||||
filePath: ?File,
|
filePath: string | File,
|
||||||
fileText: string,
|
fileText: string,
|
||||||
bid: ?number,
|
bid: ?number,
|
||||||
bidError: ?string,
|
bidError: ?string,
|
||||||
|
@ -208,6 +208,7 @@ function PublishForm(props: Props) {
|
||||||
isNameValid(name) &&
|
isNameValid(name) &&
|
||||||
title &&
|
title &&
|
||||||
bid &&
|
bid &&
|
||||||
|
thumbnail &&
|
||||||
!bidError &&
|
!bidError &&
|
||||||
!emptyPostError &&
|
!emptyPostError &&
|
||||||
!(thumbnailError && !thumbnailUploaded) &&
|
!(thumbnailError && !thumbnailUploaded) &&
|
||||||
|
@ -372,6 +373,9 @@ function PublishForm(props: Props) {
|
||||||
if (!output || output === '') {
|
if (!output || output === '') {
|
||||||
// Generate a temporary file:
|
// Generate a temporary file:
|
||||||
output = tempy.file({ name: 'post.md' });
|
output = tempy.file({ name: 'post.md' });
|
||||||
|
} else if (typeof filePath === 'string') {
|
||||||
|
// Use current file
|
||||||
|
output = filePath;
|
||||||
}
|
}
|
||||||
// Create a temporary file and save file changes
|
// Create a temporary file and save file changes
|
||||||
if (output && output !== '') {
|
if (output && output !== '') {
|
||||||
|
@ -443,7 +447,7 @@ function PublishForm(props: Props) {
|
||||||
// with other properties such as name, title, etc.) for security reasons.
|
// with other properties such as name, title, etc.) for security reasons.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === PUBLISH_MODES.FILE) {
|
if (mode === PUBLISH_MODES.FILE) {
|
||||||
updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 });
|
updatePublishForm({ filePath: '', fileDur: 0, fileSize: 0 });
|
||||||
}
|
}
|
||||||
}, [mode, updatePublishForm]);
|
}, [mode, updatePublishForm]);
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,11 @@ function PublishFormErrors(props: Props) {
|
||||||
{!bid && <div>{__('A deposit amount is required')}</div>}
|
{!bid && <div>{__('A deposit amount is required')}</div>}
|
||||||
{bidError && <div>{__('Please check your deposit amount.')}</div>}
|
{bidError && <div>{__('Please check your deposit amount.')}</div>}
|
||||||
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
|
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
|
||||||
{thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>}
|
{!isUploadingThumbnail && !thumbnail ? (
|
||||||
|
<div>{__('A thumbnail is required. Please upload or provide an image URL above.')}</div>
|
||||||
|
) : (
|
||||||
|
thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>
|
||||||
|
)}
|
||||||
{editingURI && !isStillEditing && !filePath && (
|
{editingURI && !isStillEditing && !filePath && (
|
||||||
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -89,6 +89,7 @@ export default function SearchChannelField(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={addTagRef}
|
ref={addTagRef}
|
||||||
|
requiresAuth
|
||||||
button="primary"
|
button="primary"
|
||||||
label={labelFoundAction}
|
label={labelFoundAction}
|
||||||
onClick={() => handleFoundChannelClick(claim)}
|
onClick={() => handleFoundChannelClick(claim)}
|
||||||
|
|
|
@ -27,14 +27,6 @@ type Props = {
|
||||||
// passed to the onUpdate function after the
|
// passed to the onUpdate function after the
|
||||||
// upload service returns success.
|
// upload service returns success.
|
||||||
buildImagePreview?: boolean,
|
buildImagePreview?: boolean,
|
||||||
// File extension filtering. Files can be filtered
|
|
||||||
// but the "All Files" options always shows up. To
|
|
||||||
// avoid that, you can use the filters property.
|
|
||||||
// For example, to only accept images pass the
|
|
||||||
// following filter:
|
|
||||||
// { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
|
|
||||||
filters?: Array<{ name: string, extension: string[] }>,
|
|
||||||
type?: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function filePreview(file) {
|
function filePreview(file) {
|
||||||
|
@ -51,8 +43,7 @@ function filePreview(file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectAsset(props: Props) {
|
function SelectAsset(props: Props) {
|
||||||
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview, filters, type } =
|
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview } = props;
|
||||||
props;
|
|
||||||
const [pathSelected, setPathSelected] = React.useState('');
|
const [pathSelected, setPathSelected] = React.useState('');
|
||||||
const [fileSelected, setFileSelected] = React.useState<any>(null);
|
const [fileSelected, setFileSelected] = React.useState<any>(null);
|
||||||
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
||||||
|
@ -130,17 +121,17 @@ function SelectAsset(props: Props) {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileSelector
|
<FileSelector
|
||||||
filters={filters}
|
|
||||||
type={type}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={uploadStatus === SPEECH_UPLOADING}
|
disabled={uploadStatus === SPEECH_UPLOADING}
|
||||||
label={fileSelectorLabel}
|
label={fileSelectorLabel}
|
||||||
name="assetSelector"
|
name="assetSelector"
|
||||||
currentPath={pathSelected}
|
currentPath={pathSelected}
|
||||||
onFileChosen={(fileWithPath) => {
|
onFileChosen={(file) => {
|
||||||
if (fileWithPath.file.name) {
|
if (file.name) {
|
||||||
setFileSelected(fileWithPath.file);
|
setFileSelected(file);
|
||||||
setPathSelected(fileWithPath.path);
|
// what why? why not target=WEB this?
|
||||||
|
// file.path is undefined in web but available in electron
|
||||||
|
setPathSelected(file.name || file.path);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
accept={accept}
|
accept={accept}
|
||||||
|
|
|
@ -106,7 +106,7 @@ function SelectThumbnail(props: Props) {
|
||||||
__('This will be visible in a few minutes after you submit this form.')}
|
__('This will be visible in a few minutes after you submit this form.')}
|
||||||
<img
|
<img
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
src={thumbnail || ThumbnailMissingImage}
|
src={thumbnail}
|
||||||
alt={__('Thumbnail Preview')}
|
alt={__('Thumbnail Preview')}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
if (updateThumbnailParams) {
|
if (updateThumbnailParams) {
|
||||||
|
@ -160,9 +160,9 @@ function SelectThumbnail(props: Props) {
|
||||||
label={__('Thumbnail')}
|
label={__('Thumbnail')}
|
||||||
placeholder={__('Choose an enticing thumbnail')}
|
placeholder={__('Choose an enticing thumbnail')}
|
||||||
accept={accept}
|
accept={accept}
|
||||||
onFileChosen={(fileWithPath) =>
|
onFileChosen={(file) =>
|
||||||
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
|
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
|
||||||
file: fileWithPath,
|
file,
|
||||||
cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }),
|
cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,7 +130,7 @@ export default function SettingSystem(props: Props) {
|
||||||
<FileSelector
|
<FileSelector
|
||||||
type="openDirectory"
|
type="openDirectory"
|
||||||
currentPath={daemonSettings.download_dir}
|
currentPath={daemonSettings.download_dir}
|
||||||
onFileChosen={(newDirectory: FileWithPath) => {
|
onFileChosen={(newDirectory: WebFile) => {
|
||||||
setDaemonSetting('download_dir', newDirectory.path);
|
setDaemonSetting('download_dir', newDirectory.path);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -224,7 +224,7 @@ export default function SettingSystem(props: Props) {
|
||||||
type="openDirectory"
|
type="openDirectory"
|
||||||
placeholder={__('A Folder containing FFmpeg')}
|
placeholder={__('A Folder containing FFmpeg')}
|
||||||
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
|
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
|
||||||
onFileChosen={(newDirectory: FileWithPath) => {
|
onFileChosen={(newDirectory: WebFile) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
setDaemonSetting('ffmpeg_path', newDirectory.path);
|
setDaemonSetting('ffmpeg_path', newDirectory.path);
|
||||||
findFFmpeg();
|
findFFmpeg();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Form, FormField } from 'component/common/form';
|
import { Form, FormField } from 'component/common/form';
|
||||||
|
@ -24,100 +24,67 @@ type Props = {
|
||||||
const SupportsLiquidate = (props: Props) => {
|
const SupportsLiquidate = (props: Props) => {
|
||||||
const { claim, abandonSupportForClaim, handleClose, abandonClaimError } = props;
|
const { claim, abandonSupportForClaim, handleClose, abandonClaimError } = props;
|
||||||
const [previewBalance, setPreviewBalance] = useState(undefined);
|
const [previewBalance, setPreviewBalance] = useState(undefined);
|
||||||
const [defaultValueAssigned, setDefaultValueAssigned] = useState(false);
|
const [amount, setAmount] = useState(-1);
|
||||||
const [unlockTextAmount, setUnlockTextAmount] = useState('');
|
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const initialMessage = __('How much would you like to unlock?');
|
const initialMessage = __('How much would you like to unlock?');
|
||||||
const [message, setMessage] = useState(initialMessage);
|
const [message, setMessage] = useState(initialMessage);
|
||||||
const amount = Number(unlockTextAmount) || 0;
|
|
||||||
const defaultValue = previewBalance ? previewBalance * 0.25 : 0;
|
|
||||||
const keep =
|
const keep =
|
||||||
amount >= 0
|
amount >= 0
|
||||||
? Boolean(previewBalance) && Number.parseFloat(String(Number(previewBalance) - amount)).toFixed(8)
|
? Boolean(previewBalance) && Number.parseFloat(String(Number(previewBalance) - Number(amount))).toFixed(8)
|
||||||
: Boolean(previewBalance) && Number.parseFloat(String(defaultValue * 3)).toFixed(8);
|
: Boolean(previewBalance) && Number.parseFloat(String((Number(previewBalance) / 4) * 3)).toFixed(8); // default unlock 25%
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const type = claim.value_type;
|
const type = claim.value_type;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (claimId && abandonSupportForClaim) {
|
if (claimId && abandonSupportForClaim) {
|
||||||
abandonSupportForClaim(claimId, type, false, true).then((r) => {
|
abandonSupportForClaim(claimId, type, false, true).then(r => {
|
||||||
setPreviewBalance(r.total_input);
|
setPreviewBalance(r.total_input);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [abandonSupportForClaim, claimId, type, setPreviewBalance]);
|
}, [abandonSupportForClaim, claimId, type, setPreviewBalance]);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
abandonSupportForClaim(claimId, type, keep, false).then((r) => {
|
abandonSupportForClaim(claimId, type, keep, false).then(r => {
|
||||||
if (r) {
|
if (r) {
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRangeChange = useCallback(
|
function handleChange(a) {
|
||||||
(newValue) => {
|
if (a === undefined || isNaN(Number(a))) {
|
||||||
setUnlockTextAmount(String(newValue));
|
|
||||||
},
|
|
||||||
[setUnlockTextAmount]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeUnlockText = useCallback(
|
|
||||||
(newValue) => {
|
|
||||||
// Get rid of all characters except digits, commas and periods.
|
|
||||||
const onlyValidAmount = newValue.replace(/[^0-9.,]+/, '');
|
|
||||||
setUnlockTextAmount(onlyValidAmount);
|
|
||||||
},
|
|
||||||
[setUnlockTextAmount]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUnlockTextFocus = useCallback(() => {
|
|
||||||
// Get rid of empty zero when user starts typing (small ux improvement)
|
|
||||||
if (Number(unlockTextAmount) === 0) {
|
|
||||||
setUnlockTextAmount('');
|
|
||||||
}
|
|
||||||
}, [unlockTextAmount, setUnlockTextAmount]);
|
|
||||||
|
|
||||||
const handleUnlockTextBlur = useCallback(() => {
|
|
||||||
if (!unlockTextAmount || isNaN(Number(unlockTextAmount))) {
|
|
||||||
setUnlockTextAmount(previewBalance ? String(defaultValue) : '0');
|
|
||||||
}
|
|
||||||
}, [unlockTextAmount, setUnlockTextAmount, previewBalance, defaultValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultValueAssigned || !previewBalance || unlockTextAmount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setUnlockTextAmount(String(defaultValue));
|
|
||||||
setDefaultValueAssigned(true);
|
|
||||||
}, [defaultValueAssigned, previewBalance, unlockTextAmount, setUnlockTextAmount, setDefaultValueAssigned]);
|
|
||||||
|
|
||||||
// Update message & error based on unlock amount.
|
|
||||||
useEffect(() => {
|
|
||||||
const unlockAmount = Number(unlockTextAmount);
|
|
||||||
const previewBalanceNumber = Number(previewBalance);
|
|
||||||
if (unlockTextAmount && isNaN(unlockAmount)) {
|
|
||||||
setMessage(__('Amount must be a number'));
|
setMessage(__('Amount must be a number'));
|
||||||
setError(true);
|
setError(true);
|
||||||
} else if (unlockAmount > previewBalanceNumber) {
|
setAmount(0);
|
||||||
|
} else if (a === '') {
|
||||||
|
setAmount(0);
|
||||||
|
setError(true);
|
||||||
|
setMessage(__('Amount cannot be blank'));
|
||||||
|
} else if (Number(a) > Number(previewBalance)) {
|
||||||
setMessage(__('Amount cannot be more than available'));
|
setMessage(__('Amount cannot be more than available'));
|
||||||
setError(true);
|
setError(false);
|
||||||
} else if (Math.abs(unlockAmount - previewBalanceNumber) <= Number.EPSILON) {
|
} else if (Number(a) === Number(previewBalance)) {
|
||||||
setMessage(__(`She's about to close up the library!`));
|
setMessage(__(`She's about to close up the library!`));
|
||||||
|
setAmount(a);
|
||||||
setError(false);
|
setError(false);
|
||||||
} else if (unlockAmount > previewBalanceNumber / 2) {
|
} else if (Number(a) > Number(previewBalance) / 2) {
|
||||||
setMessage(__('Your content will do better with more staked on it'));
|
setMessage(__('Your content will do better with more staked on it'));
|
||||||
|
setAmount(a);
|
||||||
setError(false);
|
setError(false);
|
||||||
} else if (unlockAmount === 0) {
|
} else if (Number(a) === 0) {
|
||||||
setMessage(__('Amount cannot be zero'));
|
setMessage(__('Amount cannot be zero'));
|
||||||
|
setAmount(0);
|
||||||
setError(true);
|
setError(true);
|
||||||
} else if (unlockAmount <= previewBalanceNumber / 2) {
|
} else if (Number(a) <= Number(previewBalance) / 2) {
|
||||||
setMessage(__('A prudent choice'));
|
setMessage(__('A prudent choice'));
|
||||||
|
setAmount(Number(a));
|
||||||
setError(false);
|
setError(false);
|
||||||
} else {
|
} else {
|
||||||
setMessage(initialMessage);
|
setMessage(initialMessage);
|
||||||
|
setAmount(a);
|
||||||
setError(false);
|
setError(false);
|
||||||
}
|
}
|
||||||
}, [unlockTextAmount, previewBalance, setMessage, setError]);
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -173,8 +140,8 @@ const SupportsLiquidate = (props: Props) => {
|
||||||
min={0}
|
min={0}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
max={previewBalance}
|
max={previewBalance}
|
||||||
value={amount}
|
value={Number(amount) >= 0 ? amount : previewBalance / 4} // by default, set it to 25% of available
|
||||||
onChange={(e) => handleRangeChange(e.target.value)}
|
onChange={e => handleChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label className="range__label">
|
<label className="range__label">
|
||||||
<span>0</span>
|
<span>0</span>
|
||||||
|
@ -183,11 +150,9 @@ const SupportsLiquidate = (props: Props) => {
|
||||||
</label>
|
</label>
|
||||||
<FormField
|
<FormField
|
||||||
type="text"
|
type="text"
|
||||||
value={unlockTextAmount}
|
value={amount >= 0 ? amount || '' : previewBalance && previewBalance / 4}
|
||||||
helper={message}
|
helper={message}
|
||||||
onFocus={handleUnlockTextFocus}
|
onChange={e => handleChange(e.target.value)}
|
||||||
onChange={(e) => handleChangeUnlockText(e.target.value)}
|
|
||||||
onBlur={handleUnlockTextBlur}
|
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -41,7 +41,7 @@ function SyncToggle(props: Props) {
|
||||||
{!verifiedEmail && (
|
{!verifiedEmail && (
|
||||||
<div>
|
<div>
|
||||||
<p className="help">{__('An email address is required to sync your account.')}</p>
|
<p className="help">{__('An email address is required to sync your account.')}</p>
|
||||||
<Button button="primary" label={__('Add Email')} />
|
<Button requiresAuth button="primary" label={__('Add Email')} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
|
@ -103,8 +103,6 @@ function TxoList(props: Props) {
|
||||||
params[TXO.TX_TYPE] = currentUrlParams.type;
|
params[TXO.TX_TYPE] = currentUrlParams.type;
|
||||||
} else if (currentUrlParams.type === TXO.PUBLISH) {
|
} else if (currentUrlParams.type === TXO.PUBLISH) {
|
||||||
params[TXO.TX_TYPE] = TXO.STREAM;
|
params[TXO.TX_TYPE] = TXO.STREAM;
|
||||||
} else if (currentUrlParams.type === TXO.COLLECTION) {
|
|
||||||
params[TXO.TX_TYPE] = currentUrlParams.type;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentUrlParams.active) {
|
if (currentUrlParams.active) {
|
||||||
|
|
27
ui/component/walletSwap/index.js
Normal file
27
ui/component/walletSwap/index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import WalletSwap from './view';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { doAddCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
import { selectCoinSwaps } from 'redux/selectors/coinSwap';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import { doGetNewAddress, doCheckAddressIsMine } from 'redux/actions/wallet';
|
||||||
|
import { selectReceiveAddress } from 'redux/selectors/wallet';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
receiveAddress: selectReceiveAddress(state),
|
||||||
|
coinSwaps: selectCoinSwaps(state),
|
||||||
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
|
addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)),
|
||||||
|
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||||
|
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
|
||||||
|
queryCoinSwapStatus: (sendAddress) => dispatch(doQueryCoinSwapStatus(sendAddress)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(select, perform)(WalletSwap));
|
707
ui/component/walletSwap/view.jsx
Normal file
707
ui/component/walletSwap/view.jsx
Normal file
|
@ -0,0 +1,707 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { FormField, Form } from 'component/common/form';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
import CopyableText from 'component/copyableText';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import QRCode from 'component/common/qr-code';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import { clipboard } from 'electron';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import { Redirect, useHistory } from 'react-router';
|
||||||
|
|
||||||
|
const ENABLE_ALTERNATIVE_COINS = true;
|
||||||
|
|
||||||
|
const BTC_SATOSHIS = 100000000;
|
||||||
|
const LBC_MAX = 21000000;
|
||||||
|
const LBC_MIN = 1;
|
||||||
|
|
||||||
|
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||||
|
const DEBOUNCE_BTC_CHANGE_MS = 400;
|
||||||
|
|
||||||
|
const INTERNAL_APIS_DOWN = 'internal_apis_down';
|
||||||
|
const BTC_API_STATUS_PENDING = 'NEW'; // Started swap, waiting for coin.
|
||||||
|
const BTC_API_STATUS_CONFIRMING = 'PENDING'; // Coin receiving, waiting confirmation.
|
||||||
|
const BTC_API_STATUS_PROCESSING = 'COMPLETED'; // Coin confirmed. Sending LBC.
|
||||||
|
const BTC_API_STATUS_UNRESOLVED = 'UNRESOLVED'; // Underpaid, overpaid, etc.
|
||||||
|
const BTC_API_STATUS_EXPIRED = 'EXPIRED'; // Charge expired (60 minutes).
|
||||||
|
const BTC_API_STATUS_ERROR = 'Error';
|
||||||
|
|
||||||
|
const ACTION_MAIN = 'action_main';
|
||||||
|
const ACTION_STATUS_PENDING = 'action_pending';
|
||||||
|
const ACTION_STATUS_CONFIRMING = 'action_confirming';
|
||||||
|
const ACTION_STATUS_PROCESSING = 'action_processing';
|
||||||
|
const ACTION_STATUS_SUCCESS = 'action_success';
|
||||||
|
const ACTION_PAST_SWAPS = 'action_past_swaps';
|
||||||
|
|
||||||
|
const NAG_API_STATUS_PENDING = 'Waiting to receive your crypto.';
|
||||||
|
const NAG_API_STATUS_CONFIRMING = 'Confirming transaction.';
|
||||||
|
const NAG_API_STATUS_PROCESSING = 'Crypto received. Sending your Credits.';
|
||||||
|
const NAG_API_STATUS_SUCCESS = 'Credits sent. You should see it in your wallet.';
|
||||||
|
const NAG_API_STATUS_ERROR = 'An error occurred on the previous swap.';
|
||||||
|
const NAG_SWAP_CALL_FAILED = 'Failed to initiate swap.';
|
||||||
|
// const NAG_STATUS_CALL_FAILED = 'Failed to query swap status.';
|
||||||
|
const NAG_SERVER_DOWN = 'The system is currently down. Come back later.';
|
||||||
|
const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.';
|
||||||
|
const NAG_EXPIRED = 'Swap expired.';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
receiveAddress: string,
|
||||||
|
coinSwaps: Array<CoinSwapInfo>,
|
||||||
|
isAuthenticated: boolean,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
addCoinSwap: (CoinSwapInfo) => void,
|
||||||
|
getNewAddress: () => void,
|
||||||
|
checkAddressIsMine: (string) => void,
|
||||||
|
openModal: (string, {}) => void,
|
||||||
|
queryCoinSwapStatus: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function WalletSwap(props: Props) {
|
||||||
|
const {
|
||||||
|
receiveAddress,
|
||||||
|
doToast,
|
||||||
|
coinSwaps,
|
||||||
|
isAuthenticated,
|
||||||
|
addCoinSwap,
|
||||||
|
getNewAddress,
|
||||||
|
checkAddressIsMine,
|
||||||
|
openModal,
|
||||||
|
queryCoinSwapStatus,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [btc, setBtc] = React.useState(0);
|
||||||
|
const [lbcError, setLbcError] = React.useState();
|
||||||
|
const [lbc, setLbc] = usePersistedState('swap-desired-lbc', LBC_MIN);
|
||||||
|
const [action, setAction] = React.useState(ACTION_MAIN);
|
||||||
|
const [nag, setNag] = React.useState(null);
|
||||||
|
const [showQr, setShowQr] = React.useState(false);
|
||||||
|
const [isFetchingRate, setIsFetchingRate] = React.useState(false);
|
||||||
|
const [isSwapping, setIsSwapping] = React.useState(false);
|
||||||
|
const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false);
|
||||||
|
const { location } = useHistory();
|
||||||
|
const [swap, setSwap] = React.useState({});
|
||||||
|
const [coin, setCoin] = React.useState('bitcoin');
|
||||||
|
const [lastStatusQuery, setLastStatusQuery] = React.useState();
|
||||||
|
const { goBack } = useHistory();
|
||||||
|
|
||||||
|
function formatCoinAmountString(amount) {
|
||||||
|
return amount === 0 ? '---' : amount.toLocaleString(undefined, { minimumFractionDigits: 8 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToMainAction() {
|
||||||
|
setIsSwapping(false);
|
||||||
|
setAction(ACTION_MAIN);
|
||||||
|
setSwap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCoinSwap(chargeCode) {
|
||||||
|
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
|
||||||
|
chargeCode: chargeCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure 'receiveAddress' is populated
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!receiveAddress) {
|
||||||
|
getNewAddress();
|
||||||
|
} else {
|
||||||
|
checkAddressIsMine(receiveAddress);
|
||||||
|
}
|
||||||
|
}, [receiveAddress, getNewAddress, checkAddressIsMine]);
|
||||||
|
|
||||||
|
// Get 'btc/rate' and calculate required BTC.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isNaN(lbc) || lbc === 0) {
|
||||||
|
setBtc(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingRate(true);
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS })
|
||||||
|
.then((rate) => {
|
||||||
|
setIsFetchingRate(false);
|
||||||
|
setBtc((lbc * Math.round(BTC_SATOSHIS * rate)) / BTC_SATOSHIS);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsFetchingRate(false);
|
||||||
|
setBtc(0);
|
||||||
|
setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' });
|
||||||
|
});
|
||||||
|
}, DEBOUNCE_BTC_CHANGE_MS);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [lbc]);
|
||||||
|
|
||||||
|
// Resolve 'swap' with the latest info from 'coinSwaps'
|
||||||
|
React.useEffect(() => {
|
||||||
|
const swapInfo = swap && coinSwaps.find((x) => x.chargeCode === swap.chargeCode);
|
||||||
|
if (!swapInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonSwap = JSON.stringify(swap);
|
||||||
|
const jsonSwapInfo = JSON.stringify(swapInfo);
|
||||||
|
if (jsonSwap !== jsonSwapInfo) {
|
||||||
|
setSwap({ ...swapInfo });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!swapInfo.status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (swapInfo.status.status) {
|
||||||
|
case BTC_API_STATUS_PENDING:
|
||||||
|
setAction(ACTION_STATUS_PENDING);
|
||||||
|
setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' });
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_CONFIRMING:
|
||||||
|
setAction(ACTION_STATUS_CONFIRMING);
|
||||||
|
setNag({ msg: NAG_API_STATUS_CONFIRMING, type: 'helpful' });
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_PROCESSING:
|
||||||
|
if (swapInfo.status.lbcTxid) {
|
||||||
|
setAction(ACTION_STATUS_SUCCESS);
|
||||||
|
setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' });
|
||||||
|
setIsSwapping(false);
|
||||||
|
} else {
|
||||||
|
setAction(ACTION_STATUS_PROCESSING);
|
||||||
|
setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_ERROR:
|
||||||
|
setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' });
|
||||||
|
break;
|
||||||
|
case INTERNAL_APIS_DOWN:
|
||||||
|
setNag({ msg: NAG_SERVER_DOWN, type: 'error' });
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_EXPIRED:
|
||||||
|
setNag({ msg: NAG_EXPIRED, type: 'error' });
|
||||||
|
if (action === ACTION_PAST_SWAPS) {
|
||||||
|
setAction(ACTION_STATUS_PENDING);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_UNRESOLVED:
|
||||||
|
setNag({
|
||||||
|
msg: __(
|
||||||
|
'Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.',
|
||||||
|
{ chargeCode: swapInfo.chargeCode }
|
||||||
|
),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
if (action === ACTION_PAST_SWAPS) {
|
||||||
|
setAction(ACTION_STATUS_PENDING);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setNag({ msg: swapInfo.status.status, type: 'error' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [swap, coinSwaps]);
|
||||||
|
|
||||||
|
// Validate entered LBC
|
||||||
|
React.useEffect(() => {
|
||||||
|
let msg;
|
||||||
|
if (lbc < LBC_MIN) {
|
||||||
|
msg = __('The amount needs to be higher');
|
||||||
|
} else if (lbc > LBC_MAX) {
|
||||||
|
msg = __('The amount is too high');
|
||||||
|
}
|
||||||
|
setLbcError(msg);
|
||||||
|
}, [lbc]);
|
||||||
|
|
||||||
|
// 'Refresh' button feedback
|
||||||
|
React.useEffect(() => {
|
||||||
|
let timer;
|
||||||
|
if (isRefreshingStatus) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
setIsRefreshingStatus(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isRefreshingStatus]);
|
||||||
|
|
||||||
|
function getCoinAddress(coin) {
|
||||||
|
if (swap && swap.sendAddresses) {
|
||||||
|
return swap.sendAddresses[coin];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoinSendAmountStr(coin) {
|
||||||
|
if (swap && swap.sendAmounts && swap.sendAmounts[coin]) {
|
||||||
|
return `${swap.sendAmounts[coin].amount} ${swap.sendAmounts[coin].currency}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function currencyToCoin(currency) {
|
||||||
|
const MAP = {
|
||||||
|
DAI: 'dai',
|
||||||
|
USDC: 'usdc',
|
||||||
|
BTC: 'bitcoin',
|
||||||
|
ETH: 'ethereum',
|
||||||
|
LTC: 'litecoin',
|
||||||
|
BCH: 'bitcoincash',
|
||||||
|
};
|
||||||
|
return MAP[currency] || 'bitcoin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSentAmountStr(swapInfo) {
|
||||||
|
if (swapInfo && swapInfo.status) {
|
||||||
|
const currency = swapInfo.status.receiptCurrency;
|
||||||
|
const coin = currencyToCoin(currency);
|
||||||
|
return getCoinSendAmountStr(coin);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoinLabel(coin) {
|
||||||
|
const COIN_LABEL = {
|
||||||
|
dai: 'Dai',
|
||||||
|
usdc: 'USD Coin',
|
||||||
|
bitcoin: 'Bitcoin',
|
||||||
|
ethereum: 'Ethereum',
|
||||||
|
litecoin: 'Litecoin',
|
||||||
|
bitcoincash: 'Bitcoin Cash',
|
||||||
|
};
|
||||||
|
|
||||||
|
return COIN_LABEL[coin] || coin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLbcAmountStrForSwap(swap) {
|
||||||
|
if (swap && swap.lbcAmount) {
|
||||||
|
return formatCoinAmountString(swap.lbcAmount);
|
||||||
|
}
|
||||||
|
return '---';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStartSwap() {
|
||||||
|
setIsSwapping(true);
|
||||||
|
setSwap(null);
|
||||||
|
setNag(null);
|
||||||
|
|
||||||
|
Lbryio.call('btc', 'swap', {
|
||||||
|
lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS + 0.5),
|
||||||
|
btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS + 0.5),
|
||||||
|
pay_to_wallet_address: receiveAddress,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const btcAmount = response.Charge.data.pricing['bitcoin'].amount;
|
||||||
|
const rate = response.Exchange.rate;
|
||||||
|
|
||||||
|
const timeline = response.Charge.data.timeline;
|
||||||
|
const lastTimeline = timeline[timeline.length - 1];
|
||||||
|
|
||||||
|
const newSwap = {
|
||||||
|
chargeCode: response.Exchange.charge_code,
|
||||||
|
coins: Object.keys(response.Charge.data.addresses),
|
||||||
|
sendAddresses: response.Charge.data.addresses,
|
||||||
|
sendAmounts: response.Charge.data.pricing,
|
||||||
|
lbcAmount: (btcAmount * BTC_SATOSHIS) / rate,
|
||||||
|
status: {
|
||||||
|
status: lastTimeline.status,
|
||||||
|
receiptCurrency: lastTimeline.payment.value.currency,
|
||||||
|
receiptTxid: lastTimeline.payment.transaction_id,
|
||||||
|
lbcTxid: response.Exchange.lbc_txid || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setSwap({ ...newSwap });
|
||||||
|
addCoinSwap({ ...newSwap });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const translateError = (err) => {
|
||||||
|
// TODO: https://github.com/lbryio/lbry.go/issues/87
|
||||||
|
// Translate error codes instead of strings when it is available.
|
||||||
|
if (err === 'users are currently limited to 4 transactions per month') {
|
||||||
|
return __('Users are currently limited to 4 completed swaps per month or 5 pending swaps.');
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : translateError(err.message), type: 'error' });
|
||||||
|
returnToMainAction();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewPastSwaps() {
|
||||||
|
setAction(ACTION_PAST_SWAPS);
|
||||||
|
setNag(null);
|
||||||
|
setIsRefreshingStatus(true);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (!lastStatusQuery || now - lastStatusQuery > 30000) {
|
||||||
|
// There is a '200/minute' limit in the commerce API. If the history is
|
||||||
|
// long, or if the user goes trigger-happy, the limit could be reached
|
||||||
|
// easily. Statuses don't change often, so just limit it to every 30s.
|
||||||
|
setLastStatusQuery(now);
|
||||||
|
coinSwaps.forEach((x) => {
|
||||||
|
queryCoinSwapStatus(x.chargeCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShortStatusStr(coinSwap: CoinSwapInfo) {
|
||||||
|
const swapInfo = coinSwaps.find((x) => x.chargeCode === coinSwap.chargeCode);
|
||||||
|
if (!swapInfo || !swapInfo.status) {
|
||||||
|
return '---';
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg;
|
||||||
|
switch (swapInfo.status.status) {
|
||||||
|
case BTC_API_STATUS_PENDING:
|
||||||
|
msg = __('Waiting');
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_CONFIRMING:
|
||||||
|
msg = __('Confirming');
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_PROCESSING:
|
||||||
|
if (swapInfo.status.lbcTxid) {
|
||||||
|
msg = __('Credits sent');
|
||||||
|
} else {
|
||||||
|
msg = __('Sending Credits');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_ERROR:
|
||||||
|
msg = __('Failed');
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_EXPIRED:
|
||||||
|
msg = __('Expired');
|
||||||
|
break;
|
||||||
|
case BTC_API_STATUS_UNRESOLVED:
|
||||||
|
msg = __('Unresolved');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
msg = swapInfo.status.status;
|
||||||
|
// if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewTransactionElement(swap, isSend) {
|
||||||
|
if (!swap || !swap.status) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const explorerUrl = (coin, txid) => {
|
||||||
|
// It's unclear whether we can link to sites like blockchain.com.
|
||||||
|
// Don't do it for now.
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSend) {
|
||||||
|
const sendTxId = swap.status.receiptTxid;
|
||||||
|
const url = explorerUrl(swap.status.receiptCurrency, sendTxId);
|
||||||
|
return sendTxId ? (
|
||||||
|
<>
|
||||||
|
{url && <Button button="link" href={url} label={__('View transaction')} />}
|
||||||
|
{!url && (
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
label={__('Copy transaction ID')}
|
||||||
|
title={sendTxId}
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.writeText(sendTxId);
|
||||||
|
doToast({
|
||||||
|
message: __('Transaction ID copied.'),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
} else {
|
||||||
|
const lbcTxId = swap.status.lbcTxid;
|
||||||
|
return lbcTxId ? (
|
||||||
|
<Button button="link" href={`https://explorer.lbry.com/tx/${lbcTxId}`} label={__('View transaction')} />
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCloseButton() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button autoFocus button="primary" label={__('Close')} onClick={() => goBack()} />
|
||||||
|
<Icon
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
customTooltipText={__(
|
||||||
|
'This page can be closed while the transactions are in progress.\nYou can view the status later from:\n • Wallet » Swap » View Past Swaps'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGap() {
|
||||||
|
return <div className="confirm__value" />; // better way?
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionElement() {
|
||||||
|
switch (action) {
|
||||||
|
case ACTION_MAIN:
|
||||||
|
return actionMain;
|
||||||
|
|
||||||
|
case ACTION_STATUS_PENDING:
|
||||||
|
return actionPending;
|
||||||
|
|
||||||
|
case ACTION_STATUS_CONFIRMING:
|
||||||
|
return actionConfirmingSend;
|
||||||
|
|
||||||
|
case ACTION_STATUS_PROCESSING: // fall-through
|
||||||
|
case ACTION_STATUS_SUCCESS:
|
||||||
|
return actionProcessingAndSuccess;
|
||||||
|
|
||||||
|
case ACTION_PAST_SWAPS:
|
||||||
|
return actionPastSwaps;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (IS_DEV) throw new Error('Unhandled action: ' + action);
|
||||||
|
return actionMain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionMain = (
|
||||||
|
<>
|
||||||
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
|
<div className="section">
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
label={
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
lbc: <LbcSymbol size={22} />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enter desired %lbc%
|
||||||
|
</I18nMessage>
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
name="lbc"
|
||||||
|
className="form-field--price-amount--auto"
|
||||||
|
affixClass="form-field--fix-no-height"
|
||||||
|
max={LBC_MAX}
|
||||||
|
min={LBC_MIN}
|
||||||
|
step={1 / BTC_SATOSHIS}
|
||||||
|
placeholder="12.34"
|
||||||
|
value={lbc}
|
||||||
|
error={lbcError}
|
||||||
|
disabled={isSwapping}
|
||||||
|
onChange={(event) => setLbc(parseFloat(event.target.value))}
|
||||||
|
/>
|
||||||
|
{getGap()}
|
||||||
|
<div className="confirm__label">{__('Estimated BTC price')}</div>
|
||||||
|
<div className="confirm__value">
|
||||||
|
{formatCoinAmountString(btc)} {btc === 0 ? '' : 'BTC'}
|
||||||
|
{isFetchingRate && <Spinner type="small" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
onClick={handleStartSwap}
|
||||||
|
button="primary"
|
||||||
|
disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || lbcError}
|
||||||
|
label={isSwapping ? __('Processing...') : __('Start Swap')}
|
||||||
|
/>
|
||||||
|
{!isSwapping && coinSwaps.length !== 0 && (
|
||||||
|
<Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
|
||||||
|
)}
|
||||||
|
{isSwapping && <Spinner type="small" />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionPending = (
|
||||||
|
<>
|
||||||
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
|
<div className="section">
|
||||||
|
{swap && swap.coins && ENABLE_ALTERNATIVE_COINS && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
type="select"
|
||||||
|
name="select_coin"
|
||||||
|
value={coin}
|
||||||
|
label={__('Alternative coins')}
|
||||||
|
onChange={(e) => setCoin(e.target.value)}
|
||||||
|
>
|
||||||
|
{swap.coins.map((x) => (
|
||||||
|
<option key={x} value={x}>
|
||||||
|
{getCoinLabel(x)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</FormField>
|
||||||
|
{getGap()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="confirm__label">{__('Send')}</div>
|
||||||
|
<CopyableText
|
||||||
|
primaryButton
|
||||||
|
copyable={getCoinSendAmountStr(coin)}
|
||||||
|
snackMessage={__('Amount copied.')}
|
||||||
|
onCopy={(inputElem) => {
|
||||||
|
const inputStr = inputElem.value;
|
||||||
|
const selectEndIndex = inputStr.lastIndexOf(' ');
|
||||||
|
if (selectEndIndex > -1 && inputStr.substring(0, selectEndIndex).match(/[\d.]/)) {
|
||||||
|
inputElem.setSelectionRange(0, selectEndIndex, 'forward');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="help">{__('Use the copy button to ensure the EXACT amount is sent!')}</div>
|
||||||
|
{getGap()}
|
||||||
|
<div className="confirm__label">{__('To')}</div>
|
||||||
|
<CopyableText primaryButton copyable={getCoinAddress(coin)} snackMessage={__('Address copied.')} />
|
||||||
|
<div className="confirm__value--subitem">
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
label={showQr ? __('Hide QR code') : __('Show QR code')}
|
||||||
|
onClick={() => setShowQr(!showQr)}
|
||||||
|
/>
|
||||||
|
{showQr && getCoinAddress(coin) && <QRCode value={getCoinAddress(coin)} />}
|
||||||
|
</div>
|
||||||
|
{getGap()}
|
||||||
|
<div className="confirm__label">{__('Receive')}</div>
|
||||||
|
<div className="confirm__value">{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">{getCloseButton()}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionConfirmingSend = (
|
||||||
|
<>
|
||||||
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
|
<div className="section">
|
||||||
|
<div className="confirm__label">{__('Confirming')}</div>
|
||||||
|
<div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
|
||||||
|
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">{getCloseButton()}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionProcessingAndSuccess = (
|
||||||
|
<>
|
||||||
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
|
<div className="section">
|
||||||
|
<div className="confirm__label">{__('Sent')}</div>
|
||||||
|
<div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
|
||||||
|
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
|
||||||
|
{getGap()}
|
||||||
|
<div className="confirm__label">{action === ACTION_STATUS_SUCCESS ? __('Received') : __('Receiving')}</div>
|
||||||
|
<div className="confirm__value confirm__value--no-gap">
|
||||||
|
{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}
|
||||||
|
</div>
|
||||||
|
{action === ACTION_STATUS_SUCCESS && (
|
||||||
|
<div className="confirm__value--subitem">{getViewTransactionElement(swap, false)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">{getCloseButton()}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionPastSwaps = (
|
||||||
|
<>
|
||||||
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
|
<div className="section">
|
||||||
|
<div className="table__wrapper">
|
||||||
|
<table className="table table--btc-swap">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{__('Code')}</th>
|
||||||
|
<th>{__('Status')}</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{coinSwaps.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td>{'---'}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{coinSwaps.length !== 0 &&
|
||||||
|
coinSwaps.map((x) => {
|
||||||
|
return (
|
||||||
|
<tr key={x.chargeCode}>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
className="button--hash-id"
|
||||||
|
title={x.chargeCode}
|
||||||
|
label={x.chargeCode}
|
||||||
|
onClick={() => {
|
||||||
|
setSwap({ ...x });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{isRefreshingStatus ? '...' : getShortStatusStr(x)}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
icon={ICONS.REMOVE}
|
||||||
|
title={__('Remove swap')}
|
||||||
|
onClick={() => removeCoinSwap(x.chargeCode)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
button="primary"
|
||||||
|
label={__('Go Back')}
|
||||||
|
onClick={() => {
|
||||||
|
returnToMainAction();
|
||||||
|
setNag(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{coinSwaps.length !== 0 && !isRefreshingStatus && (
|
||||||
|
<Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
|
||||||
|
)}
|
||||||
|
{isRefreshingStatus && <Spinner type="small" />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Redirect to={`/$/${PAGES.AUTH_SIGNIN}?redirect=${location.pathname}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleStartSwap}>
|
||||||
|
<Card
|
||||||
|
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Swap Crypto for %lbc%</I18nMessage>}
|
||||||
|
subtitle={__(
|
||||||
|
'Send crypto to the address provided and you will be sent an equivalent amount of Credits. You can pay with BCH, LTC, ETH, USDC or DAI after starting the swap.'
|
||||||
|
)}
|
||||||
|
actions={getActionElement()}
|
||||||
|
nag={nag ? <Nag relative type={nag.type} message={__(nag.msg)} /> : null}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WalletSwap;
|
|
@ -431,6 +431,11 @@ export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
|
||||||
// Blocked channels
|
// Blocked channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||||
|
|
||||||
|
// Coin swap
|
||||||
|
export const ADD_COIN_SWAP = 'ADD_COIN_SWAP';
|
||||||
|
export const REMOVE_COIN_SWAP = 'REMOVE_COIN_SWAP';
|
||||||
|
export const COIN_SWAP_STATUS_RECEIVED = 'COIN_SWAP_STATUS_RECEIVED';
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
|
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
|
||||||
export const TAG_ADD = 'TAG_ADD';
|
export const TAG_ADD = 'TAG_ADD';
|
||||||
|
|
|
@ -186,5 +186,3 @@ export const MYSTERIES = 'Mysteries';
|
||||||
export const TECHNOLOGY = 'Technology';
|
export const TECHNOLOGY = 'Technology';
|
||||||
export const EMOJI = 'Emoji';
|
export const EMOJI = 'Emoji';
|
||||||
export const STICKER = 'Sticker';
|
export const STICKER = 'Sticker';
|
||||||
export const SIMPLE_EDITOR = 'SimpleEditor';
|
|
||||||
export const ADVANCED_EDITOR = 'AdvancedEditor';
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const SYNC_ENABLE = 'SYNC_ENABLE';
|
||||||
export const IMAGE_UPLOAD = 'image_upload';
|
export const IMAGE_UPLOAD = 'image_upload';
|
||||||
export const MOBILE_SEARCH = 'mobile_search';
|
export const MOBILE_SEARCH = 'mobile_search';
|
||||||
export const VIEW_IMAGE = 'view_image';
|
export const VIEW_IMAGE = 'view_image';
|
||||||
|
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
|
||||||
export const BLOCK_CHANNEL = 'block_channel';
|
export const BLOCK_CHANNEL = 'block_channel';
|
||||||
export const COLLECTION_ADD = 'collection_add';
|
export const COLLECTION_ADD = 'collection_add';
|
||||||
export const COLLECTION_DELETE = 'collection_delete';
|
export const COLLECTION_DELETE = 'collection_delete';
|
||||||
|
|
|
@ -51,6 +51,7 @@ export const PAGE_TITLE = {
|
||||||
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
||||||
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
||||||
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
||||||
|
[PAGES.SWAP]: 'Swap Credits',
|
||||||
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
||||||
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
||||||
[PAGES.UPLOAD]: 'Upload',
|
[PAGES.UPLOAD]: 'Upload',
|
||||||
|
|
|
@ -70,6 +70,7 @@ exports.CODE_2257 = '2257';
|
||||||
exports.BUY = 'buy';
|
exports.BUY = 'buy';
|
||||||
exports.RECEIVE = 'receive';
|
exports.RECEIVE = 'receive';
|
||||||
exports.SEND = 'send';
|
exports.SEND = 'send';
|
||||||
|
exports.SWAP = 'swap';
|
||||||
exports.CHANNEL_NEW = 'channel/new';
|
exports.CHANNEL_NEW = 'channel/new';
|
||||||
exports.NOTIFICATIONS = 'notifications';
|
exports.NOTIFICATIONS = 'notifications';
|
||||||
exports.YOUTUBE_SYNC = 'youtube';
|
exports.YOUTUBE_SYNC = 'youtube';
|
||||||
|
|
|
@ -11,8 +11,7 @@ export const SUPPORT = 'support';
|
||||||
export const CHANNEL = 'channel';
|
export const CHANNEL = 'channel';
|
||||||
export const PUBLISH = 'publish';
|
export const PUBLISH = 'publish';
|
||||||
export const REPOST = 'repost';
|
export const REPOST = 'repost';
|
||||||
export const COLLECTION = 'collection';
|
export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST];
|
||||||
export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST, COLLECTION];
|
|
||||||
// dropdown subtypes
|
// dropdown subtypes
|
||||||
export const TIP = 'tip';
|
export const TIP = 'tip';
|
||||||
export const PURCHASE = 'purchase';
|
export const PURCHASE = 'purchase';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import '@babel/polyfill';
|
import 'babel-polyfill';
|
||||||
import ErrorBoundary from 'component/errorBoundary';
|
import ErrorBoundary from 'component/errorBoundary';
|
||||||
import App from 'component/app';
|
import App from 'component/app';
|
||||||
import SnackBar from 'component/snackBar';
|
import SnackBar from 'component/snackBar';
|
||||||
|
|
|
@ -36,7 +36,7 @@ const Lbry = {
|
||||||
|
|
||||||
// Returns a human readable media type based on the content type or extension of a file that is returned by the sdk
|
// Returns a human readable media type based on the content type or extension of a file that is returned by the sdk
|
||||||
getMediaType: (contentType: ?string, fileName: ?string) => {
|
getMediaType: (contentType: ?string, fileName: ?string) => {
|
||||||
if (fileName && fileName.split('.').length > 1) {
|
if (fileName) {
|
||||||
const formats = [
|
const formats = [
|
||||||
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
|
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
|
||||||
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
|
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Modal } from 'modal/modal';
|
||||||
import { formatFileSystemPath } from 'util/url';
|
import { formatFileSystemPath } from 'util/url';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
upload: (File) => void,
|
upload: WebFile => void,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
showToast: ({}) => void,
|
showToast: ({}) => void,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import ModalConfirmThumbnailUpload from './view';
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
upload: (fileWithPath, cb) => dispatch(doUploadThumbnail(null, fileWithPath.file, null, null, fileWithPath.path, cb)),
|
upload: (file, cb) => dispatch(doUploadThumbnail(null, file, null, null, file.path, cb)),
|
||||||
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { Modal } from 'modal/modal';
|
||||||
import { DOMAIN } from 'config';
|
import { DOMAIN } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
file: FileWithPath,
|
file: WebFile,
|
||||||
upload: (FileWithPath, (string) => void) => void,
|
upload: (WebFile, (string) => void) => void,
|
||||||
cb: (string) => void,
|
cb: (string) => void,
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
|
@ -23,7 +23,7 @@ class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { closeModal, file } = this.props;
|
const { closeModal, file } = this.props;
|
||||||
const filePath = file && file.path;
|
const filePath = file && (file.path || file.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
@ -9,12 +9,12 @@ import Button from 'component/button';
|
||||||
import FileList from 'component/common/file-list';
|
import FileList from 'component/common/file-list';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
files: Array<File>,
|
files: Array<WebFile>,
|
||||||
hideModal: () => void,
|
hideModal: () => void,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
history: {
|
history: {
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
push: (string) => void,
|
push: string => void,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const ModalFileSelection = (props: Props) => {
|
||||||
navigateToPublish();
|
navigateToPublish();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileChange = (file?: File) => {
|
const handleFileChange = (file?: WebFile) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,13 +14,10 @@ type Props = {
|
||||||
|
|
||||||
function ModalImageUpload(props: Props) {
|
function ModalImageUpload(props: Props) {
|
||||||
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
|
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
|
||||||
const filters = React.useMemo(() => [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg'] }]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
|
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
|
||||||
<SelectAsset
|
<SelectAsset
|
||||||
filters={filters}
|
|
||||||
type="openFile"
|
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
currentValue={currentValue}
|
currentValue={currentValue}
|
||||||
assetName={assetName}
|
assetName={assetName}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Icon from 'component/common/icon';
|
||||||
import { NO_FILE } from 'redux/actions/publish';
|
import { NO_FILE } from 'redux/actions/publish';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filePath: ?File,
|
filePath: string | WebFile,
|
||||||
isMarkdownPost: boolean,
|
isMarkdownPost: boolean,
|
||||||
optimize: boolean,
|
optimize: boolean,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
|
@ -104,11 +104,16 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
// @endif
|
// @endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilePathName(filePath: ?File) {
|
function getFilePathName(filePath: string | WebFile) {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return NO_FILE;
|
return NO_FILE;
|
||||||
}
|
}
|
||||||
return filePath.name;
|
|
||||||
|
if (typeof filePath === 'string') {
|
||||||
|
return filePath;
|
||||||
|
} else {
|
||||||
|
return filePath.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRow(label: string, value: any) {
|
function createRow(label: string, value: any) {
|
||||||
|
@ -122,7 +127,7 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
|
|
||||||
const txFee = previewResponse ? previewResponse['total_fee'] : null;
|
const txFee = previewResponse ? previewResponse['total_fee'] : null;
|
||||||
// $FlowFixMe add outputs[0] etc to PublishResponse type
|
// $FlowFixMe add outputs[0] etc to PublishResponse type
|
||||||
const isOptimizeAvail = filePath && isVid && ffmpegStatus.available;
|
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
||||||
let modalTitle;
|
let modalTitle;
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
modalTitle = __('Confirm Edit');
|
modalTitle = __('Confirm Edit');
|
||||||
|
|
13
ui/modal/modalRemoveBtcSwapAddress/index.js
Normal file
13
ui/modal/modalRemoveBtcSwapAddress/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
import ModalRemoveBtcSwapAddress from './view';
|
||||||
|
import { doRemoveCoinSwap } from 'redux/actions/coinSwap';
|
||||||
|
|
||||||
|
const select = (state, props) => ({});
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
removeCoinSwap: (chargeCode) => dispatch(doRemoveCoinSwap(chargeCode)),
|
||||||
|
closeModal: () => dispatch(doHideModal()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(ModalRemoveBtcSwapAddress);
|
43
ui/modal/modalRemoveBtcSwapAddress/view.jsx
Normal file
43
ui/modal/modalRemoveBtcSwapAddress/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
chargeCode: string,
|
||||||
|
removeCoinSwap: (string) => void,
|
||||||
|
closeModal: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ModalRemoveBtcSwapAddress(props: Props) {
|
||||||
|
const { chargeCode, removeCoinSwap, closeModal } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen contentLabel={__('Confirm Swap Removal')} type="card" onAborted={closeModal}>
|
||||||
|
<Card
|
||||||
|
title={__('Remove Swap')}
|
||||||
|
subtitle={<I18nMessage tokens={{ address: <em>{`${chargeCode}`}</em> }}>Remove %address%?</I18nMessage>}
|
||||||
|
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
label={__('OK')}
|
||||||
|
onClick={() => {
|
||||||
|
removeCoinSwap(chargeCode);
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalRemoveBtcSwapAddress;
|
|
@ -41,6 +41,8 @@ import ModalPasswordUnsave from 'modal/modalPasswordUnsave';
|
||||||
import ModalPublish from 'modal/modalPublish';
|
import ModalPublish from 'modal/modalPublish';
|
||||||
import ModalPublishPreview from 'modal/modalPublishPreview';
|
import ModalPublishPreview from 'modal/modalPublishPreview';
|
||||||
|
|
||||||
|
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
|
||||||
|
|
||||||
import ModalRemoveCard from 'modal/modalRemoveCard';
|
import ModalRemoveCard from 'modal/modalRemoveCard';
|
||||||
import ModalRemoveComment from 'modal/modalRemoveComment';
|
import ModalRemoveComment from 'modal/modalRemoveComment';
|
||||||
|
|
||||||
|
@ -135,6 +137,8 @@ function getModal(id) {
|
||||||
return ModalViewImage;
|
return ModalViewImage;
|
||||||
case MODALS.MASS_TIP_UNLOCK:
|
case MODALS.MASS_TIP_UNLOCK:
|
||||||
return ModalMassTipsUnlock;
|
return ModalMassTipsUnlock;
|
||||||
|
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
|
||||||
|
return ModalRemoveBtcSwapAddress;
|
||||||
case MODALS.BLOCK_CHANNEL:
|
case MODALS.BLOCK_CHANNEL:
|
||||||
return ModalBlockChannel;
|
return ModalBlockChannel;
|
||||||
case MODALS.COLLECTION_ADD:
|
case MODALS.COLLECTION_ADD:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import FileTitleSection from 'component/fileTitleSection';
|
import FileTitleSection from 'component/fileTitleSection';
|
||||||
|
@ -60,7 +59,6 @@ function FilePage(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
const cost = costInfo ? costInfo.cost : null;
|
const cost = costInfo ? costInfo.cost : null;
|
||||||
const hasFileInfo = fileInfo !== undefined;
|
const hasFileInfo = fileInfo !== undefined;
|
||||||
const isMediumScreen = useIsMediumScreen();
|
|
||||||
const isMarkdown = renderMode === RENDER_MODES.MARKDOWN;
|
const isMarkdown = renderMode === RENDER_MODES.MARKDOWN;
|
||||||
const videoPlayedEnoughToResetPosition = React.useMemo(() => {
|
const videoPlayedEnoughToResetPosition = React.useMemo(() => {
|
||||||
const durationInSecs =
|
const durationInSecs =
|
||||||
|
@ -171,10 +169,8 @@ function FilePage(props: Props) {
|
||||||
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||||
<FileTitleSection uri={uri} isNsfwBlocked />
|
<FileTitleSection uri={uri} isNsfwBlocked />
|
||||||
</div>
|
</div>
|
||||||
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||||
<CollectionContent id={collectionId} uri={uri} />
|
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||||
)}
|
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -191,17 +187,13 @@ function FilePage(props: Props) {
|
||||||
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
||||||
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
|
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
|
||||||
</div>
|
</div>
|
||||||
{!collection && !isMarkdown && videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||||
{collection && !isMarkdown && videoTheaterMode && !isMediumScreen && (
|
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||||
<CollectionContent id={collectionId} uri={uri} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||||
<CollectionContent id={collectionId} uri={uri} />
|
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||||
)}
|
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
|
||||||
{isMarkdown && (
|
{isMarkdown && (
|
||||||
<div className="file-page__post-comments">
|
<div className="file-page__post-comments">
|
||||||
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />}
|
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ReportPage extends React.Component {
|
||||||
name="message"
|
name="message"
|
||||||
stretch
|
stretch
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={(event) => {
|
onChange={event => {
|
||||||
this.onMessageChange(event);
|
this.onMessageChange(event);
|
||||||
}}
|
}}
|
||||||
placeholder={__('Description of your issue or feature request')}
|
placeholder={__('Description of your issue or feature request')}
|
||||||
|
@ -71,7 +71,7 @@ class ReportPage extends React.Component {
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
onClick={(event) => {
|
onClick={event => {
|
||||||
this.submitMessage(event);
|
this.submitMessage(event);
|
||||||
}}
|
}}
|
||||||
className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`}
|
className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`}
|
||||||
|
|
|
@ -313,6 +313,39 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={
|
||||||
|
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
|
||||||
|
}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
{__(HELP.MIN_SUPER)}
|
||||||
|
{minTip !== 0 && (
|
||||||
|
<p className="help--inline">
|
||||||
|
<em>{__(HELP.MIN_SUPER_OFF)}</em>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
name="min_tip_amount_super_chat"
|
||||||
|
className="form-field--price-amount"
|
||||||
|
min={0}
|
||||||
|
step="any"
|
||||||
|
type="number"
|
||||||
|
placeholder="1"
|
||||||
|
value={minSuper}
|
||||||
|
disabled={minTip !== 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newMinSuper = parseFloat(e.target.value);
|
||||||
|
setMinSuper(newMinSuper);
|
||||||
|
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
|
||||||
|
}}
|
||||||
|
onBlur={() => setLastUpdated(Date.now())}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
|
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
|
||||||
<SearchChannelField
|
<SearchChannelField
|
||||||
label={__('Moderators')}
|
label={__('Moderators')}
|
||||||
|
|
|
@ -10,9 +10,6 @@ import {
|
||||||
selectTitleForUri,
|
selectTitleForUri,
|
||||||
selectClaimIsMine,
|
selectClaimIsMine,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
makeSelectIsBlacklisted,
|
|
||||||
makeSelectBlacklistedDueToDMCA,
|
|
||||||
makeSelectClaimErrorCensor,
|
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import {
|
import {
|
||||||
makeSelectCollectionForId,
|
makeSelectCollectionForId,
|
||||||
|
@ -26,6 +23,7 @@ import { normalizeURI } from 'util/lbryURI';
|
||||||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
import ShowPage from './view';
|
import ShowPage from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -74,6 +72,7 @@ const select = (state, props) => {
|
||||||
uri,
|
uri,
|
||||||
claim,
|
claim,
|
||||||
isResolvingUri: selectIsUriResolving(state, uri),
|
isResolvingUri: selectIsUriResolving(state, uri),
|
||||||
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
||||||
isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
|
isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
|
||||||
title: selectTitleForUri(state, uri),
|
title: selectTitleForUri(state, uri),
|
||||||
|
@ -83,9 +82,6 @@ const select = (state, props) => {
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
|
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
|
||||||
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
|
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
|
||||||
isBlacklisted: makeSelectIsBlacklisted(uri)(state),
|
|
||||||
isBlacklistedDueToDMCA: makeSelectBlacklistedDueToDMCA(uri)(state),
|
|
||||||
errorCensor: makeSelectClaimErrorCensor(uri)(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
location: UrlLocation,
|
location: UrlLocation,
|
||||||
|
blackListedOutpoints: Array<{
|
||||||
|
txid: string,
|
||||||
|
nout: number,
|
||||||
|
}>,
|
||||||
title: string,
|
title: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
claimIsPending: boolean,
|
claimIsPending: boolean,
|
||||||
|
@ -31,9 +35,6 @@ type Props = {
|
||||||
collectionUrls: Array<string>,
|
collectionUrls: Array<string>,
|
||||||
isResolvingCollection: boolean,
|
isResolvingCollection: boolean,
|
||||||
fetchCollectionItems: (string) => void,
|
fetchCollectionItems: (string) => void,
|
||||||
isBlacklisted: boolean,
|
|
||||||
isBlacklistedDueToDMCA: boolean,
|
|
||||||
errorCensor: ?ClaimErrorCensor,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShowPage(props: Props) {
|
function ShowPage(props: Props) {
|
||||||
|
@ -42,6 +43,7 @@ function ShowPage(props: Props) {
|
||||||
resolveUri,
|
resolveUri,
|
||||||
uri,
|
uri,
|
||||||
claim,
|
claim,
|
||||||
|
blackListedOutpoints,
|
||||||
location,
|
location,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
|
@ -52,13 +54,11 @@ function ShowPage(props: Props) {
|
||||||
collection,
|
collection,
|
||||||
collectionUrls,
|
collectionUrls,
|
||||||
isResolvingCollection,
|
isResolvingCollection,
|
||||||
isBlacklisted,
|
|
||||||
isBlacklistedDueToDMCA,
|
|
||||||
errorCensor,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
|
|
||||||
|
const signingChannel = claim && claim.signing_channel;
|
||||||
const canonicalUrl = claim && claim.canonical_url;
|
const canonicalUrl = claim && claim.canonical_url;
|
||||||
const claimExists = claim !== null && claim !== undefined;
|
const claimExists = claim !== null && claim !== undefined;
|
||||||
const haventFetchedYet = claim === undefined;
|
const haventFetchedYet = claim === undefined;
|
||||||
|
@ -103,7 +103,7 @@ function ShowPage(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let innerContent = '';
|
let innerContent = '';
|
||||||
if ((!claim || (claim && !claim.name)) && !isBlacklisted) {
|
if (!claim || (claim && !claim.name)) {
|
||||||
innerContent = (
|
innerContent = (
|
||||||
<Page>
|
<Page>
|
||||||
{(claim === undefined ||
|
{(claim === undefined ||
|
||||||
|
@ -142,42 +142,38 @@ function ShowPage(props: Props) {
|
||||||
{!isResolvingUri && isSubscribed && claim === null && <AbandonedChannelPreview uri={uri} type={'large'} />}
|
{!isResolvingUri && isSubscribed && claim === null && <AbandonedChannelPreview uri={uri} type={'large'} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
} else if (claim && claim.name.length && claim.name[0] === '@') {
|
} else if (claim.name.length && claim.name[0] === '@') {
|
||||||
innerContent = <ChannelPage uri={uri} location={location} />;
|
innerContent = <ChannelPage uri={uri} location={location} />;
|
||||||
} else if (isBlacklisted && !claimIsMine) {
|
|
||||||
innerContent = isBlacklistedDueToDMCA ? (
|
|
||||||
<Page>
|
|
||||||
<Card
|
|
||||||
title={uri}
|
|
||||||
subtitle={__(
|
|
||||||
'Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.'
|
|
||||||
)}
|
|
||||||
actions={
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button button="link" href="https://lbry.com/faq/dmca" label={__('Read More')} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Page>
|
|
||||||
) : (
|
|
||||||
<Page>
|
|
||||||
<Card
|
|
||||||
title={uri}
|
|
||||||
subtitle={
|
|
||||||
<>
|
|
||||||
{__('Your hub has blocked this content because it subscribes to the following blocking channel:')}{' '}
|
|
||||||
<Button
|
|
||||||
button="link"
|
|
||||||
navigate={errorCensor && errorCensor.canonical_url}
|
|
||||||
label={errorCensor && errorCensor.name}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
} else if (claim) {
|
} else if (claim) {
|
||||||
innerContent = <FilePage uri={uri} location={location} />;
|
let isClaimBlackListed = false;
|
||||||
|
|
||||||
|
isClaimBlackListed =
|
||||||
|
blackListedOutpoints &&
|
||||||
|
blackListedOutpoints.some(
|
||||||
|
(outpoint) =>
|
||||||
|
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
|
||||||
|
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isClaimBlackListed && !claimIsMine) {
|
||||||
|
innerContent = (
|
||||||
|
<Page>
|
||||||
|
<Card
|
||||||
|
title={uri}
|
||||||
|
subtitle={__(
|
||||||
|
'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.'
|
||||||
|
)}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="link" href="https://lbry.com/faq/dmca" label={__('Read More')} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
innerContent = <FilePage uri={uri} location={location} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>{innerContent}</React.Fragment>;
|
return <React.Fragment>{innerContent}</React.Fragment>;
|
||||||
|
|
|
@ -16,6 +16,7 @@ import rewardsReducer from 'redux/reducers/rewards';
|
||||||
import userReducer from 'redux/reducers/user';
|
import userReducer from 'redux/reducers/user';
|
||||||
import commentsReducer from 'redux/reducers/comments';
|
import commentsReducer from 'redux/reducers/comments';
|
||||||
import blockedReducer from 'redux/reducers/blocked';
|
import blockedReducer from 'redux/reducers/blocked';
|
||||||
|
import coinSwapReducer from 'redux/reducers/coinSwap';
|
||||||
import searchReducer from 'redux/reducers/search';
|
import searchReducer from 'redux/reducers/search';
|
||||||
import reactionsReducer from 'redux/reducers/reactions';
|
import reactionsReducer from 'redux/reducers/reactions';
|
||||||
import syncReducer from 'redux/reducers/sync';
|
import syncReducer from 'redux/reducers/sync';
|
||||||
|
@ -43,6 +44,7 @@ export default (history) =>
|
||||||
subscriptions: subscriptionsReducer,
|
subscriptions: subscriptionsReducer,
|
||||||
tags: tagsReducer,
|
tags: tagsReducer,
|
||||||
blocked: blockedReducer,
|
blocked: blockedReducer,
|
||||||
|
coinSwap: coinSwapReducer,
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
wallet: walletReducer,
|
wallet: walletReducer,
|
||||||
sync: syncReducer,
|
sync: syncReducer,
|
||||||
|
|
|
@ -458,34 +458,32 @@ export function doAnalyticsView(uri, timeToStart) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAnalyticsBuffer(uri, bufferData) {
|
export function doAnalyticsBuffer(uri, bufferData) {
|
||||||
return () => {
|
return (dispatch, getState) => {
|
||||||
// return (dispatch, getState) => {
|
const state = getState();
|
||||||
// const state = getState();
|
const claim = selectClaimForUri(state, uri);
|
||||||
// const claim = selectClaimForUri(state, uri);
|
const user = selectUser(state);
|
||||||
// const user = selectUser(state);
|
const {
|
||||||
// const {
|
value: { video, audio, source },
|
||||||
// value: { video, audio, source },
|
} = claim;
|
||||||
// } = claim;
|
const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
||||||
// const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
||||||
// const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||||
// const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
const fileSize = source.size; // size in bytes
|
||||||
// const fileSize = source.size; // size in bytes
|
const fileSizeInBits = fileSize * 8;
|
||||||
// const fileSizeInBits = fileSize * 8;
|
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||||
// const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
const userId = user && user.id.toString();
|
||||||
// const userId = user && user.id.toString();
|
|
||||||
// if there's a logged in user, send buffer event data to watchman
|
// if there's a logged in user, send buffer event data to watchman
|
||||||
// if (<condition>) {
|
if (userId) {
|
||||||
// STUB: any buffer events here
|
analytics.videoBufferEvent(claim, {
|
||||||
// analytics.videoBufferEvent(claim, {
|
timeAtBuffer,
|
||||||
// timeAtBuffer,
|
bufferDuration,
|
||||||
// bufferDuration,
|
bitRate,
|
||||||
// bitRate,
|
userId,
|
||||||
// userId,
|
duration: fileDurationInSeconds,
|
||||||
// duration: fileDurationInSeconds,
|
playerPoweredBy: bufferData.playerPoweredBy,
|
||||||
// playerPoweredBy: bufferData.playerPoweredBy,
|
readyState: bufferData.readyState,
|
||||||
// readyState: bufferData.readyState,
|
});
|
||||||
// });
|
}
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ export function doResolveUris(
|
||||||
if (uriResolveInfo) {
|
if (uriResolveInfo) {
|
||||||
if (uriResolveInfo.error) {
|
if (uriResolveInfo.error) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
resolveInfo[uri] = { ...fallbackResolveInfo, errorCensor: uriResolveInfo.error.censor };
|
resolveInfo[uri] = { ...fallbackResolveInfo };
|
||||||
} else {
|
} else {
|
||||||
if (checkReposts) {
|
if (checkReposts) {
|
||||||
if (uriResolveInfo.reposted_claim) {
|
if (uriResolveInfo.reposted_claim) {
|
||||||
|
|
44
ui/redux/actions/coinSwap.js
Normal file
44
ui/redux/actions/coinSwap.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { selectPrefsReady } from 'redux/selectors/sync';
|
||||||
|
import { doAlertWaitingForSync } from 'redux/actions/app';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
|
||||||
|
export const doAddCoinSwap = (coinSwapInfo: CoinSwapInfo) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const ready = selectPrefsReady(state);
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return dispatch(doAlertWaitingForSync());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.ADD_COIN_SWAP,
|
||||||
|
data: coinSwapInfo,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doRemoveCoinSwap = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const ready = selectPrefsReady(state);
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return dispatch(doAlertWaitingForSync());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.REMOVE_COIN_SWAP,
|
||||||
|
data: {
|
||||||
|
chargeCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doQueryCoinSwapStatus = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
Lbryio.call('btc', 'status', { charge_code: chargeCode }).then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
|
||||||
|
data: response,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -1143,9 +1143,6 @@ export function doCommentModUnBlockAsModerator(commenterUri: string, creatorUri:
|
||||||
|
|
||||||
export function doFetchModBlockedList() {
|
export function doFetchModBlockedList() {
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const LOOP_CHUNK_SIZE = 1;
|
|
||||||
const yieldThread = () => new Promise((resolve) => setTimeout(resolve));
|
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const myChannels = selectMyChannelClaims(state);
|
const myChannels = selectMyChannelClaims(state);
|
||||||
if (!myChannels) {
|
if (!myChannels) {
|
||||||
|
@ -1175,7 +1172,7 @@ export function doFetchModBlockedList() {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(async (res) => {
|
.then((res) => {
|
||||||
let personalBlockList = [];
|
let personalBlockList = [];
|
||||||
let adminBlockList = [];
|
let adminBlockList = [];
|
||||||
let moderatorBlockList = [];
|
let moderatorBlockList = [];
|
||||||
|
@ -1188,76 +1185,66 @@ export function doFetchModBlockedList() {
|
||||||
const adminTimeoutMap = {};
|
const adminTimeoutMap = {};
|
||||||
const moderatorTimeoutMap = {};
|
const moderatorTimeoutMap = {};
|
||||||
|
|
||||||
const blockListsPerChannel = [];
|
const blockListsPerChannel = res.map((r) => r.value);
|
||||||
for (let i = 0; i < res.length; ++i) {
|
blockListsPerChannel
|
||||||
blockListsPerChannel.push(res[i].value);
|
.sort((a, b) => {
|
||||||
if (i % 2 === 0) {
|
return 1;
|
||||||
await yieldThread();
|
})
|
||||||
}
|
.forEach((channelBlockLists) => {
|
||||||
}
|
const storeList = (fetchedList, blockedList, timeoutMap, blockedByMap) => {
|
||||||
for (let i = 0; i < blockListsPerChannel.length; ++i) {
|
if (fetchedList) {
|
||||||
const storeList = async (fetchedList, blockedList, timeoutMap, blockedByMap) => {
|
fetchedList.forEach((blockedChannel) => {
|
||||||
if (fetchedList) {
|
if (blockedChannel.blocked_channel_name) {
|
||||||
for (let j = 0; j < fetchedList.length; ++j) {
|
const channelUri = buildURI({
|
||||||
const blockedChannel = fetchedList[j];
|
channelName: blockedChannel.blocked_channel_name,
|
||||||
if (j > 0 && i % LOOP_CHUNK_SIZE === 0) {
|
channelClaimId: blockedChannel.blocked_channel_id,
|
||||||
await yieldThread();
|
|
||||||
}
|
|
||||||
if (blockedChannel.blocked_channel_name) {
|
|
||||||
const channelUri = buildURI({
|
|
||||||
channelName: blockedChannel.blocked_channel_name,
|
|
||||||
channelClaimId: blockedChannel.blocked_channel_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
|
||||||
blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
|
|
||||||
|
|
||||||
if (blockedChannel.banned_for) {
|
|
||||||
timeoutMap[channelUri] = {
|
|
||||||
blockedAt: blockedChannel.blocked_at,
|
|
||||||
bannedFor: blockedChannel.banned_for,
|
|
||||||
banRemaining: blockedChannel.ban_remaining,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockedByMap !== undefined) {
|
|
||||||
const blockedByChannelUri = buildURI({
|
|
||||||
channelName: blockedChannel.blocked_by_channel_name,
|
|
||||||
channelClaimId: blockedChannel.blocked_by_channel_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blockedByMap[channelUri]) {
|
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
||||||
if (!blockedByMap[channelUri].includes(blockedByChannelUri)) {
|
blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
|
||||||
blockedByMap[channelUri].push(blockedByChannelUri);
|
|
||||||
|
if (blockedChannel.banned_for) {
|
||||||
|
timeoutMap[channelUri] = {
|
||||||
|
blockedAt: blockedChannel.blocked_at,
|
||||||
|
bannedFor: blockedChannel.banned_for,
|
||||||
|
banRemaining: blockedChannel.ban_remaining,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockedByMap !== undefined) {
|
||||||
|
const blockedByChannelUri = buildURI({
|
||||||
|
channelName: blockedChannel.blocked_by_channel_name,
|
||||||
|
channelClaimId: blockedChannel.blocked_by_channel_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blockedByMap[channelUri]) {
|
||||||
|
if (!blockedByMap[channelUri].includes(blockedByChannelUri)) {
|
||||||
|
blockedByMap[channelUri].push(blockedByChannelUri);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blockedByMap[channelUri] = [blockedByChannelUri];
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
blockedByMap[channelUri] = [blockedByChannelUri];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const channelBlockLists = blockListsPerChannel[i];
|
const blocked_channels = channelBlockLists && channelBlockLists.blocked_channels;
|
||||||
const blocked_channels = channelBlockLists && channelBlockLists.blocked_channels;
|
const globally_blocked_channels = channelBlockLists && channelBlockLists.globally_blocked_channels;
|
||||||
const globally_blocked_channels = channelBlockLists && channelBlockLists.globally_blocked_channels;
|
const delegated_blocked_channels = channelBlockLists && channelBlockLists.delegated_blocked_channels;
|
||||||
const delegated_blocked_channels = channelBlockLists && channelBlockLists.delegated_blocked_channels;
|
|
||||||
|
|
||||||
if (i > 0 && i % LOOP_CHUNK_SIZE === 0) {
|
storeList(blocked_channels, personalBlockList, personalTimeoutMap);
|
||||||
await yieldThread();
|
storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
|
||||||
}
|
storeList(
|
||||||
|
delegated_blocked_channels,
|
||||||
|
moderatorBlockList,
|
||||||
|
moderatorTimeoutMap,
|
||||||
|
moderatorBlockListDelegatorsMap
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await storeList(blocked_channels, personalBlockList, personalTimeoutMap);
|
|
||||||
await storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
|
|
||||||
await storeList(
|
|
||||||
delegated_blocked_channels,
|
|
||||||
moderatorBlockList,
|
|
||||||
moderatorTimeoutMap,
|
|
||||||
moderatorBlockListDelegatorsMap
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import Lbry from 'lbry';
|
||||||
import { isClaimNsfw } from 'util/claim';
|
import { isClaimNsfw } from 'util/claim';
|
||||||
|
|
||||||
export const NO_FILE = '---';
|
export const NO_FILE = '---';
|
||||||
export const doPublishDesktop = (filePath: ?File, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
const publishPreview = (previewResponse) => {
|
const publishPreview = (previewResponse) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
||||||
|
@ -138,327 +138,335 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) =>
|
||||||
data: { ...publishFormValue },
|
data: { ...publishFormValue },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const doUploadThumbnail =
|
export const doUploadThumbnail = (
|
||||||
(filePath?: string, thumbnailBlob?: File, fsAdapter?: any, fs?: any, path?: any, cb?: (string) => void) =>
|
filePath?: string,
|
||||||
(dispatch: Dispatch) => {
|
thumbnailBlob?: File,
|
||||||
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
fsAdapter?: any,
|
||||||
let thumbnail, fileExt, fileName, fileType;
|
fs?: any,
|
||||||
|
path?: any,
|
||||||
|
cb?: (string) => void
|
||||||
|
) => (dispatch: Dispatch) => {
|
||||||
|
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
||||||
|
let thumbnail, fileExt, fileName, fileType;
|
||||||
|
|
||||||
const makeid = () => {
|
const makeid = () => {
|
||||||
let text = '';
|
let text = '';
|
||||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadError = (error = '') => {
|
const uploadError = (error = '') => {
|
||||||
dispatch(
|
dispatch(
|
||||||
batchActions(
|
batchActions(
|
||||||
{
|
{
|
||||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
data: {
|
data: {
|
||||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
||||||
thumbnail: '',
|
thumbnail: '',
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
doError(error)
|
},
|
||||||
)
|
doError(error)
|
||||||
);
|
)
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
data: {
|
data: {
|
||||||
thumbnailError: undefined,
|
thumbnailError: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const doUpload = (data) => {
|
const doUpload = (data) => {
|
||||||
return fetch(SPEECH_PUBLISH, {
|
return fetch(SPEECH_PUBLISH, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
})
|
})
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then((text) => (text.length ? JSON.parse(text) : {}))
|
.then((text) => (text.length ? JSON.parse(text) : {}))
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!json.success) return uploadError(json.message || downMessage);
|
if (!json.success) return uploadError(json.message || downMessage);
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb(json.data.serveUrl);
|
cb(json.data.serveUrl);
|
||||||
}
|
}
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
data: {
|
data: {
|
||||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
||||||
thumbnail: json.data.serveUrl,
|
thumbnail: json.data.serveUrl,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
let message = err.message;
|
|
||||||
|
|
||||||
// This sucks but ¯\_(ツ)_/¯
|
|
||||||
if (message === 'Failed to fetch') {
|
|
||||||
message = downMessage;
|
|
||||||
}
|
|
||||||
const userInput = [fileName, fileExt, fileType, thumbnail];
|
|
||||||
uploadError(`${message}\nUser input: ${userInput.join(', ')}`);
|
|
||||||
});
|
});
|
||||||
};
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
let message = err.message;
|
||||||
|
|
||||||
dispatch({
|
// This sucks but ¯\_(ツ)_/¯
|
||||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
if (message === 'Failed to fetch') {
|
||||||
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
|
message = downMessage;
|
||||||
});
|
}
|
||||||
|
const userInput = [fileName, fileExt, fileType, thumbnail];
|
||||||
if (fsAdapter && fsAdapter.readFile && filePath) {
|
uploadError(`${message}\nUser input: ${userInput.join(', ')}`);
|
||||||
fsAdapter.readFile(filePath, 'base64').then((base64Image) => {
|
|
||||||
fileExt = 'png';
|
|
||||||
fileName = 'thumbnail.png';
|
|
||||||
fileType = 'image/png';
|
|
||||||
|
|
||||||
const data = new FormData();
|
|
||||||
const name = makeid();
|
|
||||||
data.append('name', name);
|
|
||||||
// $FlowFixMe
|
|
||||||
data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName });
|
|
||||||
return doUpload(data);
|
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
if (filePath && fs && path) {
|
|
||||||
thumbnail = fs.readFileSync(filePath);
|
dispatch({
|
||||||
fileExt = path.extname(filePath);
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
fileName = path.basename(filePath);
|
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
|
||||||
fileType = `image/${fileExt.slice(1)}`;
|
});
|
||||||
} else if (thumbnailBlob) {
|
|
||||||
fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`;
|
if (fsAdapter && fsAdapter.readFile && filePath) {
|
||||||
fileName = thumbnailBlob.name;
|
fsAdapter.readFile(filePath, 'base64').then((base64Image) => {
|
||||||
fileType = thumbnailBlob.type;
|
fileExt = 'png';
|
||||||
} else {
|
fileName = 'thumbnail.png';
|
||||||
return null;
|
fileType = 'image/png';
|
||||||
}
|
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
const name = makeid();
|
const name = makeid();
|
||||||
const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType }));
|
|
||||||
data.append('name', name);
|
data.append('name', name);
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
data.append('file', file);
|
data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName });
|
||||||
return doUpload(data);
|
return doUpload(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (filePath && fs && path) {
|
||||||
|
thumbnail = fs.readFileSync(filePath);
|
||||||
|
fileExt = path.extname(filePath);
|
||||||
|
fileName = path.basename(filePath);
|
||||||
|
fileType = `image/${fileExt.slice(1)}`;
|
||||||
|
} else if (thumbnailBlob) {
|
||||||
|
fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`;
|
||||||
|
fileName = thumbnailBlob.name;
|
||||||
|
fileType = thumbnailBlob.type;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
const name = makeid();
|
||||||
|
const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType }));
|
||||||
|
data.append('name', name);
|
||||||
|
// $FlowFixMe
|
||||||
|
data.append('file', file);
|
||||||
|
return doUpload(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (
|
||||||
|
dispatch: Dispatch
|
||||||
|
) => {
|
||||||
|
const { name, amount, value = {} } = claim;
|
||||||
|
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
||||||
|
const {
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
// use same values as default state
|
||||||
|
// fee will be undefined for free content
|
||||||
|
fee = {
|
||||||
|
amount: '0',
|
||||||
|
currency: 'LBC',
|
||||||
|
},
|
||||||
|
languages,
|
||||||
|
release_time,
|
||||||
|
license,
|
||||||
|
license_url: licenseUrl,
|
||||||
|
thumbnail,
|
||||||
|
title,
|
||||||
|
tags,
|
||||||
|
} = value;
|
||||||
|
|
||||||
|
const publishData: UpdatePublishFormData = {
|
||||||
|
name,
|
||||||
|
bid: Number(amount),
|
||||||
|
contentIsFree: fee.amount === '0',
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
fee,
|
||||||
|
languages,
|
||||||
|
releaseTime: release_time,
|
||||||
|
releaseTimeEdited: undefined,
|
||||||
|
thumbnail: thumbnail ? thumbnail.url : null,
|
||||||
|
title,
|
||||||
|
uri,
|
||||||
|
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
||||||
|
licenseUrl,
|
||||||
|
nsfw: isClaimNsfw(claim),
|
||||||
|
tags: tags ? tags.map((tag) => ({ name: tag })) : [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doPrepareEdit =
|
// Make sure custom licenses are mapped properly
|
||||||
(claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (dispatch: Dispatch) => {
|
// If the license isn't one of the standard licenses, map the custom license and description/url
|
||||||
const { name, amount, value = {} } = claim;
|
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
||||||
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
|
||||||
const {
|
|
||||||
author,
|
|
||||||
description,
|
|
||||||
// use same values as default state
|
|
||||||
// fee will be undefined for free content
|
|
||||||
fee = {
|
|
||||||
amount: '0',
|
|
||||||
currency: 'LBC',
|
|
||||||
},
|
|
||||||
languages,
|
|
||||||
release_time,
|
|
||||||
license,
|
|
||||||
license_url: licenseUrl,
|
|
||||||
thumbnail,
|
|
||||||
title,
|
|
||||||
tags,
|
|
||||||
} = value;
|
|
||||||
|
|
||||||
const publishData: UpdatePublishFormData = {
|
|
||||||
name,
|
|
||||||
bid: Number(amount),
|
|
||||||
contentIsFree: fee.amount === '0',
|
|
||||||
author,
|
|
||||||
description,
|
|
||||||
fee,
|
|
||||||
languages,
|
|
||||||
releaseTime: release_time,
|
|
||||||
releaseTimeEdited: undefined,
|
|
||||||
thumbnail: thumbnail ? thumbnail.url : null,
|
|
||||||
title,
|
|
||||||
uri,
|
|
||||||
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
|
||||||
licenseUrl,
|
|
||||||
nsfw: isClaimNsfw(claim),
|
|
||||||
tags: tags ? tags.map((tag) => ({ name: tag })) : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make sure custom licenses are mapped properly
|
|
||||||
// If the license isn't one of the standard licenses, map the custom license and description/url
|
|
||||||
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
|
||||||
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
|
|
||||||
publishData.licenseType = license;
|
|
||||||
} else if (license && !licenseUrl && license !== NONE) {
|
|
||||||
publishData.licenseType = COPYRIGHT;
|
|
||||||
} else {
|
|
||||||
publishData.licenseType = OTHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
publishData.otherLicenseDescription = license;
|
|
||||||
} else {
|
|
||||||
publishData.licenseType = license;
|
publishData.licenseType = license;
|
||||||
}
|
} else if (license && !licenseUrl && license !== NONE) {
|
||||||
if (channelName) {
|
publishData.licenseType = COPYRIGHT;
|
||||||
publishData['channel'] = channelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const doPublish =
|
|
||||||
(success: Function, fail: Function, preview: Function) => (dispatch: Dispatch, getState: () => {}) => {
|
|
||||||
if (!preview) {
|
|
||||||
dispatch({ type: ACTIONS.PUBLISH_START });
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const myClaimForUri = selectMyClaimForUri(state);
|
|
||||||
const myChannels = selectMyChannelClaims(state);
|
|
||||||
// const myClaims = selectMyClaimsWithoutChannels(state);
|
|
||||||
// get redux publish form
|
|
||||||
const publishData = selectPublishFormValues(state);
|
|
||||||
|
|
||||||
// destructure the data values
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
bid,
|
|
||||||
filePath,
|
|
||||||
description,
|
|
||||||
language,
|
|
||||||
releaseTimeEdited,
|
|
||||||
// license,
|
|
||||||
licenseUrl,
|
|
||||||
useLBRYUploader,
|
|
||||||
licenseType,
|
|
||||||
otherLicenseDescription,
|
|
||||||
thumbnail,
|
|
||||||
channel,
|
|
||||||
title,
|
|
||||||
contentIsFree,
|
|
||||||
fee,
|
|
||||||
tags,
|
|
||||||
// locations,
|
|
||||||
optimize,
|
|
||||||
} = publishData;
|
|
||||||
|
|
||||||
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
|
||||||
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
|
||||||
|
|
||||||
let publishingLicense;
|
|
||||||
switch (licenseType) {
|
|
||||||
case COPYRIGHT:
|
|
||||||
case OTHER:
|
|
||||||
publishingLicense = otherLicenseDescription;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
publishingLicense = licenseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the claim id from the channel name, we will use that instead
|
|
||||||
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
|
||||||
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
|
||||||
|
|
||||||
const publishPayload: {
|
|
||||||
name: ?string,
|
|
||||||
bid: string,
|
|
||||||
description?: string,
|
|
||||||
channel_id?: string,
|
|
||||||
file_path?: string,
|
|
||||||
license_url?: string,
|
|
||||||
license?: string,
|
|
||||||
thumbnail_url?: string,
|
|
||||||
release_time?: number,
|
|
||||||
fee_currency?: string,
|
|
||||||
fee_amount?: string,
|
|
||||||
languages?: Array<string>,
|
|
||||||
tags: Array<string>,
|
|
||||||
locations?: Array<any>,
|
|
||||||
blocking: boolean,
|
|
||||||
optimize_file?: boolean,
|
|
||||||
preview?: boolean,
|
|
||||||
remote_url?: string,
|
|
||||||
} = {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
locations: [],
|
|
||||||
bid: creditsToString(bid),
|
|
||||||
languages: [language],
|
|
||||||
tags: tags && tags.map((tag) => tag.name),
|
|
||||||
thumbnail_url: thumbnail,
|
|
||||||
blocking: true,
|
|
||||||
preview: false,
|
|
||||||
};
|
|
||||||
// Temporary solution to keep the same publish flow with the new tags api
|
|
||||||
// Eventually we will allow users to enter their own tags on publish
|
|
||||||
|
|
||||||
if (publishingLicense) {
|
|
||||||
publishPayload.license = publishingLicense;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (licenseUrl) {
|
|
||||||
publishPayload.license_url = licenseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbnail) {
|
|
||||||
publishPayload.thumbnail_url = thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useLBRYUploader) {
|
|
||||||
publishPayload.tags.push('lbry-first');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
|
||||||
if (releaseTimeEdited) {
|
|
||||||
publishPayload.release_time = releaseTimeEdited;
|
|
||||||
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
|
||||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
|
||||||
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
|
||||||
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
|
||||||
} else {
|
} else {
|
||||||
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
publishData.licenseType = OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelId) {
|
publishData.otherLicenseDescription = license;
|
||||||
publishPayload.channel_id = channelId;
|
} else {
|
||||||
}
|
publishData.licenseType = license;
|
||||||
|
}
|
||||||
|
if (channelName) {
|
||||||
|
publishData['channel'] = channelName;
|
||||||
|
}
|
||||||
|
|
||||||
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||||
publishPayload.locations = myClaimForUriEditing.value.locations;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
export const doPublish = (success: Function, fail: Function, preview: Function) => (
|
||||||
publishPayload.fee_currency = fee.currency;
|
dispatch: Dispatch,
|
||||||
publishPayload.fee_amount = creditsToString(fee.amount);
|
getState: () => {}
|
||||||
}
|
) => {
|
||||||
|
if (!preview) {
|
||||||
|
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||||
|
}
|
||||||
|
|
||||||
if (optimize) {
|
const state = getState();
|
||||||
publishPayload.optimize_file = true;
|
const myClaimForUri = selectMyClaimForUri(state);
|
||||||
}
|
const myChannels = selectMyChannelClaims(state);
|
||||||
|
// const myClaims = selectMyClaimsWithoutChannels(state);
|
||||||
|
// get redux publish form
|
||||||
|
const publishData = selectPublishFormValues(state);
|
||||||
|
|
||||||
// Only pass file on new uploads, not metadata only edits.
|
// destructure the data values
|
||||||
// The sdk will figure it out
|
const {
|
||||||
if (filePath) publishPayload.file_path = filePath;
|
name,
|
||||||
|
bid,
|
||||||
|
filePath,
|
||||||
|
description,
|
||||||
|
language,
|
||||||
|
releaseTimeEdited,
|
||||||
|
// license,
|
||||||
|
licenseUrl,
|
||||||
|
useLBRYUploader,
|
||||||
|
licenseType,
|
||||||
|
otherLicenseDescription,
|
||||||
|
thumbnail,
|
||||||
|
channel,
|
||||||
|
title,
|
||||||
|
contentIsFree,
|
||||||
|
fee,
|
||||||
|
tags,
|
||||||
|
// locations,
|
||||||
|
optimize,
|
||||||
|
} = publishData;
|
||||||
|
|
||||||
if (preview) {
|
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
||||||
publishPayload.preview = true;
|
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
||||||
publishPayload.optimize_file = false;
|
|
||||||
|
|
||||||
return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => {
|
let publishingLicense;
|
||||||
return preview(previewResponse);
|
switch (licenseType) {
|
||||||
}, fail);
|
case COPYRIGHT:
|
||||||
}
|
case OTHER:
|
||||||
|
publishingLicense = otherLicenseDescription;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
publishingLicense = licenseType;
|
||||||
|
}
|
||||||
|
|
||||||
return Lbry.publish(publishPayload).then((response: PublishResponse) => {
|
// get the claim id from the channel name, we will use that instead
|
||||||
return success(response);
|
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
||||||
}, fail);
|
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||||
|
|
||||||
|
const publishPayload: {
|
||||||
|
name: ?string,
|
||||||
|
bid: string,
|
||||||
|
description?: string,
|
||||||
|
channel_id?: string,
|
||||||
|
file_path?: string,
|
||||||
|
license_url?: string,
|
||||||
|
license?: string,
|
||||||
|
thumbnail_url?: string,
|
||||||
|
release_time?: number,
|
||||||
|
fee_currency?: string,
|
||||||
|
fee_amount?: string,
|
||||||
|
languages?: Array<string>,
|
||||||
|
tags: Array<string>,
|
||||||
|
locations?: Array<any>,
|
||||||
|
blocking: boolean,
|
||||||
|
optimize_file?: boolean,
|
||||||
|
preview?: boolean,
|
||||||
|
remote_url?: string,
|
||||||
|
} = {
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
locations: [],
|
||||||
|
bid: creditsToString(bid),
|
||||||
|
languages: [language],
|
||||||
|
tags: tags && tags.map((tag) => tag.name),
|
||||||
|
thumbnail_url: thumbnail,
|
||||||
|
blocking: true,
|
||||||
|
preview: false,
|
||||||
};
|
};
|
||||||
|
// Temporary solution to keep the same publish flow with the new tags api
|
||||||
|
// Eventually we will allow users to enter their own tags on publish
|
||||||
|
|
||||||
|
if (publishingLicense) {
|
||||||
|
publishPayload.license = publishingLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (licenseUrl) {
|
||||||
|
publishPayload.license_url = licenseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
publishPayload.thumbnail_url = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useLBRYUploader) {
|
||||||
|
publishPayload.tags.push('lbry-first');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
||||||
|
if (releaseTimeEdited) {
|
||||||
|
publishPayload.release_time = releaseTimeEdited;
|
||||||
|
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
||||||
|
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||||
|
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
||||||
|
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
||||||
|
} else {
|
||||||
|
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId) {
|
||||||
|
publishPayload.channel_id = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
||||||
|
publishPayload.locations = myClaimForUriEditing.value.locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
||||||
|
publishPayload.fee_currency = fee.currency;
|
||||||
|
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optimize) {
|
||||||
|
publishPayload.optimize_file = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pass file on new uploads, not metadata only edits.
|
||||||
|
// The sdk will figure it out
|
||||||
|
if (filePath) publishPayload.file_path = filePath;
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
publishPayload.preview = true;
|
||||||
|
publishPayload.optimize_file = false;
|
||||||
|
|
||||||
|
return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => {
|
||||||
|
return preview(previewResponse);
|
||||||
|
}, fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Lbry.publish(publishPayload).then((response: PublishResponse) => {
|
||||||
|
return success(response);
|
||||||
|
}, fail);
|
||||||
|
};
|
||||||
|
|
||||||
// Calls file_list until any reflecting files are done
|
// Calls file_list until any reflecting files are done
|
||||||
export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetState) => {
|
export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
|
|
@ -98,22 +98,24 @@ export function doSetSync(oldHash: string, newHash: string, data: any) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const doGetSyncDesktop =
|
export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => (
|
||||||
(cb?: (any, any) => void, password?: string) => (dispatch: Dispatch, getState: GetState) => {
|
dispatch: Dispatch,
|
||||||
const state = getState();
|
getState: GetState
|
||||||
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
) => {
|
||||||
const getSyncPending = selectGetSyncIsPending(state);
|
const state = getState();
|
||||||
const setSyncPending = selectSetSyncIsPending(state);
|
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
||||||
const syncLocked = selectSyncIsLocked(state);
|
const getSyncPending = selectGetSyncIsPending(state);
|
||||||
|
const setSyncPending = selectSetSyncIsPending(state);
|
||||||
|
const syncLocked = selectSyncIsLocked(state);
|
||||||
|
|
||||||
return getSavedPassword().then((savedPassword) => {
|
return getSavedPassword().then((savedPassword) => {
|
||||||
const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword;
|
const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword;
|
||||||
|
|
||||||
if (syncEnabled && !getSyncPending && !setSyncPending && !syncLocked) {
|
if (syncEnabled && !getSyncPending && !setSyncPending && !syncLocked) {
|
||||||
return dispatch(doGetSync(passwordArgument, cb));
|
return dispatch(doGetSync(passwordArgument, cb));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doSyncLoop(noInterval?: boolean) {
|
export function doSyncLoop(noInterval?: boolean) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
@ -383,6 +385,7 @@ type SharedData = {
|
||||||
following?: Array<{ uri: string, notificationsDisabled: boolean }>,
|
following?: Array<{ uri: string, notificationsDisabled: boolean }>,
|
||||||
tags?: Array<string>,
|
tags?: Array<string>,
|
||||||
blocked?: Array<string>,
|
blocked?: Array<string>,
|
||||||
|
coin_swap_codes?: Array<string>,
|
||||||
settings?: any,
|
settings?: any,
|
||||||
app_welcome_version?: number,
|
app_welcome_version?: number,
|
||||||
sharing_3P?: boolean,
|
sharing_3P?: boolean,
|
||||||
|
@ -400,6 +403,7 @@ function extractUserState(rawObj: SharedData) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
|
coin_swap_codes,
|
||||||
settings,
|
settings,
|
||||||
app_welcome_version,
|
app_welcome_version,
|
||||||
sharing_3P,
|
sharing_3P,
|
||||||
|
@ -414,6 +418,7 @@ function extractUserState(rawObj: SharedData) {
|
||||||
...(following ? { following } : {}),
|
...(following ? { following } : {}),
|
||||||
...(tags ? { tags } : {}),
|
...(tags ? { tags } : {}),
|
||||||
...(blocked ? { blocked } : {}),
|
...(blocked ? { blocked } : {}),
|
||||||
|
...(coin_swap_codes ? { coin_swap_codes } : {}),
|
||||||
...(settings ? { settings } : {}),
|
...(settings ? { settings } : {}),
|
||||||
...(app_welcome_version ? { app_welcome_version } : {}),
|
...(app_welcome_version ? { app_welcome_version } : {}),
|
||||||
...(sharing_3P ? { sharing_3P } : {}),
|
...(sharing_3P ? { sharing_3P } : {}),
|
||||||
|
@ -440,6 +445,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
|
coin_swap_codes,
|
||||||
settings,
|
settings,
|
||||||
app_welcome_version,
|
app_welcome_version,
|
||||||
sharing_3P,
|
sharing_3P,
|
||||||
|
@ -464,6 +470,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
|
coinSwapCodes: coin_swap_codes,
|
||||||
walletPrefSettings: settings,
|
walletPrefSettings: settings,
|
||||||
mergedClientSettings,
|
mergedClientSettings,
|
||||||
welcomeVersion: app_welcome_version,
|
welcomeVersion: app_welcome_version,
|
||||||
|
|
|
@ -82,6 +82,12 @@ export const doNotificationSocketConnect = (enableNotifications) => (dispatch) =
|
||||||
dispatch(doNotificationList());
|
dispatch(doNotificationList());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'swap-status':
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
|
||||||
|
data: data.data,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,6 @@ type State = {
|
||||||
createCollectionError: ?string,
|
createCollectionError: ?string,
|
||||||
channelClaimCounts: { [string]: number },
|
channelClaimCounts: { [string]: number },
|
||||||
claimsByUri: { [string]: string },
|
claimsByUri: { [string]: string },
|
||||||
blacklistedByUri: { [string]: ClaimErrorCensor },
|
|
||||||
byId: { [string]: Claim },
|
byId: { [string]: Claim },
|
||||||
pendingById: { [string]: Claim }, // keep pending claims
|
pendingById: { [string]: Claim }, // keep pending claims
|
||||||
resolvingUris: Array<string>,
|
resolvingUris: Array<string>,
|
||||||
|
@ -68,7 +67,6 @@ type State = {
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
blacklistedByUri: {},
|
|
||||||
byId: {},
|
byId: {},
|
||||||
claimsByUri: {},
|
claimsByUri: {},
|
||||||
paginatedClaimsByChannel: {},
|
paginatedClaimsByChannel: {},
|
||||||
|
@ -120,7 +118,6 @@ const defaultState = {
|
||||||
function handleClaimAction(state: State, action: any): State {
|
function handleClaimAction(state: State, action: any): State {
|
||||||
const { resolveInfo }: ClaimActionResolveInfo = action.data;
|
const { resolveInfo }: ClaimActionResolveInfo = action.data;
|
||||||
|
|
||||||
const blacklistedByUri = Object.assign({}, state.blacklistedByUri);
|
|
||||||
const byUri = Object.assign({}, state.claimsByUri);
|
const byUri = Object.assign({}, state.claimsByUri);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
|
const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
|
||||||
|
@ -130,11 +127,9 @@ function handleClaimAction(state: State, action: any): State {
|
||||||
|
|
||||||
Object.entries(resolveInfo).forEach(([url, resolveResponse]) => {
|
Object.entries(resolveInfo).forEach(([url, resolveResponse]) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const { claimsInChannel, stream, channel: channelFromResolve, collection, errorCensor } = resolveResponse;
|
const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse;
|
||||||
const channel = channelFromResolve || (stream && stream.signing_channel);
|
const channel = channelFromResolve || (stream && stream.signing_channel);
|
||||||
|
|
||||||
blacklistedByUri[url] = errorCensor;
|
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
if (pendingById[stream.claim_id]) {
|
if (pendingById[stream.claim_id]) {
|
||||||
byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]);
|
byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]);
|
||||||
|
@ -202,7 +197,6 @@ function handleClaimAction(state: State, action: any): State {
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
blacklistedByUri,
|
|
||||||
byId,
|
byId,
|
||||||
claimsByUri: byUri,
|
claimsByUri: byUri,
|
||||||
channelClaimCounts,
|
channelClaimCounts,
|
||||||
|
@ -524,8 +518,10 @@ reducers[ACTIONS.UPDATE_PENDING_CLAIMS] = (state: State, action: any): State =>
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.UPDATE_CONFIRMED_CLAIMS] = (state: State, action: any): State => {
|
reducers[ACTIONS.UPDATE_CONFIRMED_CLAIMS] = (state: State, action: any): State => {
|
||||||
const { claims: confirmedClaims, pending: pendingClaims }: { claims: Array<Claim>, pending: { [string]: Claim } } =
|
const {
|
||||||
action.data;
|
claims: confirmedClaims,
|
||||||
|
pending: pendingClaims,
|
||||||
|
}: { claims: Array<Claim>, pending: { [string]: Claim } } = action.data;
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
const byUri = Object.assign({}, state.claimsByUri);
|
const byUri = Object.assign({}, state.claimsByUri);
|
||||||
//
|
//
|
||||||
|
|
152
ui/redux/reducers/coinSwap.js
Normal file
152
ui/redux/reducers/coinSwap.js
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { handleActions } from 'util/redux-utils';
|
||||||
|
|
||||||
|
const SWAP_HISTORY_LENGTH_LIMIT = 10;
|
||||||
|
|
||||||
|
function getBottomEntries(array, count) {
|
||||||
|
const curCount = array.length;
|
||||||
|
if (curCount < count) {
|
||||||
|
return array;
|
||||||
|
} else {
|
||||||
|
return array.slice(curCount - count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultState: CoinSwapState = {
|
||||||
|
coinSwaps: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[ACTIONS.ADD_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapAddAction): CoinSwapState => {
|
||||||
|
const { coinSwaps } = state;
|
||||||
|
const { chargeCode } = action.data;
|
||||||
|
|
||||||
|
const newCoinSwaps = coinSwaps.slice();
|
||||||
|
if (!newCoinSwaps.find((x) => x.chargeCode === chargeCode)) {
|
||||||
|
newCoinSwaps.push({ ...action.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
coinSwaps: newCoinSwaps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.REMOVE_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapRemoveAction): CoinSwapState => {
|
||||||
|
const { coinSwaps } = state;
|
||||||
|
const { chargeCode } = action.data;
|
||||||
|
let newCoinSwaps = coinSwaps.slice();
|
||||||
|
newCoinSwaps = newCoinSwaps.filter((x) => x.chargeCode !== chargeCode);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
coinSwaps: newCoinSwaps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.COIN_SWAP_STATUS_RECEIVED]: (state: CoinSwapState, action: any) => {
|
||||||
|
const { coinSwaps } = state;
|
||||||
|
const newCoinSwaps = coinSwaps.slice();
|
||||||
|
|
||||||
|
let exchange;
|
||||||
|
let charge;
|
||||||
|
|
||||||
|
if (action.data.event_data) {
|
||||||
|
// Source: Websocket
|
||||||
|
exchange = { lbc_txid: action.data.lbc_txid };
|
||||||
|
charge = action.data.event_data;
|
||||||
|
} else {
|
||||||
|
// Source: btc/status
|
||||||
|
exchange = action.data.Exchange;
|
||||||
|
charge = action.data.Charge.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateLbcAmount = (pricing, exchange, fallback) => {
|
||||||
|
if (!exchange || !exchange.rate) {
|
||||||
|
return fallback || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btcAmount = pricing['bitcoin'].amount;
|
||||||
|
const SATOSHIS = 100000000;
|
||||||
|
return (btcAmount * SATOSHIS) / exchange.rate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeline = charge.timeline;
|
||||||
|
const lastTimeline = timeline[timeline.length - 1];
|
||||||
|
|
||||||
|
const index = newCoinSwaps.findIndex((x) => x.chargeCode === charge.code);
|
||||||
|
if (index > -1) {
|
||||||
|
newCoinSwaps[index] = {
|
||||||
|
chargeCode: charge.code,
|
||||||
|
coins: Object.keys(charge.addresses),
|
||||||
|
sendAddresses: charge.addresses,
|
||||||
|
sendAmounts: charge.pricing,
|
||||||
|
lbcAmount: calculateLbcAmount(charge.pricing, exchange, newCoinSwaps[index].lbcAmount),
|
||||||
|
status: {
|
||||||
|
status: lastTimeline.status,
|
||||||
|
receiptCurrency: lastTimeline.payment.value.currency,
|
||||||
|
receiptTxid: lastTimeline.payment.transaction_id,
|
||||||
|
lbcTxid: exchange.lbc_txid || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If a pending swap is removed, the websocket will return an update
|
||||||
|
// when it expires, for example, causing the entry to re-appear. This
|
||||||
|
// might be a good thing (e.g. to get back accidental removals), but it
|
||||||
|
// actually causes synchronization confusion across multiple instances.
|
||||||
|
const IGNORED_DELETED_SWAPS = true;
|
||||||
|
|
||||||
|
if (!IGNORED_DELETED_SWAPS) {
|
||||||
|
newCoinSwaps.push({
|
||||||
|
chargeCode: charge.code,
|
||||||
|
coins: Object.keys(charge.addresses),
|
||||||
|
sendAddresses: charge.addresses,
|
||||||
|
sendAmounts: charge.pricing,
|
||||||
|
lbcAmount: calculateLbcAmount(charge.pricing, exchange, 0),
|
||||||
|
status: {
|
||||||
|
status: lastTimeline.status,
|
||||||
|
receiptCurrency: lastTimeline.payment.value.currency,
|
||||||
|
receiptTxid: lastTimeline.payment.transaction_id,
|
||||||
|
lbcTxid: exchange.lbc_txid || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
coinSwaps: newCoinSwaps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.SYNC_STATE_POPULATE]: (state: CoinSwapState, action: { data: { coinSwapCodes: ?Array<string> } }) => {
|
||||||
|
const { coinSwapCodes } = action.data;
|
||||||
|
const newCoinSwaps = [];
|
||||||
|
|
||||||
|
if (coinSwapCodes) {
|
||||||
|
coinSwapCodes.forEach((chargeCode) => {
|
||||||
|
if (chargeCode && typeof chargeCode === 'string') {
|
||||||
|
const existingSwap = state.coinSwaps.find((x) => x.chargeCode === chargeCode);
|
||||||
|
if (existingSwap) {
|
||||||
|
newCoinSwaps.push({ ...existingSwap });
|
||||||
|
} else {
|
||||||
|
newCoinSwaps.push({
|
||||||
|
// Just restore the 'chargeCode', and query the other data
|
||||||
|
// via 'btc/status' later.
|
||||||
|
chargeCode: chargeCode,
|
||||||
|
coins: [],
|
||||||
|
sendAddresses: {},
|
||||||
|
sendAmounts: {},
|
||||||
|
lbcAmount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
coinSwaps: getBottomEntries(newCoinSwaps, SWAP_HISTORY_LENGTH_LIMIT),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultState
|
||||||
|
);
|
|
@ -4,7 +4,6 @@ import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { createCachedSelector } from 're-reselect';
|
import { createCachedSelector } from 're-reselect';
|
||||||
import { isClaimNsfw, filterClaims } from 'util/claim';
|
import { isClaimNsfw, filterClaims } from 'util/claim';
|
||||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
|
||||||
import * as CLAIM from 'constants/claim';
|
import * as CLAIM from 'constants/claim';
|
||||||
|
|
||||||
type State = { claims: any };
|
type State = { claims: any };
|
||||||
|
@ -84,46 +83,6 @@ export const selectClaimIdForUri = (state: State, uri: string) => selectClaimIds
|
||||||
|
|
||||||
export const selectReflectingById = (state: State) => selectState(state).reflectingById;
|
export const selectReflectingById = (state: State) => selectState(state).reflectingById;
|
||||||
|
|
||||||
export const makeSelectBlacklistedDueToDMCA = (claimUri: string) =>
|
|
||||||
createSelector(makeSelectClaimErrorCensor(claimUri), (claimError) => {
|
|
||||||
if (!claimError) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return claimError.name === '@LBRY-DMCA';
|
|
||||||
});
|
|
||||||
|
|
||||||
export const makeSelectClaimErrorCensor = (claimUri: string) =>
|
|
||||||
createSelector(selectState, (state) => state.blacklistedByUri[claimUri]);
|
|
||||||
|
|
||||||
export const makeSelectIsBlacklisted = (claimUri: string) =>
|
|
||||||
createSelector(
|
|
||||||
makeSelectClaimErrorCensor(claimUri),
|
|
||||||
selectBlackListedOutpoints,
|
|
||||||
makeSelectClaimForUri(claimUri),
|
|
||||||
(errorCensor, legacyBlacklistedList, claim) => {
|
|
||||||
if (errorCensor) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Fallback to legacy just in case.
|
|
||||||
if (!claim) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!legacyBlacklistedList) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const signingChannel = claim.signing_channel;
|
|
||||||
if (!signingChannel) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const isInLegacyBlacklist = legacyBlacklistedList.some(
|
|
||||||
(outpoint) =>
|
|
||||||
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
|
|
||||||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
|
|
||||||
);
|
|
||||||
return isInLegacyBlacklist;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]);
|
export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]);
|
||||||
|
|
||||||
export const selectClaimForUri = createCachedSelector(
|
export const selectClaimForUri = createCachedSelector(
|
||||||
|
|
8
ui/redux/selectors/coinSwap.js
Normal file
8
ui/redux/selectors/coinSwap.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const selectState = (state) => state.coinSwap || {};
|
||||||
|
|
||||||
|
export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
|
||||||
|
return state.coinSwaps;
|
||||||
|
});
|
|
@ -161,24 +161,18 @@ function filterFileInfos(fileInfos, query) {
|
||||||
export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) =>
|
export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) =>
|
||||||
createSelector(selectFileInfosDownloaded, (fileInfos) => {
|
createSelector(selectFileInfosDownloaded, (fileInfos) => {
|
||||||
const matchingFileInfos = filterFileInfos(fileInfos, query);
|
const matchingFileInfos = filterFileInfos(fileInfos, query);
|
||||||
if (!matchingFileInfos || !matchingFileInfos.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = (Number(page) - 1) * Number(PAGE_SIZE);
|
const start = (Number(page) - 1) * Number(PAGE_SIZE);
|
||||||
const end = Number(page) * Number(PAGE_SIZE);
|
const end = Number(page) * Number(PAGE_SIZE);
|
||||||
// Recently downloaded elements first.
|
|
||||||
const sortedMatchedFileInfos = matchingFileInfos.sort((a, b) => {
|
|
||||||
return b.added_on - a.added_on;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortedMatchedFileInfos.slice(start, end).map((fileInfo) =>
|
return matchingFileInfos && matchingFileInfos.length
|
||||||
buildURI({
|
? matchingFileInfos.slice(start, end).map((fileInfo) =>
|
||||||
streamName: fileInfo.claim_name,
|
buildURI({
|
||||||
channelName: fileInfo.channel_name,
|
streamName: fileInfo.claim_name,
|
||||||
channelClaimId: fileInfo.channel_claim_id,
|
channelName: fileInfo.channel_name,
|
||||||
})
|
channelClaimId: fileInfo.channel_claim_id,
|
||||||
);
|
})
|
||||||
|
)
|
||||||
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectSearchDownloadUrlsCount = (query) =>
|
export const makeSelectSearchDownloadUrlsCount = (query) =>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
selectClaimForUri,
|
selectClaimForUri,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { swapKeyAndValue } from 'util/swap-json';
|
import { swapKeyAndValue } from 'util/swap-json';
|
||||||
import { getChannelFromClaim, isChannelClaim } from 'util/claim';
|
import { getChannelFromClaim } from 'util/claim';
|
||||||
|
|
||||||
// Returns the entire subscriptions state
|
// Returns the entire subscriptions state
|
||||||
const selectState = (state) => state.subscriptions || {};
|
const selectState = (state) => state.subscriptions || {};
|
||||||
|
@ -114,18 +114,12 @@ export const makeSelectChannelInSubscriptions = (uri) =>
|
||||||
createSelector(selectSubscriptions, (subscriptions) => subscriptions.some((sub) => sub.uri === uri));
|
createSelector(selectSubscriptions, (subscriptions) => subscriptions.some((sub) => sub.uri === uri));
|
||||||
|
|
||||||
export const selectIsSubscribedForUri = createCachedSelector(
|
export const selectIsSubscribedForUri = createCachedSelector(
|
||||||
(state, uri) => uri,
|
|
||||||
selectClaimForUri,
|
selectClaimForUri,
|
||||||
selectSubscriptions,
|
selectSubscriptions,
|
||||||
(uri, claim, subscriptions) => {
|
(claim, subscriptions) => {
|
||||||
const channelClaim = getChannelFromClaim(claim);
|
const channelClaim = getChannelFromClaim(claim);
|
||||||
if (channelClaim) {
|
if (channelClaim) {
|
||||||
const permanentUrl = channelClaim.permanent_url;
|
const uri = channelClaim.permanent_url;
|
||||||
return subscriptions.some((sub) => isURIEqual(sub.uri, permanentUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it failed, it could be an abandoned channel. Try parseURI:
|
|
||||||
if (isChannelClaim(claim, uri)) {
|
|
||||||
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -167,7 +167,7 @@ a.button--alt {
|
||||||
.vjs-button--theater-mode.vjs-button {
|
.vjs-button--theater-mode.vjs-button {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
display: block;
|
display: block;
|
||||||
order: 1;
|
order: 1;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
margin: 0px var(--spacing-xxs);
|
margin: 0px var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button + .comment-create {
|
.button + .commentCreate {
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -615,7 +615,7 @@
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
border-bottom: none;
|
|
||||||
.button--link {
|
.button--link {
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
@ -659,7 +659,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button__selected-server {
|
.button_selectedServer {
|
||||||
display: inline;
|
display: inline;
|
||||||
float: right;
|
float: right;
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -621,7 +621,7 @@
|
||||||
@media (max-width: $breakpoint-xsmall) {
|
@media (max-width: $breakpoint-xsmall) {
|
||||||
-webkit-line-clamp: 2 !important;
|
-webkit-line-clamp: 2 !important;
|
||||||
}
|
}
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
-webkit-line-clamp: 1 !important;
|
-webkit-line-clamp: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create {
|
.commentCreate {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create--reply {
|
.commentCreate--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create--nestedReply {
|
.commentCreate--nestedReply {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||||
|
|
||||||
|
@ -149,40 +149,27 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create--bottom {
|
.commentCreate--bottom {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
.comment-create__header-button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--alt {
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
height: unset;
|
|
||||||
margin-bottom: var(--spacing-xxs);
|
|
||||||
background: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-create__label-wrapper {
|
.comment-create__label-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
max-width: 50%;
|
width: 100%;
|
||||||
|
|
||||||
.comment-create__label {
|
.comment-create__label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset-section {
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
font-size: var(--font-xxsmall);
|
font-size: var(--font-xxsmall);
|
||||||
|
@ -192,14 +179,14 @@ $thumbnailWidthSmall: 1rem;
|
||||||
font-size: var(--font-xxsmall);
|
font-size: var(--font-xxsmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
//select {
|
select {
|
||||||
// height: 1rem;
|
height: 1rem;
|
||||||
// margin: var(--spacing-xxs) 0px;
|
margin: var(--spacing-xxs) 0px;
|
||||||
//}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create__supportCommentPreview {
|
.commentCreate__supportCommentPreview {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
@ -207,7 +194,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
|
|
||||||
.comment-create__supportCommentPreviewAmount {
|
.commentCreate__supportCommentPreviewAmount {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
font-size: var(--font-large);
|
font-size: var(--font-large);
|
||||||
}
|
}
|
||||||
|
@ -236,8 +223,8 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create__stickerPreview {
|
.commentCreate__stickerPreview {
|
||||||
@extend .comment-create;
|
@extend .commentCreate;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--color-header-background);
|
background-color: var(--color-header-background);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
@ -247,12 +234,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
|
|
||||||
.comment-create__stickerPreviewInfo {
|
.commentCreate__stickerPreviewInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create__stickerPreviewImage {
|
.commentCreate__stickerPreviewImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
|
|
|
@ -45,12 +45,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__actions-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -507,7 +501,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
min-width: 40%;
|
min-width: 40%;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
}
|
}
|
||||||
|
@ -549,7 +543,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
margin: 0 var(--spacing-xs);
|
margin: 0 var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,7 +558,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
padding-left: var(--spacing-m);
|
padding-left: var(--spacing-m);
|
||||||
border-left: 4px solid var(--color-border);
|
border-left: 4px solid var(--color-border);
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,6 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--spacing-s);
|
top: var(--spacing-s);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__viewer--disable-click {
|
.content__viewer--disable-click {
|
||||||
|
@ -179,10 +175,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__cover--text {
|
.content__cover--text {
|
||||||
|
|
|
@ -29,12 +29,7 @@ select,
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textarea {
|
|
||||||
height: var(--height-input);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-input);
|
|
||||||
background-color: var(--color-input-bg);
|
|
||||||
}
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
textarea {
|
textarea {
|
||||||
height: var(--height-input);
|
height: var(--height-input);
|
||||||
|
@ -537,7 +532,6 @@ fieldset-group {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-action {
|
.form-field__quick-action {
|
||||||
text-align: right;
|
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--pusher--open {
|
.sidebar--pusher--open {
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9);
|
transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
|
@ -461,6 +461,7 @@ body {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
width: 100vw;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
|
@ -803,7 +804,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
> :first-child {
|
> :first-child {
|
||||||
width: calc(30% - var(--spacing-l));
|
width: calc(30% - var(--spacing-l));
|
||||||
max-width: 25rem;
|
max-width: 25rem;
|
||||||
|
|
|
@ -114,15 +114,10 @@
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
border: transparent;
|
border: transparent;
|
||||||
margin-right: var(--spacing-xxs);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-header-link);
|
color: var(--color-header-link-active);
|
||||||
background-color: var(--color-editor-button-hover-bg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button.active {
|
|
||||||
background: var(--color-editor-button-active-bg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
color: var(--color-brand-contrast) !important;
|
color: var(--color-brand-contrast) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@
|
||||||
@extend .navigation-link--highlighted;
|
@extend .navigation-link--highlighted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (max-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
|
|
@ -32,17 +32,13 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create {
|
.commentCreate {
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
|
|
||||||
.comment-create__label {
|
.commentCreate__label {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
.comment-create__header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 3fr 1fr;
|
|
||||||
}
|
|
||||||
textarea,
|
textarea,
|
||||||
select,
|
select,
|
||||||
.button:not(.button--file-action) {
|
.button:not(.button--file-action) {
|
||||||
|
@ -85,7 +81,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-create,
|
.commentCreate,
|
||||||
.comment__content {
|
.comment__content {
|
||||||
margin: var(--spacing-m);
|
margin: var(--spacing-m);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue