Compare commits
56 commits
Comment-se
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
d14c9141db | ||
|
06c350c4db | ||
|
c3a9d9d002 | ||
|
aeada6dc74 | ||
|
6ba985fd28 | ||
|
2a0bc85738 | ||
|
523ea284a2 | ||
|
a66d7534c2 | ||
|
89ec07622f | ||
|
7e6ad31392 | ||
|
4ab23f03fc | ||
|
29cea5cc07 | ||
|
8dd7150d67 | ||
|
f1b1523017 | ||
|
88ac250fee | ||
|
0a5e9e87ed | ||
|
20413d79b6 | ||
|
ff9011e6ac | ||
|
802139d0a4 | ||
|
68d307fa50 | ||
|
d3900e39b6 | ||
|
35769dede6 | ||
|
ae1e20d131 | ||
|
051af8b6ad | ||
|
5d77b115f9 | ||
|
7dbeeac112 | ||
|
de062c4aee | ||
|
18a3336714 | ||
|
ebf35a1df8 | ||
|
28e168d5e5 | ||
|
7ad66b99e7 | ||
|
7eb7c1a5ff | ||
|
b88e704e6b | ||
|
8d85af8064 | ||
|
f9d7340729 | ||
|
d57300f785 | ||
|
09baf1d9b9 | ||
|
ce692d38ea | ||
|
b1ca3b0183 | ||
|
a4c34d89e2 | ||
|
d7b9ca3391 | ||
|
55a5c7b051 | ||
|
0e2a9a1033 | ||
|
3fd38be789 | ||
|
329d434c83 | ||
|
6a2939d9fc | ||
|
65781e33f7 | ||
|
608721c7ac | ||
|
2846dd926b | ||
|
7a8a16cd9c | ||
|
1c17ff5dd9 | ||
|
b9be8d9f3a | ||
|
6b1069f02a | ||
|
b92fb03856 | ||
|
1a9743e639 | ||
|
2773cbbe6e |
109 changed files with 1805 additions and 2890 deletions
.github/workflows
.yarn/versions
CHANGELOG.mdREADME.mdbuild
electron
extras/lbryinc/redux/selectors
flow-typed
package.jsonstatic
ui
analytics.js
component
button
channelForm
collectionEdit
comment
commentCreate
commentsList
common
fileDrop
fileRenderFloating
fileRenderInitiator
fileThumbnail
page
postEditor
publishDescription
publishFile
publishForm
publishFormErrors
searchChannelField
selectAsset
selectThumbnail
settingSystem
supportsLiquidate
syncToggle
txoList
walletSwap
constants
index.jsxlbry.jsmodal
modalAutoGenerateThumbnail
modalConfirmThumbnailUpload
modalFileSelection
modalImageUpload
modalPublishPreview
modalRemoveBtcSwapAddress
modalRouter
page
reducers.jsredux
actions
reducers
selectors
scss/component
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,7 +38,22 @@ 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: '12.4.0'
|
xcode-version: '13.1.0'
|
||||||
|
# This is gonna be hacky.
|
||||||
|
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
||||||
|
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
||||||
|
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
||||||
|
# So for now we'll install python2 on mac and make sure it can find it.
|
||||||
|
# Remove this after successfully upgrading electron-builder.
|
||||||
|
# HACK part 1
|
||||||
|
- uses: Homebrew/actions/setup-homebrew@master
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
# HACK part 2
|
||||||
|
- name: Install Python2
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
run: |
|
||||||
|
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
||||||
|
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Download blockchain headers
|
- name: Download blockchain headers
|
||||||
run: |
|
run: |
|
||||||
|
@ -58,7 +73,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/cert-2021-2022.pfx
|
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.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
Normal file
0
.yarn/versions/5bc94294.yml
vendored
Normal file
0
.yarn/versions/5f1212ad.yml
vendored
Normal file
0
.yarn/versions/5f1212ad.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/8e384637.yml
vendored
Normal file
0
.yarn/versions/8e384637.yml
vendored
Normal file
0
.yarn/versions/c6e2b914.yml
vendored
Normal file
0
.yarn/versions/c6e2b914.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -1,22 +1,73 @@
|
||||||
# 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-app-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-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||||
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@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/) (v14 required)
|
- [Node.js](https://nodejs.org/en/download/) (v16 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)
|
||||||
|
|
||||||
|
|
BIN
build/cert2023.pfx
Normal file
BIN
build/cert2023.pfx
Normal file
Binary file not shown.
|
@ -20,9 +20,12 @@ 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');
|
||||||
|
@ -299,6 +302,96 @@ 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 } from 'redux/selectors/claims';
|
import { selectClaimForUri, makeSelectIsBlacklisted } 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,7 +18,8 @@ export const selectBanStateForUri = createCachedSelector(
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
selectMutedChannels,
|
selectMutedChannels,
|
||||||
selectModerationBlockList,
|
selectModerationBlockList,
|
||||||
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist) => {
|
(state, uri) => makeSelectIsBlacklisted(uri)(state),
|
||||||
|
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist, isBlacklisted) => {
|
||||||
const banState = {};
|
const banState = {};
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
|
@ -27,6 +28,10 @@ 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,12 +145,49 @@ 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
Normal file
10
flow-typed/file-data.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type FileData = {
|
||||||
|
file?: Blob,
|
||||||
|
path: string,
|
||||||
|
duration?: number,
|
||||||
|
size?: number,
|
||||||
|
mimeType: string,
|
||||||
|
error?: string,
|
||||||
|
}
|
9
flow-typed/file-with-path.js
vendored
Normal file
9
flow-typed/file-with-path.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// @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
6
flow-typed/web-file.js
vendored
|
@ -1,6 +0,0 @@
|
||||||
// @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.4",
|
"version": "0.53.9",
|
||||||
"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,32 +41,29 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.1",
|
"@electron/remote": "^2.0.1",
|
||||||
"@emotion/react": "^11.6.0",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.6.0",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@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": "^2.2.12",
|
"electron-log": "^4.4.8",
|
||||||
"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",
|
||||||
|
@ -77,7 +74,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.2.5",
|
"@babel/polyfill": "^7.12.1",
|
||||||
"@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",
|
||||||
|
@ -85,7 +82,6 @@
|
||||||
"@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",
|
||||||
|
@ -96,21 +92,17 @@
|
||||||
"@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",
|
||||||
|
@ -121,10 +113,9 @@
|
||||||
"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": "15.5.5",
|
"electron": "17.2.0",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
|
@ -159,10 +150,7 @@
|
||||||
"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",
|
||||||
|
@ -177,7 +165,6 @@
|
||||||
"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",
|
||||||
|
@ -188,7 +175,6 @@
|
||||||
"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",
|
||||||
|
@ -205,20 +191,16 @@
|
||||||
"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",
|
||||||
|
@ -228,15 +210,14 @@
|
||||||
"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": ">=7",
|
"node": ">=16.13",
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.107.2",
|
"lbrynetDaemonVersion": "0.113.0",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -2316,5 +2316,11 @@
|
||||||
"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,4 +1,11 @@
|
||||||
// @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';
|
||||||
|
@ -14,9 +21,6 @@ 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',
|
||||||
|
@ -68,114 +72,10 @@ 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) => {
|
||||||
amountOfBufferEvents = amountOfBufferEvents + 1;
|
// stub
|
||||||
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
|
||||||
|
@ -183,40 +83,9 @@ const analytics: Analytics = {
|
||||||
* @param {object} passedPlayer - VideoJS Player object
|
* @param {object} passedPlayer - VideoJS Player object
|
||||||
*/
|
*/
|
||||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||||
let playerIsSeeking = false;
|
// stub
|
||||||
// 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);
|
||||||
},
|
},
|
||||||
|
@ -382,24 +251,9 @@ 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,7 +5,6 @@ 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 = {
|
||||||
|
@ -34,7 +33,6 @@ 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,
|
||||||
|
@ -66,7 +64,6 @@ 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,
|
||||||
|
@ -75,7 +72,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const disable = disabled || (user === null && requiresAuth);
|
const disable = disabled;
|
||||||
|
|
||||||
const combinedClassName = classnames(
|
const combinedClassName = classnames(
|
||||||
'button',
|
'button',
|
||||||
|
@ -183,31 +180,6 @@ 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 } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } 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,7 +325,6 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
@ -377,7 +376,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}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
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 } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } 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>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
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 { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, 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}>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
className="comment__edit-input"
|
className="comment__edit-input"
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
|
|
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// @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 { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, 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,31 +363,6 @@ 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
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -409,7 +384,11 @@ export function CommentCreate(props: Props) {
|
||||||
push(pathPlusRedirect);
|
push(pathPlusRedirect);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
<FormFieldAreaAdvanced
|
||||||
|
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>
|
||||||
|
@ -420,22 +399,22 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
className={classnames('commentCreate', {
|
className={classnames('comment-create', {
|
||||||
'commentCreate--reply': isReply,
|
'comment-create--reply': isReply,
|
||||||
'commentCreate--nestedReply': isNested,
|
'comment-create--nestedReply': isNested,
|
||||||
'commentCreate--bottom': bottom,
|
'comment-create--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="commentCreate__stickerPreview">
|
<div className="comment-create__stickerPreview">
|
||||||
<div className="commentCreate__stickerPreviewInfo">
|
<div className="comment-create__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="commentCreate__stickerPreviewImage">
|
<div className="comment-create__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 */}
|
||||||
|
@ -447,15 +426,15 @@ export function CommentCreate(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
<div className="commentCreate__supportCommentPreview">
|
<div className="comment-create__supportCommentPreview">
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
className="commentCreate__supportCommentPreviewAmount"
|
className="comment-create__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="commentCreate__supportCommentBody">
|
<div className="comment-create__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -470,23 +449,22 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
label={
|
header={
|
||||||
<div className="commentCreate__labelWrapper">
|
<CommentCreateHeader
|
||||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
isReply={isReply}
|
||||||
<SelectChannel tiny />
|
advanced={advancedEditor}
|
||||||
</div>
|
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
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...')}
|
||||||
|
@ -654,7 +632,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 commentCreate__minAmountNotice">
|
<div className="help--notice comment-create__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 && (
|
||||||
<span className="comment__sort">
|
<div className="comment__sort-group">
|
||||||
<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,17 +377,16 @@ 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} />
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="button_refresh">
|
|
||||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{allServers.length >= 2 && (
|
{allServers.length >= 2 && (
|
||||||
<div className="button_selectedServer">
|
<div className="button__selected-server">
|
||||||
<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);
|
||||||
|
@ -407,7 +406,10 @@ 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<WebFile>,
|
files: Array<File>,
|
||||||
onChange: (WebFile | void) => void,
|
onChange: (File | 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: WebFile) => file.name === value);
|
return files.find((file: File) => file.name === value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (radio.stops.length) {
|
if (radio.items.length) {
|
||||||
if (!radio.currentId) {
|
if (!radio.currentId) {
|
||||||
radio.first();
|
radio.first();
|
||||||
} else {
|
} else {
|
||||||
const first = radio.stops[0].ref.current;
|
const first = radio.items[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.stops.find(item => item.id === radio.currentId);
|
const stop = radio.items.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);
|
||||||
// Sselect new file and update state
|
// Select new file and update state
|
||||||
onChange(file);
|
onChange(file);
|
||||||
radio.setState(element.value);
|
radio.setState(element.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
// @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: (WebFile) => void,
|
onFileChosen: (FileWithPath) => 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>;
|
||||||
|
@ -41,7 +45,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);
|
this.props.onFileChosen({ file, path: file.path || file.name });
|
||||||
}
|
}
|
||||||
this.fileInput.current.value = null; // clear the file input
|
this.fileInput.current.value = null; // clear the file input
|
||||||
};
|
};
|
||||||
|
@ -64,12 +68,26 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
properties = ['openDirectory'];
|
properties = ['openDirectory'];
|
||||||
}
|
}
|
||||||
|
|
||||||
remote.dialog.showOpenDialog({ properties, defaultPath }).then((result) => {
|
remote.dialog
|
||||||
|
.showOpenDialog({
|
||||||
|
properties,
|
||||||
|
defaultPath,
|
||||||
|
filters: this.props.filters,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
const path = result && result.filePaths[0];
|
const path = result && result.filePaths[0];
|
||||||
if (path) {
|
if (path) {
|
||||||
// $FlowFixMe
|
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile);
|
||||||
this.props.onFileChosen({ path });
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = new File([result.buffer], result.name, {
|
||||||
|
type: result.mime,
|
||||||
|
});
|
||||||
|
this.props.onFileChosen({ file, path: result.path });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// @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,14 +1,7 @@
|
||||||
// @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 = {
|
||||||
|
@ -21,19 +14,15 @@ 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,
|
||||||
|
@ -41,8 +30,6 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,21 +59,15 @@ 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;
|
||||||
|
@ -101,18 +82,10 @@ 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} />
|
||||||
|
@ -143,102 +116,14 @@ 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 || quickAction) && (
|
{label && (
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hideSuggestions ? (
|
|
||||||
<textarea
|
<textarea
|
||||||
type={type}
|
type={type}
|
||||||
id={name}
|
id={name}
|
||||||
|
@ -246,30 +131,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
ref={this.input}
|
ref={this.input}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
) : (
|
<div className="form-field__textarea-info">{countInfo}</div>
|
||||||
<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,4 +1,5 @@
|
||||||
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,4 +2054,15 @@ 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 | WebFile,
|
filePath: ?string,
|
||||||
clearPublish: () => void,
|
clearPublish: () => void,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
openModal: (id: string, { files: Array<WebFile> }) => void,
|
openModal: (id: string, { files: Array<File> }) => 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<?WebFile>(null);
|
const [target, setTarget] = React.useState<?File>(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,24 +65,26 @@ function FileDrop(props: Props) {
|
||||||
}
|
}
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
|
// Handle file selection
|
||||||
|
const handleFileSelected = React.useCallback(
|
||||||
|
(selectedFile) => {
|
||||||
// Delay hide and navigation for a smooth transition
|
// Delay hide and navigation for a smooth transition
|
||||||
const hideDropArea = React.useCallback(() => {
|
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
// Navigate to publish area
|
// Navigate to publish area
|
||||||
navigationTimer.current = setTimeout(() => {
|
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();
|
navigateToPublish();
|
||||||
|
updatePublishForm({
|
||||||
|
filePath: selectedFile.path || selectedFile.name,
|
||||||
|
});
|
||||||
}, NAVIGATE_TIME_OUT);
|
}, NAVIGATE_TIME_OUT);
|
||||||
}, HIDE_TIME_OUT);
|
}, HIDE_TIME_OUT);
|
||||||
}, [navigateToPublish]);
|
|
||||||
|
|
||||||
// Handle file selection
|
|
||||||
const handleFileSelected = React.useCallback(
|
|
||||||
(selectedFile) => {
|
|
||||||
updatePublishForm({ filePath: selectedFile });
|
|
||||||
hideDropArea();
|
|
||||||
},
|
},
|
||||||
[updatePublishForm, hideDropArea]
|
[setFiles, navigateToPublish, updatePublishForm]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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 } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } 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,7 +21,6 @@ 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;
|
||||||
|
@ -133,6 +132,7 @@ 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,7 +344,8 @@ 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': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
'content__viewer--theater-mode':
|
||||||
|
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
@ -356,7 +357,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
top:
|
top:
|
||||||
fileViewerRect.windowOffset +
|
fileViewerRect.windowOffset +
|
||||||
fileViewerRect.top -
|
fileViewerRect.top -
|
||||||
(isMobile ? HEADER_HEIGHT_MOBILE : HEADER_HEIGHT) -
|
(isMobile ? 0 : HEADER_HEIGHT) -
|
||||||
(IS_DESKTOP_MAC ? 24 : 0),
|
(IS_DESKTOP_MAC ? 24 : 0),
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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';
|
||||||
|
@ -63,6 +64,7 @@ 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>();
|
||||||
|
|
||||||
|
@ -151,7 +153,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,
|
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
||||||
'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_id === claimBeingPlayed.claim_id;
|
isBeingPlayed = claim && 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,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!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 && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
label: ?string,
|
label: ?string,
|
||||||
disabled: ?boolean,
|
disabled: ?boolean,
|
||||||
filePath: string | WebFile,
|
filePath: File,
|
||||||
fileText: ?string,
|
fileText: ?string,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
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 { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } 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={
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
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';
|
||||||
|
@ -13,13 +14,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 | WebFile,
|
filePath: ?string,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -77,7 +78,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, setCurrentFile] = useState(null);
|
const currentFile = filePath;
|
||||||
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);
|
||||||
|
@ -91,17 +92,35 @@ 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 || filePath === '') {
|
if (!filePath) {
|
||||||
setCurrentFile('');
|
return;
|
||||||
updateFileInfo(0, 0, false);
|
|
||||||
} else if (typeof filePath !== 'string') {
|
|
||||||
// Update currentFile file
|
|
||||||
if (filePath.name !== currentFile && filePath.path !== currentFile) {
|
|
||||||
handleFileChange(filePath);
|
|
||||||
}
|
}
|
||||||
|
async function readSelectedFileDetails() {
|
||||||
|
// Read the file to get the file's duration (if possible)
|
||||||
|
// and offer transcoding it.
|
||||||
|
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
|
||||||
|
let file;
|
||||||
|
if (result.buffer) {
|
||||||
|
file = new File([result.buffer], result.name, {
|
||||||
|
type: result.mime,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [filePath, currentFile, handleFileChange, updateFileInfo]);
|
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]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
||||||
|
@ -209,11 +228,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(file: WebFile, clearName = true) {
|
function processSelectedFile(fileData: FileData, 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 (!file) {
|
if (!fileData || fileData.error) {
|
||||||
if (isStillEditing || !clearName) {
|
if (isStillEditing || !clearName) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,8 +241,12 @@ function PublishFile(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if video, extract duration so we can warn about bitrateif (typeof file !== 'string') {
|
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
||||||
const contentType = file.type && file.type.split('/');
|
const file = fileData.file;
|
||||||
|
// Check to see if it's a video and if mp4
|
||||||
|
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
|
||||||
|
const duration = fileData.duration;
|
||||||
|
const size = fileData.size;
|
||||||
const isVideo = contentType && contentType[0] === 'video';
|
const isVideo = contentType && contentType[0] === 'video';
|
||||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||||
|
|
||||||
|
@ -231,34 +254,25 @@ 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);
|
setCurrentFileType(contentType.join('/'));
|
||||||
} else if (file.name) {
|
} else if (path.parse(fileData.path).ext) {
|
||||||
// If user's machine is missign a valid content type registration
|
// If user's machine is missing a valid content type registration
|
||||||
// for markdown content: text/markdown, file extension will be used instead
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
const extension = file.name.split('.').pop();
|
const extension = path.parse(fileData.path).ext;
|
||||||
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
const video = document.createElement('video');
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
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(0, file.size, isVideo);
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(0, size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextPost) {
|
if (isTextPost && file) {
|
||||||
// Create reader
|
// Create reader
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// Handler for file reader
|
// Handler for file reader
|
||||||
|
@ -270,21 +284,17 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode(PUBLISH_MODES.FILE);
|
setPublishMode(PUBLISH_MODES.FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
// Strip off extension and replace invalid characters
|
||||||
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
|
||||||
// File.path will be undefined from web due to browser security, so it will default to the File Object.
|
|
||||||
filePath: file.path || file,
|
|
||||||
};
|
|
||||||
// Strip off extention and replace invalid characters
|
|
||||||
let fileName = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
|
|
||||||
|
|
||||||
if (!isStillEditing) {
|
if (!isStillEditing) {
|
||||||
publishFormParams.name = parseName(fileName);
|
const fileWithoutExtension = path.parse(fileData.path).name;
|
||||||
|
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File path is not supported on web for security reasons so we use the name instead.
|
function handleFileChange(fileWithPath: FileWithPath) {
|
||||||
setCurrentFile(file.path || file.name);
|
if (fileWithPath) {
|
||||||
updatePublishForm(publishFormParams);
|
updatePublishForm({ filePath: fileWithPath.path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
||||||
|
@ -332,6 +342,7 @@ 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?: string | File, ?boolean) => void,
|
publish: (source: ?File, ?boolean) => void,
|
||||||
filePath: string | File,
|
filePath: ?File,
|
||||||
fileText: string,
|
fileText: string,
|
||||||
bid: ?number,
|
bid: ?number,
|
||||||
bidError: ?string,
|
bidError: ?string,
|
||||||
|
@ -208,7 +208,6 @@ function PublishForm(props: Props) {
|
||||||
isNameValid(name) &&
|
isNameValid(name) &&
|
||||||
title &&
|
title &&
|
||||||
bid &&
|
bid &&
|
||||||
thumbnail &&
|
|
||||||
!bidError &&
|
!bidError &&
|
||||||
!emptyPostError &&
|
!emptyPostError &&
|
||||||
!(thumbnailError && !thumbnailUploaded) &&
|
!(thumbnailError && !thumbnailUploaded) &&
|
||||||
|
@ -373,9 +372,6 @@ 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 !== '') {
|
||||||
|
@ -447,7 +443,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: '', fileDur: 0, fileSize: 0 });
|
updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 });
|
||||||
}
|
}
|
||||||
}, [mode, updatePublishForm]);
|
}, [mode, updatePublishForm]);
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,7 @@ 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>}
|
||||||
{!isUploadingThumbnail && !thumbnail ? (
|
{thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>}
|
||||||
<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,7 +89,6 @@ 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,6 +27,14 @@ 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) {
|
||||||
|
@ -43,7 +51,8 @@ function filePreview(file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectAsset(props: Props) {
|
function SelectAsset(props: Props) {
|
||||||
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview } = props;
|
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview, filters, type } =
|
||||||
|
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);
|
||||||
|
@ -121,17 +130,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={(file) => {
|
onFileChosen={(fileWithPath) => {
|
||||||
if (file.name) {
|
if (fileWithPath.file.name) {
|
||||||
setFileSelected(file);
|
setFileSelected(fileWithPath.file);
|
||||||
// what why? why not target=WEB this?
|
setPathSelected(fileWithPath.path);
|
||||||
// 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}
|
src={thumbnail || ThumbnailMissingImage}
|
||||||
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={(file) =>
|
onFileChosen={(fileWithPath) =>
|
||||||
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
|
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
|
||||||
file,
|
file: fileWithPath,
|
||||||
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: WebFile) => {
|
onFileChosen={(newDirectory: FileWithPath) => {
|
||||||
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: WebFile) => {
|
onFileChosen={(newDirectory: FileWithPath) => {
|
||||||
// $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, { useEffect, useState } from 'react';
|
import React, { useCallback, 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,67 +24,100 @@ 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 [amount, setAmount] = useState(-1);
|
const [defaultValueAssigned, setDefaultValueAssigned] = useState(false);
|
||||||
|
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) - Number(amount))).toFixed(8)
|
? Boolean(previewBalance) && Number.parseFloat(String(Number(previewBalance) - amount)).toFixed(8)
|
||||||
: Boolean(previewBalance) && Number.parseFloat(String((Number(previewBalance) / 4) * 3)).toFixed(8); // default unlock 25%
|
: Boolean(previewBalance) && Number.parseFloat(String(defaultValue * 3)).toFixed(8);
|
||||||
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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(a) {
|
const handleRangeChange = useCallback(
|
||||||
if (a === undefined || isNaN(Number(a))) {
|
(newValue) => {
|
||||||
|
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);
|
||||||
setAmount(0);
|
} else if (unlockAmount > previewBalanceNumber) {
|
||||||
} 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(false);
|
|
||||||
} else if (Number(a) === Number(previewBalance)) {
|
|
||||||
setMessage(__(`She's about to close up the library!`));
|
|
||||||
setAmount(a);
|
|
||||||
setError(false);
|
|
||||||
} else if (Number(a) > Number(previewBalance) / 2) {
|
|
||||||
setMessage(__('Your content will do better with more staked on it'));
|
|
||||||
setAmount(a);
|
|
||||||
setError(false);
|
|
||||||
} else if (Number(a) === 0) {
|
|
||||||
setMessage(__('Amount cannot be zero'));
|
|
||||||
setAmount(0);
|
|
||||||
setError(true);
|
setError(true);
|
||||||
} else if (Number(a) <= Number(previewBalance) / 2) {
|
} else if (Math.abs(unlockAmount - previewBalanceNumber) <= Number.EPSILON) {
|
||||||
|
setMessage(__(`She's about to close up the library!`));
|
||||||
|
setError(false);
|
||||||
|
} else if (unlockAmount > previewBalanceNumber / 2) {
|
||||||
|
setMessage(__('Your content will do better with more staked on it'));
|
||||||
|
setError(false);
|
||||||
|
} else if (unlockAmount === 0) {
|
||||||
|
setMessage(__('Amount cannot be zero'));
|
||||||
|
setError(true);
|
||||||
|
} else if (unlockAmount <= previewBalanceNumber / 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
|
||||||
|
@ -140,8 +173,8 @@ const SupportsLiquidate = (props: Props) => {
|
||||||
min={0}
|
min={0}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
max={previewBalance}
|
max={previewBalance}
|
||||||
value={Number(amount) >= 0 ? amount : previewBalance / 4} // by default, set it to 25% of available
|
value={amount}
|
||||||
onChange={e => handleChange(e.target.value)}
|
onChange={(e) => handleRangeChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label className="range__label">
|
<label className="range__label">
|
||||||
<span>0</span>
|
<span>0</span>
|
||||||
|
@ -150,9 +183,11 @@ const SupportsLiquidate = (props: Props) => {
|
||||||
</label>
|
</label>
|
||||||
<FormField
|
<FormField
|
||||||
type="text"
|
type="text"
|
||||||
value={amount >= 0 ? amount || '' : previewBalance && previewBalance / 4}
|
value={unlockTextAmount}
|
||||||
helper={message}
|
helper={message}
|
||||||
onChange={e => handleChange(e.target.value)}
|
onFocus={handleUnlockTextFocus}
|
||||||
|
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 requiresAuth button="primary" label={__('Add Email')} />
|
<Button button="primary" label={__('Add Email')} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
|
@ -103,6 +103,8 @@ 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) {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
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));
|
|
|
@ -1,707 +0,0 @@
|
||||||
// @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,11 +431,6 @@ 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,3 +186,5 @@ 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,7 +40,6 @@ 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,7 +51,6 @@ 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,7 +70,6 @@ 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,7 +11,8 @@ 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 DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST];
|
export const COLLECTION = 'collection';
|
||||||
|
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) {
|
if (fileName && fileName.split('.').length > 1) {
|
||||||
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: WebFile => void,
|
upload: (File) => 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: (file, cb) => dispatch(doUploadThumbnail(null, file, null, null, file.path, cb)),
|
upload: (fileWithPath, cb) => dispatch(doUploadThumbnail(null, fileWithPath.file, null, null, fileWithPath.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: WebFile,
|
file: FileWithPath,
|
||||||
upload: (WebFile, (string) => void) => void,
|
upload: (FileWithPath, (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 || file.name);
|
const filePath = file && file.path;
|
||||||
|
|
||||||
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<WebFile>,
|
files: Array<File>,
|
||||||
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?: WebFile) => {
|
const handleFileChange = (file?: File) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,13 @@ 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: string | WebFile,
|
filePath: ?File,
|
||||||
isMarkdownPost: boolean,
|
isMarkdownPost: boolean,
|
||||||
optimize: boolean,
|
optimize: boolean,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
|
@ -104,17 +104,12 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
// @endif
|
// @endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilePathName(filePath: string | WebFile) {
|
function getFilePathName(filePath: ?File) {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return NO_FILE;
|
return NO_FILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof filePath === 'string') {
|
|
||||||
return filePath;
|
|
||||||
} else {
|
|
||||||
return filePath.name;
|
return filePath.name;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function createRow(label: string, value: any) {
|
function createRow(label: string, value: any) {
|
||||||
return (
|
return (
|
||||||
|
@ -127,7 +122,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 && filePath !== '' && isVid && ffmpegStatus.available;
|
const isOptimizeAvail = filePath && isVid && ffmpegStatus.available;
|
||||||
let modalTitle;
|
let modalTitle;
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
modalTitle = __('Confirm Edit');
|
modalTitle = __('Confirm Edit');
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
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);
|
|
|
@ -1,43 +0,0 @@
|
||||||
// @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,8 +41,6 @@ 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';
|
||||||
|
|
||||||
|
@ -137,8 +135,6 @@ 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,6 +1,7 @@
|
||||||
// @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';
|
||||||
|
@ -59,6 +60,7 @@ 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 =
|
||||||
|
@ -169,8 +171,10 @@ 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 && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
<CollectionContent id={collectionId} uri={uri} />
|
||||||
|
)}
|
||||||
|
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -187,13 +191,17 @@ 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 && <RecommendedContent uri={uri} />}
|
{!collection && !isMarkdown && videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && videoTheaterMode && !isMediumScreen && (
|
||||||
|
<CollectionContent id={collectionId} uri={uri} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
<CollectionContent id={collectionId} 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,39 +313,6 @@ 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,6 +10,9 @@ import {
|
||||||
selectTitleForUri,
|
selectTitleForUri,
|
||||||
selectClaimIsMine,
|
selectClaimIsMine,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
|
makeSelectIsBlacklisted,
|
||||||
|
makeSelectBlacklistedDueToDMCA,
|
||||||
|
makeSelectClaimErrorCensor,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import {
|
import {
|
||||||
makeSelectCollectionForId,
|
makeSelectCollectionForId,
|
||||||
|
@ -23,7 +26,6 @@ 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) => {
|
||||||
|
@ -72,7 +74,6 @@ 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),
|
||||||
|
@ -82,6 +83,9 @@ 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,10 +22,6 @@ 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,
|
||||||
|
@ -35,6 +31,9 @@ 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) {
|
||||||
|
@ -43,7 +42,6 @@ function ShowPage(props: Props) {
|
||||||
resolveUri,
|
resolveUri,
|
||||||
uri,
|
uri,
|
||||||
claim,
|
claim,
|
||||||
blackListedOutpoints,
|
|
||||||
location,
|
location,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
|
@ -54,11 +52,13 @@ 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)) {
|
if ((!claim || (claim && !claim.name)) && !isBlacklisted) {
|
||||||
innerContent = (
|
innerContent = (
|
||||||
<Page>
|
<Page>
|
||||||
{(claim === undefined ||
|
{(claim === undefined ||
|
||||||
|
@ -142,26 +142,15 @@ 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.name.length && claim.name[0] === '@') {
|
} else if (claim && claim.name.length && claim.name[0] === '@') {
|
||||||
innerContent = <ChannelPage uri={uri} location={location} />;
|
innerContent = <ChannelPage uri={uri} location={location} />;
|
||||||
} else if (claim) {
|
} else if (isBlacklisted && !claimIsMine) {
|
||||||
let isClaimBlackListed = false;
|
innerContent = isBlacklistedDueToDMCA ? (
|
||||||
|
|
||||||
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>
|
<Page>
|
||||||
<Card
|
<Card
|
||||||
title={uri}
|
title={uri}
|
||||||
subtitle={__(
|
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.'
|
'Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.'
|
||||||
)}
|
)}
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
|
@ -170,10 +159,25 @@ function ShowPage(props: Props) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
) : (
|
||||||
} else {
|
<Page>
|
||||||
innerContent = <FilePage uri={uri} location={location} />;
|
<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) {
|
||||||
|
innerContent = <FilePage uri={uri} location={location} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>{innerContent}</React.Fragment>;
|
return <React.Fragment>{innerContent}</React.Fragment>;
|
||||||
|
|
|
@ -16,7 +16,6 @@ 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';
|
||||||
|
@ -44,7 +43,6 @@ 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,32 +458,34 @@ export function doAnalyticsView(uri, timeToStart) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAnalyticsBuffer(uri, bufferData) {
|
export function doAnalyticsBuffer(uri, bufferData) {
|
||||||
return (dispatch, getState) => {
|
return () => {
|
||||||
const state = getState();
|
// return (dispatch, getState) => {
|
||||||
const claim = selectClaimForUri(state, uri);
|
// const state = getState();
|
||||||
const user = selectUser(state);
|
// const claim = selectClaimForUri(state, uri);
|
||||||
const {
|
// const user = selectUser(state);
|
||||||
value: { video, audio, source },
|
// const {
|
||||||
} = claim;
|
// value: { video, audio, source },
|
||||||
const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
// } = claim;
|
||||||
const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
// const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
||||||
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
// const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
||||||
const fileSize = source.size; // size in bytes
|
// const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||||
const fileSizeInBits = fileSize * 8;
|
// const fileSize = source.size; // size in bytes
|
||||||
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
// const fileSizeInBits = fileSize * 8;
|
||||||
const userId = user && user.id.toString();
|
// const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||||
|
// 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 (userId) {
|
// if (<condition>) {
|
||||||
analytics.videoBufferEvent(claim, {
|
// STUB: any buffer events here
|
||||||
timeAtBuffer,
|
// analytics.videoBufferEvent(claim, {
|
||||||
bufferDuration,
|
// timeAtBuffer,
|
||||||
bitRate,
|
// bufferDuration,
|
||||||
userId,
|
// bitRate,
|
||||||
duration: fileDurationInSeconds,
|
// userId,
|
||||||
playerPoweredBy: bufferData.playerPoweredBy,
|
// duration: fileDurationInSeconds,
|
||||||
readyState: bufferData.readyState,
|
// playerPoweredBy: bufferData.playerPoweredBy,
|
||||||
});
|
// 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 };
|
resolveInfo[uri] = { ...fallbackResolveInfo, errorCensor: uriResolveInfo.error.censor };
|
||||||
} else {
|
} else {
|
||||||
if (checkReposts) {
|
if (checkReposts) {
|
||||||
if (uriResolveInfo.reposted_claim) {
|
if (uriResolveInfo.reposted_claim) {
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
// @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,6 +1143,9 @@ 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) {
|
||||||
|
@ -1172,7 +1175,7 @@ export function doFetchModBlockedList() {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then(async (res) => {
|
||||||
let personalBlockList = [];
|
let personalBlockList = [];
|
||||||
let adminBlockList = [];
|
let adminBlockList = [];
|
||||||
let moderatorBlockList = [];
|
let moderatorBlockList = [];
|
||||||
|
@ -1185,15 +1188,21 @@ export function doFetchModBlockedList() {
|
||||||
const adminTimeoutMap = {};
|
const adminTimeoutMap = {};
|
||||||
const moderatorTimeoutMap = {};
|
const moderatorTimeoutMap = {};
|
||||||
|
|
||||||
const blockListsPerChannel = res.map((r) => r.value);
|
const blockListsPerChannel = [];
|
||||||
blockListsPerChannel
|
for (let i = 0; i < res.length; ++i) {
|
||||||
.sort((a, b) => {
|
blockListsPerChannel.push(res[i].value);
|
||||||
return 1;
|
if (i % 2 === 0) {
|
||||||
})
|
await yieldThread();
|
||||||
.forEach((channelBlockLists) => {
|
}
|
||||||
const storeList = (fetchedList, blockedList, timeoutMap, blockedByMap) => {
|
}
|
||||||
|
for (let i = 0; i < blockListsPerChannel.length; ++i) {
|
||||||
|
const storeList = async (fetchedList, blockedList, timeoutMap, blockedByMap) => {
|
||||||
if (fetchedList) {
|
if (fetchedList) {
|
||||||
fetchedList.forEach((blockedChannel) => {
|
for (let j = 0; j < fetchedList.length; ++j) {
|
||||||
|
const blockedChannel = fetchedList[j];
|
||||||
|
if (j > 0 && i % LOOP_CHUNK_SIZE === 0) {
|
||||||
|
await yieldThread();
|
||||||
|
}
|
||||||
if (blockedChannel.blocked_channel_name) {
|
if (blockedChannel.blocked_channel_name) {
|
||||||
const channelUri = buildURI({
|
const channelUri = buildURI({
|
||||||
channelName: blockedChannel.blocked_channel_name,
|
channelName: blockedChannel.blocked_channel_name,
|
||||||
|
@ -1227,24 +1236,28 @@ export function doFetchModBlockedList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
storeList(blocked_channels, personalBlockList, personalTimeoutMap);
|
if (i > 0 && i % LOOP_CHUNK_SIZE === 0) {
|
||||||
storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
|
await yieldThread();
|
||||||
storeList(
|
}
|
||||||
|
|
||||||
|
await storeList(blocked_channels, personalBlockList, personalTimeoutMap);
|
||||||
|
await storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
|
||||||
|
await storeList(
|
||||||
delegated_blocked_channels,
|
delegated_blocked_channels,
|
||||||
moderatorBlockList,
|
moderatorBlockList,
|
||||||
moderatorTimeoutMap,
|
moderatorTimeoutMap,
|
||||||
moderatorBlockListDelegatorsMap
|
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: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
export const doPublishDesktop = (filePath: ?File, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
const publishPreview = (previewResponse) => {
|
const publishPreview = (previewResponse) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
||||||
|
@ -138,14 +138,9 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) =>
|
||||||
data: { ...publishFormValue },
|
data: { ...publishFormValue },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const doUploadThumbnail = (
|
export const doUploadThumbnail =
|
||||||
filePath?: string,
|
(filePath?: string, thumbnailBlob?: File, fsAdapter?: any, fs?: any, path?: any, cb?: (string) => void) =>
|
||||||
thumbnailBlob?: File,
|
(dispatch: Dispatch) => {
|
||||||
fsAdapter?: any,
|
|
||||||
fs?: any,
|
|
||||||
path?: any,
|
|
||||||
cb?: (string) => void
|
|
||||||
) => (dispatch: Dispatch) => {
|
|
||||||
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
||||||
let thumbnail, fileExt, fileName, fileType;
|
let thumbnail, fileExt, fileName, fileType;
|
||||||
|
|
||||||
|
@ -253,9 +248,8 @@ export const doUploadThumbnail = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (
|
export const doPrepareEdit =
|
||||||
dispatch: Dispatch
|
(claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (dispatch: Dispatch) => {
|
||||||
) => {
|
|
||||||
const { name, amount, value = {} } = claim;
|
const { name, amount, value = {} } = claim;
|
||||||
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
||||||
const {
|
const {
|
||||||
|
@ -317,10 +311,8 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
||||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doPublish = (success: Function, fail: Function, preview: Function) => (
|
export const doPublish =
|
||||||
dispatch: Dispatch,
|
(success: Function, fail: Function, preview: Function) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
getState: () => {}
|
|
||||||
) => {
|
|
||||||
if (!preview) {
|
if (!preview) {
|
||||||
dispatch({ type: ACTIONS.PUBLISH_START });
|
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,10 +98,8 @@ export function doSetSync(oldHash: string, newHash: string, data: any) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => (
|
export const doGetSyncDesktop =
|
||||||
dispatch: Dispatch,
|
(cb?: (any, any) => void, password?: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
getState: GetState
|
|
||||||
) => {
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
||||||
const getSyncPending = selectGetSyncIsPending(state);
|
const getSyncPending = selectGetSyncIsPending(state);
|
||||||
|
@ -385,7 +383,6 @@ 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,
|
||||||
|
@ -403,7 +400,6 @@ 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,
|
||||||
|
@ -418,7 +414,6 @@ 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 } : {}),
|
||||||
|
@ -445,7 +440,6 @@ 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,
|
||||||
|
@ -470,7 +464,6 @@ 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,12 +82,6 @@ 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,6 +16,7 @@ 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>,
|
||||||
|
@ -67,6 +68,7 @@ type State = {
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
blacklistedByUri: {},
|
||||||
byId: {},
|
byId: {},
|
||||||
claimsByUri: {},
|
claimsByUri: {},
|
||||||
paginatedClaimsByChannel: {},
|
paginatedClaimsByChannel: {},
|
||||||
|
@ -118,6 +120,7 @@ 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);
|
||||||
|
@ -127,9 +130,11 @@ 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 } = resolveResponse;
|
const { claimsInChannel, stream, channel: channelFromResolve, collection, errorCensor } = 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]);
|
||||||
|
@ -197,6 +202,7 @@ function handleClaimAction(state: State, action: any): State {
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
|
blacklistedByUri,
|
||||||
byId,
|
byId,
|
||||||
claimsByUri: byUri,
|
claimsByUri: byUri,
|
||||||
channelClaimCounts,
|
channelClaimCounts,
|
||||||
|
@ -518,10 +524,8 @@ 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 {
|
const { claims: confirmedClaims, pending: pendingClaims }: { claims: Array<Claim>, pending: { [string]: Claim } } =
|
||||||
claims: confirmedClaims,
|
action.data;
|
||||||
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);
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
// @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,6 +4,7 @@ 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 };
|
||||||
|
@ -83,6 +84,46 @@ 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(
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
const selectState = (state) => state.coinSwap || {};
|
|
||||||
|
|
||||||
export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
|
|
||||||
return state.coinSwaps;
|
|
||||||
});
|
|
|
@ -161,18 +161,24 @@ 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 matchingFileInfos && matchingFileInfos.length
|
return sortedMatchedFileInfos.slice(start, end).map((fileInfo) =>
|
||||||
? matchingFileInfos.slice(start, end).map((fileInfo) =>
|
|
||||||
buildURI({
|
buildURI({
|
||||||
streamName: fileInfo.claim_name,
|
streamName: fileInfo.claim_name,
|
||||||
channelName: fileInfo.channel_name,
|
channelName: fileInfo.channel_name,
|
||||||
channelClaimId: fileInfo.channel_claim_id,
|
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 } from 'util/claim';
|
import { getChannelFromClaim, isChannelClaim } from 'util/claim';
|
||||||
|
|
||||||
// Returns the entire subscriptions state
|
// Returns the entire subscriptions state
|
||||||
const selectState = (state) => state.subscriptions || {};
|
const selectState = (state) => state.subscriptions || {};
|
||||||
|
@ -114,12 +114,18 @@ 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,
|
||||||
(claim, subscriptions) => {
|
(uri, claim, subscriptions) => {
|
||||||
const channelClaim = getChannelFromClaim(claim);
|
const channelClaim = getChannelFromClaim(claim);
|
||||||
if (channelClaim) {
|
if (channelClaim) {
|
||||||
const uri = channelClaim.permanent_url;
|
const permanentUrl = 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 (min-width: $breakpoint-medium) {
|
@media not all and (max-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 + .commentCreate {
|
.button + .comment-create {
|
||||||
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_selectedServer {
|
.button__selected-server {
|
||||||
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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
-webkit-line-clamp: 1 !important;
|
-webkit-line-clamp: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate {
|
.comment-create {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--reply {
|
.comment-create--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--nestedReply {
|
.comment-create--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,27 +149,40 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--bottom {
|
.comment-create--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;
|
||||||
width: 100%;
|
max-width: 50%;
|
||||||
|
|
||||||
.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);
|
||||||
|
@ -179,14 +192,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;
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__supportCommentPreview {
|
.comment-create__supportCommentPreview {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
@ -194,7 +207,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
|
|
||||||
.commentCreate__supportCommentPreviewAmount {
|
.comment-create__supportCommentPreviewAmount {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
font-size: var(--font-large);
|
font-size: var(--font-large);
|
||||||
}
|
}
|
||||||
|
@ -223,8 +236,8 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__stickerPreview {
|
.comment-create__stickerPreview {
|
||||||
@extend .commentCreate;
|
@extend .comment-create;
|
||||||
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);
|
||||||
|
@ -234,12 +247,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
|
|
||||||
.commentCreate__stickerPreviewInfo {
|
.comment-create__stickerPreviewInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__stickerPreviewImage {
|
.comment-create__stickerPreviewImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
|
|
|
@ -45,6 +45,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__actions-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -501,7 +507,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
min-width: 40%;
|
min-width: 40%;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
}
|
}
|
||||||
|
@ -543,7 +549,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
margin: 0 var(--spacing-xs);
|
margin: 0 var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,7 +564,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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
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 {
|
||||||
|
@ -175,6 +179,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__cover--text {
|
.content__cover--text {
|
||||||
|
|
|
@ -29,7 +29,12 @@ 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);
|
||||||
|
@ -532,6 +537,7 @@ 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 (min-width: $breakpoint-medium) {
|
@media not all and (max-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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
|
@ -461,7 +461,6 @@ 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 {
|
||||||
|
@ -804,7 +803,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-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,10 +114,15 @@
|
||||||
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-active);
|
color: var(--color-header-link);
|
||||||
}
|
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 (min-width: $breakpoint-medium) {
|
@media not all and (max-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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,17 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate {
|
.comment-create {
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
|
|
||||||
.commentCreate__label {
|
.comment-create__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) {
|
||||||
|
@ -81,7 +85,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate,
|
.comment-create,
|
||||||
.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…
Add table
Reference in a new issue