Compare commits
45 commits
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 |
76 changed files with 1580 additions and 1647 deletions
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,7 +38,22 @@ jobs:
|
|||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
if: startsWith(runner.os, 'mac')
|
||||
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
|
||||
run: |
|
||||
|
@ -58,7 +73,7 @@ jobs:
|
|||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
|
||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/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
|
||||
|
||||
# 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/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
48
CHANGELOG.md
48
CHANGELOG.md
|
@ -1,7 +1,53 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.53.9] - [2023-2-8]
|
||||
|
||||
### Changed
|
||||
- Updated lbrynet to [0.113.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.113.0)
|
||||
|
||||
## [0.53.8] - [2022-11-17]
|
||||
|
||||
### Fixed
|
||||
- Selecting a large file in publish no longer crashes ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||
- Unfollowing unpublished channels ([#7737](https://github.com/lbryio/lbry-desktop/pull/7737))
|
||||
|
||||
|
||||
### Changed
|
||||
- Updated xcode to 13.1 and hacked a fix for release ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||
|
||||
## [0.53.7] - [2022-11-10]
|
||||
|
||||
### Added
|
||||
- 'Collections' to txo filter _community pr!_ ([#7711](https://github.com/lbryio/lbry-desktop/pull/7711))
|
||||
- Swap comment servers _community pr!_ ([#7670](https://github.com/lbryio/lbry-desktop/pull/7670))
|
||||
|
||||
### Fixed
|
||||
- Thumbnails no longer disable publish ([#7714](https://github.com/lbryio/lbry-desktop/pull/7714))
|
||||
- Publishing posts were empty ([#7715](https://github.com/lbryio/lbry-desktop/pull/7715))
|
||||
- Minor layout fixes _community pr!_ ([#7709](https://github.com/lbryio/lbry-desktop/pull/7709))
|
||||
- Comment section buttons layout ([#7716](https://github.com/lbryio/lbry-desktop/pull/7716))
|
||||
|
||||
### Changed
|
||||
- Removed watchman and its errors ([#7710](https://github.com/lbryio/lbry-desktop/pull/7710))
|
||||
- Updated lbrynet to [0.112.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.112.0)
|
||||
|
||||
## [0.53.6] - [2022-10-21]
|
||||
|
||||
### Fixed
|
||||
- Make thumbnails optional ([#7690](https://github.com/lbryio/lbry-desktop/pull/7690))
|
||||
- Show downloads newest first ([#7684](https://github.com/lbryio/lbry-desktop/pull/7684))
|
||||
- Only allow images in image uploader ([#7672](https://github.com/lbryio/lbry-desktop/pull/7672))
|
||||
- Fixed bug with csv exports ([#7697](https://github.com/lbryio/lbry-desktop/pull/7697))
|
||||
- Fixed various upload bugs including transcoding ([#7688](https://github.com/lbryio/lbry-desktop/pull/7688))
|
||||
- Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704))
|
||||
|
||||
### Changed
|
||||
- Upgraded Electron to v17.2.0 ([#7703](https://github.com/lbryio/lbry-desktop/pull/7703))
|
||||
- Upgraded Electron to v17.0.0 ([#7691](https://github.com/lbryio/lbry-desktop/pull/7691))
|
||||
- Updated lbrynet to [0.111.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.111.0)
|
||||
|
||||
## [0.53.5] - [2022-08-26]
|
||||
|
||||
### Added
|
||||
|
|
|
@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
|
|||
|
||||
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
||||
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -77,7 +77,7 @@ Start the installed application to interact with the LBRY network.
|
|||
#### Prerequisites
|
||||
|
||||
- [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)
|
||||
- [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';
|
||||
|
||||
const { download } = require('electron-dl');
|
||||
const mime = require('mime');
|
||||
const remote = require('@electron/remote/main');
|
||||
const os = require('os');
|
||||
const sudo = require('sudo-prompt');
|
||||
const probe = require('ffmpeg-probe');
|
||||
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
|
||||
|
||||
remote.initialize();
|
||||
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
||||
|
@ -299,6 +302,96 @@ app.on('before-quit', () => {
|
|||
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) => {
|
||||
try {
|
||||
const { data_dir } = await Lbry.settings_get();
|
||||
|
|
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",
|
||||
"version": "0.53.5",
|
||||
"version": "0.53.9",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -41,32 +41,29 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@emotion/react": "^11.6.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/material": "^5.2.1",
|
||||
"@ungap/from-entries": "^0.2.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
"electron-dl": "^3.2.0",
|
||||
"electron-log": "^2.2.12",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-updater": "^4.2.4",
|
||||
"express": "^4.17.1",
|
||||
"ffmpeg-probe": "^1.0.6",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"if-env": "^1.0.4",
|
||||
"match-sorter": "^6.3.0",
|
||||
"mime": "^3.0.0",
|
||||
"node-html-parser": "^5.1.0",
|
||||
"parse-duration": "^1.0.0",
|
||||
"proxy-polyfill": "0.1.6",
|
||||
"re-reselect": "^4.0.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-datetime-picker": "^3.4.3",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rss": "^1.2.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tempy": "^0.6.0",
|
||||
"videojs-logo": "^2.1.4"
|
||||
"tempy": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
|
@ -77,7 +74,7 @@
|
|||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.2.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-flow": "^7.12.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
|
@ -85,7 +82,6 @@
|
|||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||
"@hot-loader/react-dom": "^16.13",
|
||||
"@meetfranz/electron-cookies": "^3.0.2",
|
||||
"@reach/auto-id": "^0.13.0",
|
||||
"@reach/combobox": "^0.12.1",
|
||||
"@reach/menu-button": "0.8.6",
|
||||
"@reach/rect": "^0.16.0",
|
||||
|
@ -96,21 +92,17 @@
|
|||
"@sentry/webpack-plugin": "^1.10.0",
|
||||
"@types/three": "^0.103.2",
|
||||
"adm-zip": "^0.4.13",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-add-module-exports": "^1.0.4",
|
||||
"babel-plugin-import-glob": "^2.0.0",
|
||||
"babel-plugin-transform-imports": "^1.5.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"chalk": "^4.1.0",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.39.2",
|
||||
"concurrently": "^4.1.2",
|
||||
"connected-react-router": "^6.8.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"country-data": "^0.0.31",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.0.0",
|
||||
|
@ -121,10 +113,9 @@
|
|||
"decompress": "^4.2.1",
|
||||
"del": "^3.0.0",
|
||||
"devtron": "^1.4.0",
|
||||
"dom-scroll-into-view": "^1.2.1",
|
||||
"dotenv-defaults": "^2.0.1",
|
||||
"dotenv-webpack": "^1.8.0",
|
||||
"electron": "15.5.5",
|
||||
"electron": "17.2.0",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-is-dev": "^0.3.0",
|
||||
|
@ -159,10 +150,7 @@
|
|||
"lodash-es": "^4.17.21",
|
||||
"mammoth": "^1.4.16",
|
||||
"moment": "^2.29.2",
|
||||
"node-abi": "^2.5.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-html-parser": "^5.1.0",
|
||||
"node-libs-browser": "^2.1.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-wget": "^0.4.3",
|
||||
"nodemon": "^1.19.1",
|
||||
|
@ -177,7 +165,6 @@
|
|||
"rc-progress": "^2.0.6",
|
||||
"react": "^16.8.2",
|
||||
"react-awesome-lightbox": "^1.7.3",
|
||||
"react-confetti": "^4.0.1",
|
||||
"react-dom": "^16.8.2",
|
||||
"react-draggable": "^3.3.0",
|
||||
"react-google-recaptcha": "^2.0.1",
|
||||
|
@ -188,7 +175,6 @@
|
|||
"react-router": "^5.1.0",
|
||||
"react-router-dom": "^5.1.0",
|
||||
"react-simplemde-editor": "^4.1.3",
|
||||
"react-spring": "^8.0.20",
|
||||
"reakit": "^1.0.0-beta.13",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^5.10.0",
|
||||
|
@ -205,20 +191,16 @@
|
|||
"sass": "^1.29.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semver": "^5.3.0",
|
||||
"stream-to-blob-url": "^2.1.1",
|
||||
"strip-markdown": "^3.0.3",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"three-full": "^28.0.2",
|
||||
"tiny-relative-date": "^1.3.0",
|
||||
"tree-kill": "^1.1.0",
|
||||
"unist-util-visit": "^2.0.3",
|
||||
"uuid": "^8.3.2",
|
||||
"video.js": "^7.14.3",
|
||||
"videojs-contrib-quality-levels": "^2.0.9",
|
||||
"videojs-event-tracking": "^1.0.1",
|
||||
"villain-react": "^1.0.9",
|
||||
"wavesurfer.js": "^2.2.1",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-bundle-analyzer": "^3.1.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
|
@ -228,15 +210,14 @@
|
|||
"webpack-hot-middleware": "^2.24.3",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"y18n": "^4.0.1",
|
||||
"yarnhook": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7",
|
||||
"node": ">=16.13",
|
||||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.110.0",
|
||||
"lbrynetDaemonVersion": "0.113.0",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
|
|
|
@ -2318,5 +2318,9 @@
|
|||
"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--"
|
||||
}
|
||||
|
|
164
ui/analytics.js
164
ui/analytics.js
|
@ -1,4 +1,11 @@
|
|||
// @flow
|
||||
/*
|
||||
Removed Watchman (internal view tracking) code.
|
||||
This file may eventually implement cantina
|
||||
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
||||
refer to ___ for removal commit.
|
||||
*/
|
||||
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||
|
@ -14,9 +21,6 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
|
|||
export const SHARE_INTERNAL = 'shareInternal';
|
||||
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
||||
|
||||
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
|
||||
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
|
||||
|
||||
if (isProduction) {
|
||||
ElectronCookies.enable({
|
||||
origin: 'https://lbry.tv',
|
||||
|
@ -68,114 +72,10 @@ type LogPublishParams = {
|
|||
let internalAnalyticsEnabled: boolean = false;
|
||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||
|
||||
/**
|
||||
* Determine the mobile device type viewing the data
|
||||
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
function getDeviceType() {
|
||||
return 'dsk';
|
||||
}
|
||||
// variables initialized for watchman
|
||||
let amountOfBufferEvents = 0;
|
||||
let amountOfBufferTimeInMS = 0;
|
||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
||||
let lastSentTime;
|
||||
|
||||
// calculate data for backend, send them, and reset buffer data for next interval
|
||||
async function sendAndResetWatchmanData() {
|
||||
if (!userId) {
|
||||
return 'Can only be used with a user id';
|
||||
}
|
||||
|
||||
if (!videoPlayer) {
|
||||
return 'Video player not initialized';
|
||||
}
|
||||
|
||||
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
||||
lastSentTime = new Date();
|
||||
|
||||
let protocol;
|
||||
if (videoType === 'application/x-mpegURL') {
|
||||
protocol = 'hls';
|
||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||
// $FlowFixMe
|
||||
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
||||
} else {
|
||||
protocol = 'stb';
|
||||
}
|
||||
|
||||
// current position in video in MS
|
||||
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
||||
|
||||
// get the duration marking the time in the video for relative position calculation
|
||||
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
||||
|
||||
// build object for watchman backend
|
||||
const objectToSend = {
|
||||
rebuf_count: amountOfBufferEvents,
|
||||
rebuf_duration: amountOfBufferTimeInMS,
|
||||
url: claimUrl.replace('lbry://', ''),
|
||||
device: getDeviceType(),
|
||||
duration: timeSinceLastIntervalSend,
|
||||
protocol,
|
||||
player: playerPoweredBy,
|
||||
user_id: userId.toString(),
|
||||
position: Math.round(positionInVideo),
|
||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||
bitrate: bitrateAsBitsPerSecond,
|
||||
bandwidth: undefined,
|
||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||
};
|
||||
|
||||
// post to watchman
|
||||
await sendWatchmanData(objectToSend);
|
||||
|
||||
// reset buffer data
|
||||
amountOfBufferEvents = 0;
|
||||
amountOfBufferTimeInMS = 0;
|
||||
}
|
||||
|
||||
let watchmanInterval;
|
||||
// clear watchman interval and mark it as null (when video paused)
|
||||
function stopWatchmanInterval() {
|
||||
clearInterval(watchmanInterval);
|
||||
watchmanInterval = null;
|
||||
}
|
||||
|
||||
// creates the setInterval that will run send to watchman on recurring basis
|
||||
function startWatchmanIntervalIfNotRunning() {
|
||||
if (!watchmanInterval) {
|
||||
// instantiate the first time to calculate duration from
|
||||
lastSentTime = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
// post data to the backend
|
||||
async function sendWatchmanData(body) {
|
||||
try {
|
||||
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
const analytics: Analytics = {
|
||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||
videoBufferEvent: async (claim, data) => {
|
||||
amountOfBufferEvents = amountOfBufferEvents + 1;
|
||||
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
||||
},
|
||||
onDispose: () => {
|
||||
stopWatchmanInterval();
|
||||
// stub
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||
let playerIsSeeking = false;
|
||||
// have to use this because videojs pauses/unpauses during seek
|
||||
// sometimes the seeking function isn't populated yet so check for it as well
|
||||
if (passedPlayer && passedPlayer.seeking) {
|
||||
playerIsSeeking = passedPlayer.seeking();
|
||||
}
|
||||
|
||||
// if being paused, and not seeking, send existing data and stop interval
|
||||
if (!isPlaying && !playerIsSeeking) {
|
||||
sendAndResetWatchmanData();
|
||||
stopWatchmanInterval();
|
||||
// if being told to pause, and seeking, send and restart interval
|
||||
} else if (!isPlaying && playerIsSeeking) {
|
||||
sendAndResetWatchmanData();
|
||||
stopWatchmanInterval();
|
||||
startWatchmanIntervalIfNotRunning();
|
||||
// is being told to play, and seeking, don't do anything,
|
||||
// assume it's been started already from pause
|
||||
} else if (isPlaying && playerIsSeeking) {
|
||||
// start but not a seek, assuming a start from paused content
|
||||
} else if (isPlaying && !playerIsSeeking) {
|
||||
startWatchmanIntervalIfNotRunning();
|
||||
}
|
||||
// stub
|
||||
},
|
||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||
// populate values for watchman when video starts
|
||||
userId = passedUserId;
|
||||
claimUrl = canonicalUrl;
|
||||
playerPoweredBy = poweredBy;
|
||||
|
||||
videoType = passedPlayer.currentSource().type;
|
||||
videoPlayer = passedPlayer;
|
||||
bitrateAsBitsPerSecond = videoBitrate;
|
||||
|
||||
// sendPromMetric('time_to_start', duration);
|
||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||
},
|
||||
|
@ -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({
|
||||
urlBase: MATOMO_URL,
|
||||
siteId: MATOMO_ID, // optional, default value: `1`
|
||||
// heartBeat: { // optional, enabled by default
|
||||
// active: true, // optional, default value: true
|
||||
// seconds: 10 // optional, default value: `15
|
||||
// },
|
||||
// linkTracking: false // optional, default value: true
|
||||
});
|
||||
|
||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
|
|||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import TagsSearch from 'component/tagsSearch';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
|
@ -325,7 +325,6 @@ function ChannelForm(props: Props) {
|
|||
uri={uri}
|
||||
thumbnailPreview={thumbnailPreview}
|
||||
allowGifs
|
||||
showDelayedMessage={isUpload.thumbnail}
|
||||
setThumbUploadError={setThumbError}
|
||||
thumbUploadError={thumbError}
|
||||
/>
|
||||
|
@ -377,7 +376,7 @@ function ChannelForm(props: Props) {
|
|||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||
maxLength={MAX_TITLE_LEN}
|
||||
/>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import { handleBidChange } from 'util/publish';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||
|
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
|
|||
usePublishFormMode
|
||||
/>
|
||||
</fieldset-section>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
|
@ -319,7 +319,7 @@ function CommentView(props: Props) {
|
|||
<div>
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
className="comment__edit-input"
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
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 { 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 { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
|
@ -22,13 +22,12 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import Icon from 'component/common/icon';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import StickerSelector from './sticker-selector';
|
||||
import CommentCreateHeader from './comment-create-header';
|
||||
import type { ElementRef } from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
|
@ -364,31 +363,6 @@ export function CommentCreate(props: Props) {
|
|||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||
|
||||
// LIVESTREAM ONLY - REMOVE
|
||||
// Handle keyboard shortcut comment creation
|
||||
// React.useEffect(() => {
|
||||
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
//
|
||||
// if (inputRef && inputRef.current === document.activeElement) {
|
||||
// // $FlowFixMe
|
||||
// const isTyping = e.target.attributes['term'];
|
||||
//
|
||||
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||
// e.preventDefault();
|
||||
// buttonRef.current.click();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('keydown', altEnterListener);
|
||||
//
|
||||
// // removes the listener so it doesn't cause problems elsewhere in the app
|
||||
// return () => {
|
||||
// window.removeEventListener('keydown', altEnterListener);
|
||||
// };
|
||||
// }, [isLivestream]);
|
||||
|
||||
// **************************************************************************
|
||||
// Render
|
||||
// **************************************************************************
|
||||
|
@ -410,7 +384,11 @@ export function CommentCreate(props: Props) {
|
|||
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">
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||
</div>
|
||||
|
@ -421,22 +399,22 @@ export function CommentCreate(props: Props) {
|
|||
return (
|
||||
<Form
|
||||
onSubmit={() => {}}
|
||||
className={classnames('commentCreate', {
|
||||
'commentCreate--reply': isReply,
|
||||
'commentCreate--nestedReply': isNested,
|
||||
'commentCreate--bottom': bottom,
|
||||
className={classnames('comment-create', {
|
||||
'comment-create--reply': isReply,
|
||||
'comment-create--nestedReply': isNested,
|
||||
'comment-create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<div className="comment-create__stickerPreview">
|
||||
<div className="comment-create__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<div className="comment-create__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
{/* figure out lbc sticker prices */}
|
||||
|
@ -448,15 +426,15 @@ export function CommentCreate(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<div className="comment-create__supportCommentPreview">
|
||||
<CreditAmount
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
className="comment-create__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<div className="comment-create__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
|
@ -471,23 +449,22 @@ export function CommentCreate(props: Props) {
|
|||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
autoFocus={isReply}
|
||||
charCount={charCount}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
disabled={isFetchingChannels}
|
||||
label={
|
||||
<div className="commentCreate__labelWrapper">
|
||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
header={
|
||||
<CommentCreateHeader
|
||||
isReply={isReply}
|
||||
advanced={advancedEditor}
|
||||
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
/>
|
||||
}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
ref={formFieldRef}
|
||||
onChange={handleCommentChange}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
|
@ -655,7 +632,7 @@ export function CommentCreate(props: Props) {
|
|||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<div className="help--notice comment-create__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
|
|
|
@ -23,6 +23,9 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
|
|||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import CommentsList from './view';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri } = props;
|
||||
|
@ -56,15 +59,19 @@ const select = (state, props) => {
|
|||
myReactsByCommentId: selectMyReacts(state),
|
||||
othersReactsById: selectOthersReacts(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
|
||||
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = {
|
||||
fetchTopLevelComments: doCommentList,
|
||||
fetchComment: doCommentById,
|
||||
fetchReacts: doCommentReactList,
|
||||
resetComments: doCommentReset,
|
||||
doResolveUris,
|
||||
};
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
|
||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
||||
doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)),
|
||||
setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentsList);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { ENABLE_COMMENT_REACTIONS, COMMENT_SERVER_API, COMMENT_SERVER_NAME } from 'config';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import { getCommentsListTitle } from 'util/comments';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
@ -15,6 +15,8 @@ import Empty from 'component/common/empty';
|
|||
import React, { useEffect } from 'react';
|
||||
import Spinner from 'component/spinner';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Comments from 'comments';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -52,6 +54,9 @@ type Props = {
|
|||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||
resetComments: (claimId: string) => void,
|
||||
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||
customCommentServers: Array<CommentServerDetails>,
|
||||
setCommentServer: (string) => void,
|
||||
commentServer: string,
|
||||
};
|
||||
|
||||
export default function CommentList(props: Props) {
|
||||
|
@ -80,11 +85,17 @@ export default function CommentList(props: Props) {
|
|||
fetchReacts,
|
||||
resetComments,
|
||||
doResolveUris,
|
||||
customCommentServers,
|
||||
setCommentServer,
|
||||
commentServer,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
|
||||
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
|
||||
const allServers = [defaultServer, ...(customCommentServers || [])];
|
||||
|
||||
const spinnerRef = React.useRef();
|
||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||
|
@ -255,7 +266,16 @@ export default function CommentList(props: Props) {
|
|||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||
|
||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||
const actionButtonsProps = {
|
||||
totalComments,
|
||||
sort,
|
||||
changeSort,
|
||||
setPage,
|
||||
allServers,
|
||||
commentServer,
|
||||
defaultServer,
|
||||
setCommentServer,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -334,17 +354,21 @@ type ActionButtonsProps = {
|
|||
sort: string,
|
||||
changeSort: (string) => void,
|
||||
setPage: (number) => void,
|
||||
allServers: Array<CommentServerDetails>,
|
||||
commentServer: string,
|
||||
setCommentServer: (string) => void,
|
||||
defaultServer: CommentServerDetails,
|
||||
};
|
||||
|
||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
||||
|
||||
const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } =
|
||||
actionButtonsProps;
|
||||
const sortButtonProps = { activeSort: sort, changeSort };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'comment__actions-row'}>
|
||||
{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}
|
||||
|
@ -353,11 +377,39 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
|||
sortOption={SORT_BY.CONTROVERSY}
|
||||
/>
|
||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</>
|
||||
{allServers.length >= 2 && (
|
||||
<div className="button__selected-server">
|
||||
<FormField
|
||||
type="select-tiny"
|
||||
onChange={function (x) {
|
||||
const selectedServer = x.target.value;
|
||||
setPage(0);
|
||||
setCommentServer(selectedServer);
|
||||
if (selectedServer === defaultServer.url) {
|
||||
Comments.setServerUrl(undefined);
|
||||
} else {
|
||||
Comments.setServerUrl(selectedServer);
|
||||
}
|
||||
}}
|
||||
value={commentServer}
|
||||
>
|
||||
{allServers.map(function (server) {
|
||||
return (
|
||||
<option key={server.url} value={server.url}>
|
||||
{server.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
<div className="button_refresh">
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
||||
|
||||
type Props = {
|
||||
files: Array<WebFile>,
|
||||
onChange: (WebFile | void) => void,
|
||||
files: Array<File>,
|
||||
onChange: (File | void) => void,
|
||||
};
|
||||
|
||||
type RadioProps = {
|
||||
|
@ -26,16 +26,16 @@ function FileList(props: Props) {
|
|||
|
||||
const getFile = (value?: string) => {
|
||||
if (files && files.length) {
|
||||
return files.find((file: WebFile) => file.name === value);
|
||||
return files.find((file: File) => file.name === value);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (radio.stops.length) {
|
||||
if (radio.items.length) {
|
||||
if (!radio.currentId) {
|
||||
radio.first();
|
||||
} else {
|
||||
const first = radio.stops[0].ref.current;
|
||||
const first = radio.items[0].ref.current;
|
||||
// First auto-selection
|
||||
if (first && first.id === radio.currentId && !radio.state) {
|
||||
const file = getFile(first.value);
|
||||
|
@ -46,12 +46,12 @@ function FileList(props: Props) {
|
|||
|
||||
if (radio.state) {
|
||||
// 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;
|
||||
// Only update state if new item is selected
|
||||
if (element && element.value !== radio.state) {
|
||||
const file = getFile(element.value);
|
||||
// Sselect new file and update state
|
||||
// Select new file and update state
|
||||
onChange(file);
|
||||
radio.setState(element.value);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as remote from '@electron/remote';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
type Props = {
|
||||
type: string,
|
||||
currentPath?: ?string,
|
||||
onFileChosen: (WebFile) => void,
|
||||
onFileChosen: (FileWithPath) => void,
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
accept?: string,
|
||||
error?: string,
|
||||
disabled?: boolean,
|
||||
autoFocus?: boolean,
|
||||
filters?: Array<{ name: string, extension: string[] }>,
|
||||
readFile?: boolean,
|
||||
};
|
||||
|
||||
class FileSelector extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
type: 'file',
|
||||
readFile: true,
|
||||
};
|
||||
|
||||
fileInput: React.ElementRef<any>;
|
||||
|
@ -41,7 +45,7 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
const file = files[0];
|
||||
|
||||
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
|
||||
};
|
||||
|
@ -64,13 +68,27 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
properties = ['openDirectory'];
|
||||
}
|
||||
|
||||
remote.dialog.showOpenDialog({ properties, defaultPath }).then((result) => {
|
||||
const path = result && result.filePaths[0];
|
||||
if (path) {
|
||||
// $FlowFixMe
|
||||
this.props.onFileChosen({ path });
|
||||
}
|
||||
});
|
||||
remote.dialog
|
||||
.showOpenDialog({
|
||||
properties,
|
||||
defaultPath,
|
||||
filters: this.props.filters,
|
||||
})
|
||||
.then((result) => {
|
||||
const path = result && result.filePaths[0];
|
||||
if (path) {
|
||||
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile);
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const file = new File([result.buffer], result.name, {
|
||||
type: result.mime,
|
||||
});
|
||||
this.props.onFileChosen({ file, path: result.path });
|
||||
});
|
||||
};
|
||||
|
||||
fileInputButton = () => {
|
||||
|
|
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
|
||||
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 = {
|
||||
|
@ -21,19 +14,15 @@ type Props = {
|
|||
disabled?: boolean,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
inputButton?: React$Node,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
postfix?: string,
|
||||
prefix?: string,
|
||||
quickActionLabel?: string,
|
||||
range?: number,
|
||||
readOnly?: boolean,
|
||||
stretch?: boolean,
|
||||
|
@ -41,8 +30,6 @@ type Props = {
|
|||
type?: string,
|
||||
value?: string | number,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
};
|
||||
|
||||
|
@ -72,21 +59,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
children,
|
||||
error,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
inputButton,
|
||||
isLivestream,
|
||||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
@ -101,18 +82,10 @@ export class FormField extends React.PureComponent<Props> {
|
|||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const Wrapper = blockWrap
|
||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||
|
||||
const quickAction =
|
||||
quickActionLabel && quickActionHandler ? (
|
||||
<div className="form-field__quick-action">
|
||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const inputSimple = (type: string) => (
|
||||
<>
|
||||
<input id={name} type={type} {...inputProps} />
|
||||
|
@ -143,133 +116,22 @@ export class FormField extends React.PureComponent<Props> {
|
|||
return inputSelect('');
|
||||
case 'select-tiny':
|
||||
return inputSelect('select--slim');
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
|
||||
if (delta <= 0) return;
|
||||
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substring(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
{label && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{quickAction}
|
||||
</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>
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">{countInfo}</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { Form } from './form-components/form';
|
||||
export { FormField } from './form-components/form-field';
|
||||
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
||||
export { FormFieldPrice } from './form-components/form-field-price';
|
||||
export { Submit } from './form-components/submit';
|
||||
|
|
|
@ -2054,4 +2054,15 @@ export const icons = {
|
|||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
||||
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -11,10 +11,10 @@ import Icon from 'component/common/icon';
|
|||
|
||||
type Props = {
|
||||
modal: { id: string, modalProps: {} },
|
||||
filePath: string | WebFile,
|
||||
filePath: ?string,
|
||||
clearPublish: () => void,
|
||||
updatePublishForm: ({}) => void,
|
||||
openModal: (id: string, { files: Array<WebFile> }) => void,
|
||||
openModal: (id: string, { files: Array<File> }) => void,
|
||||
// React router
|
||||
history: {
|
||||
entities: {}[],
|
||||
|
@ -37,7 +37,7 @@ function FileDrop(props: Props) {
|
|||
const { drag, dropData } = useDragDrop();
|
||||
const [files, setFiles] = React.useState([]);
|
||||
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 targetTimer = React.useRef(null);
|
||||
const navigationTimer = React.useRef(null);
|
||||
|
@ -65,24 +65,26 @@ function FileDrop(props: Props) {
|
|||
}
|
||||
}, [history]);
|
||||
|
||||
// Delay hide and navigation for a smooth transition
|
||||
const hideDropArea = React.useCallback(() => {
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setFiles([]);
|
||||
// Navigate to publish area
|
||||
navigationTimer.current = setTimeout(() => {
|
||||
navigateToPublish();
|
||||
}, NAVIGATE_TIME_OUT);
|
||||
}, HIDE_TIME_OUT);
|
||||
}, [navigateToPublish]);
|
||||
|
||||
// Handle file selection
|
||||
const handleFileSelected = React.useCallback(
|
||||
(selectedFile) => {
|
||||
updatePublishForm({ filePath: selectedFile });
|
||||
hideDropArea();
|
||||
// Delay hide and navigation for a smooth transition
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setFiles([]);
|
||||
// Navigate to publish area
|
||||
navigationTimer.current = setTimeout(() => {
|
||||
// Navigate first, THEN assign filePath, otherwise
|
||||
// the file selected will get reset (that's how the
|
||||
// publish file view works, when the user switches to
|
||||
// publish a file, the pathFile value gets reset to undefined)
|
||||
navigateToPublish();
|
||||
updatePublishForm({
|
||||
filePath: selectedFile.path || selectedFile.name,
|
||||
});
|
||||
}, NAVIGATE_TIME_OUT);
|
||||
}, HIDE_TIME_OUT);
|
||||
},
|
||||
[updatePublishForm, hideDropArea]
|
||||
[setFiles, navigateToPublish, updatePublishForm]
|
||||
);
|
||||
|
||||
// Clear timers when unmounted
|
||||
|
|
|
@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
|||
import Draggable from 'react-draggable';
|
||||
import { onFullscreenChange } from 'util/full-screen';
|
||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import debounce from 'util/debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
import { isURIEqual } from 'util/lbryURI';
|
||||
|
@ -132,6 +132,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
const playingUriSource = playingUri && playingUri.source;
|
||||
const isComment = playingUriSource === 'comment';
|
||||
const isMobile = useIsMobile();
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||
|
||||
const [fileViewerRect, setFileViewerRect] = useState();
|
||||
|
@ -343,7 +344,8 @@ export default function FileRenderFloating(props: Props) {
|
|||
'content__viewer--floating': isFloating,
|
||||
'content__viewer--inline': !isFloating,
|
||||
'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,
|
||||
})}
|
||||
style={
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as PAGES from 'constants/pages';
|
|||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import Button from 'component/button';
|
||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||
import Nag from 'component/common/nag';
|
||||
|
@ -63,6 +64,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
const fileStatus = fileInfo && fileInfo.status;
|
||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||
const containerRef = React.useRef<any>();
|
||||
|
||||
|
@ -151,7 +153,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
className={classnames('content__cover', {
|
||||
'content__cover--disabled': disabled,
|
||||
'content__cover--theater-mode': videoTheaterMode,
|
||||
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
||||
'content__cover--text': isText,
|
||||
'card__media--nsfw': obscurePreview,
|
||||
})}
|
||||
|
|
|
@ -13,7 +13,7 @@ const select = (state, props) => {
|
|||
if (claimUriBeingPlayed) {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
||||
isBeingPlayed = claim.claim_id === claimBeingPlayed.claim_id;
|
||||
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -92,7 +92,7 @@ function Page(props: Props) {
|
|||
<div
|
||||
className={classnames('main-wrapper__inner', {
|
||||
'main-wrapper__inner--filepage': isOnFilePage,
|
||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
||||
})}
|
||||
>
|
||||
{!authPage &&
|
||||
|
@ -124,7 +124,7 @@ function Page(props: Props) {
|
|||
'main--file-page': filePage,
|
||||
'main--settings-page': settingsPage,
|
||||
'main--markdown': isMarkdown,
|
||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
|
||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// @flow
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||
|
||||
type Props = {
|
||||
uri: ?string,
|
||||
label: ?string,
|
||||
disabled: ?boolean,
|
||||
filePath: string | WebFile,
|
||||
filePath: File,
|
||||
fileText: ?string,
|
||||
fileMimeType: ?string,
|
||||
streamingUrl: ?string,
|
||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
|||
]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type={'markdown'}
|
||||
name="content_post"
|
||||
label={label}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
|||
return (
|
||||
<Card
|
||||
actions={
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="content_description"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { Node } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { regexInvalidURI } from 'util/lbryURI';
|
||||
import PostEditor from 'component/postEditor';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
|
@ -13,13 +14,13 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||
import PublishName from 'component/publishName';
|
||||
|
||||
import path from 'path';
|
||||
type Props = {
|
||||
uri: ?string,
|
||||
mode: ?string,
|
||||
name: ?string,
|
||||
title: ?string,
|
||||
filePath: string | WebFile,
|
||||
filePath: ?string,
|
||||
fileMimeType: ?string,
|
||||
isStillEditing: boolean,
|
||||
balance: number,
|
||||
|
@ -77,7 +78,7 @@ function PublishFile(props: Props) {
|
|||
const sizeInMB = Number(size) / 1000000;
|
||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||
const ffmpegAvail = ffmpegStatus.available;
|
||||
const [currentFile, setCurrentFile] = useState(null);
|
||||
const currentFile = filePath;
|
||||
const [currentFileType, setCurrentFileType] = useState(null);
|
||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||
|
@ -91,17 +92,35 @@ function PublishFile(props: Props) {
|
|||
}
|
||||
}, [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(() => {
|
||||
if (!filePath || filePath === '') {
|
||||
setCurrentFile('');
|
||||
updateFileInfo(0, 0, false);
|
||||
} else if (typeof filePath !== 'string') {
|
||||
// Update currentFile file
|
||||
if (filePath.name !== currentFile && filePath.path !== currentFile) {
|
||||
handleFileChange(filePath);
|
||||
}
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
}, [filePath, currentFile, handleFileChange, updateFileInfo]);
|
||||
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,
|
||||
});
|
||||
}
|
||||
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(() => {
|
||||
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;
|
||||
|
||||
// select file, start to select a new one, then cancel
|
||||
if (!file) {
|
||||
if (!fileData || fileData.error) {
|
||||
if (isStillEditing || !clearName) {
|
||||
updatePublishForm({ filePath: '' });
|
||||
} else {
|
||||
|
@ -222,8 +241,12 @@ function PublishFile(props: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
// if video, extract duration so we can warn about bitrateif (typeof file !== 'string') {
|
||||
const contentType = file.type && file.type.split('/');
|
||||
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
||||
const file = fileData.file;
|
||||
// Check to see if it's a video and if mp4
|
||||
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
|
||||
const duration = fileData.duration;
|
||||
const size = fileData.size;
|
||||
const isVideo = contentType && contentType[0] === 'video';
|
||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||
|
||||
|
@ -231,34 +254,25 @@ function PublishFile(props: Props) {
|
|||
|
||||
if (contentType && contentType[0] === 'text') {
|
||||
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
||||
setCurrentFileType(contentType);
|
||||
} else if (file.name) {
|
||||
// If user's machine is missign a valid content type registration
|
||||
setCurrentFileType(contentType.join('/'));
|
||||
} else if (path.parse(fileData.path).ext) {
|
||||
// If user's machine is missing a valid content type registration
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
if (isMp4) {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.onloadedmetadata = () => {
|
||||
updateFileInfo(video.duration, file.size, isVideo);
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
};
|
||||
video.onerror = () => {
|
||||
updateFileInfo(0, file.size, isVideo);
|
||||
};
|
||||
video.src = window.URL.createObjectURL(file);
|
||||
updateFileInfo(duration || 0, size, isVideo);
|
||||
} else {
|
||||
updateFileInfo(0, file.size, isVideo);
|
||||
updateFileInfo(duration || 0, size, isVideo);
|
||||
}
|
||||
} else {
|
||||
updateFileInfo(0, file.size, isVideo);
|
||||
updateFileInfo(0, size, isVideo);
|
||||
}
|
||||
|
||||
if (isTextPost) {
|
||||
if (isTextPost && file) {
|
||||
// Create reader
|
||||
const reader = new FileReader();
|
||||
// Handler for file reader
|
||||
|
@ -270,21 +284,17 @@ function PublishFile(props: Props) {
|
|||
setPublishMode(PUBLISH_MODES.FILE);
|
||||
}
|
||||
|
||||
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
||||
// 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('.'))) || '';
|
||||
|
||||
// Strip off extension and replace invalid characters
|
||||
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.
|
||||
setCurrentFile(file.path || file.name);
|
||||
updatePublishForm(publishFormParams);
|
||||
function handleFileChange(fileWithPath: FileWithPath) {
|
||||
if (fileWithPath) {
|
||||
updatePublishForm({ filePath: fileWithPath.path });
|
||||
}
|
||||
}
|
||||
|
||||
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
||||
|
@ -332,6 +342,7 @@ function PublishFile(props: Props) {
|
|||
onFileChosen={handleFileChange}
|
||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||
placeholder={__('Select file to upload')}
|
||||
readFile={false}
|
||||
/>
|
||||
{getUploadMessage()}
|
||||
</>
|
||||
|
|
|
@ -35,8 +35,8 @@ import tempy from 'tempy';
|
|||
type Props = {
|
||||
disabled: boolean,
|
||||
tags: Array<Tag>,
|
||||
publish: (source?: string | File, ?boolean) => void,
|
||||
filePath: string | File,
|
||||
publish: (source: ?File, ?boolean) => void,
|
||||
filePath: ?File,
|
||||
fileText: string,
|
||||
bid: ?number,
|
||||
bidError: ?string,
|
||||
|
@ -208,7 +208,6 @@ function PublishForm(props: Props) {
|
|||
isNameValid(name) &&
|
||||
title &&
|
||||
bid &&
|
||||
thumbnail &&
|
||||
!bidError &&
|
||||
!emptyPostError &&
|
||||
!(thumbnailError && !thumbnailUploaded) &&
|
||||
|
@ -373,9 +372,6 @@ function PublishForm(props: Props) {
|
|||
if (!output || output === '') {
|
||||
// Generate a temporary file:
|
||||
output = tempy.file({ name: 'post.md' });
|
||||
} else if (typeof filePath === 'string') {
|
||||
// Use current file
|
||||
output = filePath;
|
||||
}
|
||||
// Create a temporary file and save file changes
|
||||
if (output && output !== '') {
|
||||
|
@ -447,7 +443,7 @@ function PublishForm(props: Props) {
|
|||
// with other properties such as name, title, etc.) for security reasons.
|
||||
useEffect(() => {
|
||||
if (mode === PUBLISH_MODES.FILE) {
|
||||
updatePublishForm({ filePath: '', fileDur: 0, fileSize: 0 });
|
||||
updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 });
|
||||
}
|
||||
}, [mode, updatePublishForm]);
|
||||
|
||||
|
|
|
@ -47,11 +47,7 @@ function PublishFormErrors(props: Props) {
|
|||
{!bid && <div>{__('A deposit amount is required')}</div>}
|
||||
{bidError && <div>{__('Please check your deposit amount.')}</div>}
|
||||
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
|
||||
{!isUploadingThumbnail && !thumbnail ? (
|
||||
<div>{__('A thumbnail is required. Please upload or provide an image URL above.')}</div>
|
||||
) : (
|
||||
thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>
|
||||
)}
|
||||
{thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>}
|
||||
{editingURI && !isStillEditing && !filePath && (
|
||||
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
||||
)}
|
||||
|
|
|
@ -27,6 +27,14 @@ type Props = {
|
|||
// passed to the onUpdate function after the
|
||||
// upload service returns success.
|
||||
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) {
|
||||
|
@ -43,7 +51,8 @@ function filePreview(file) {
|
|||
}
|
||||
|
||||
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 [fileSelected, setFileSelected] = React.useState<any>(null);
|
||||
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
||||
|
@ -121,17 +130,17 @@ function SelectAsset(props: Props) {
|
|||
/>
|
||||
) : (
|
||||
<FileSelector
|
||||
filters={filters}
|
||||
type={type}
|
||||
autoFocus
|
||||
disabled={uploadStatus === SPEECH_UPLOADING}
|
||||
label={fileSelectorLabel}
|
||||
name="assetSelector"
|
||||
currentPath={pathSelected}
|
||||
onFileChosen={(file) => {
|
||||
if (file.name) {
|
||||
setFileSelected(file);
|
||||
// what why? why not target=WEB this?
|
||||
// file.path is undefined in web but available in electron
|
||||
setPathSelected(file.name || file.path);
|
||||
onFileChosen={(fileWithPath) => {
|
||||
if (fileWithPath.file.name) {
|
||||
setFileSelected(fileWithPath.file);
|
||||
setPathSelected(fileWithPath.path);
|
||||
}
|
||||
}}
|
||||
accept={accept}
|
||||
|
|
|
@ -106,7 +106,7 @@ function SelectThumbnail(props: Props) {
|
|||
__('This will be visible in a few minutes after you submit this form.')}
|
||||
<img
|
||||
style={{ display: 'none' }}
|
||||
src={thumbnail}
|
||||
src={thumbnail || ThumbnailMissingImage}
|
||||
alt={__('Thumbnail Preview')}
|
||||
onError={() => {
|
||||
if (updateThumbnailParams) {
|
||||
|
@ -160,9 +160,9 @@ function SelectThumbnail(props: Props) {
|
|||
label={__('Thumbnail')}
|
||||
placeholder={__('Choose an enticing thumbnail')}
|
||||
accept={accept}
|
||||
onFileChosen={(file) =>
|
||||
onFileChosen={(fileWithPath) =>
|
||||
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
|
||||
file,
|
||||
file: fileWithPath,
|
||||
cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ export default function SettingSystem(props: Props) {
|
|||
<FileSelector
|
||||
type="openDirectory"
|
||||
currentPath={daemonSettings.download_dir}
|
||||
onFileChosen={(newDirectory: WebFile) => {
|
||||
onFileChosen={(newDirectory: FileWithPath) => {
|
||||
setDaemonSetting('download_dir', newDirectory.path);
|
||||
}}
|
||||
/>
|
||||
|
@ -224,7 +224,7 @@ export default function SettingSystem(props: Props) {
|
|||
type="openDirectory"
|
||||
placeholder={__('A Folder containing FFmpeg')}
|
||||
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
|
||||
onFileChosen={(newDirectory: WebFile) => {
|
||||
onFileChosen={(newDirectory: FileWithPath) => {
|
||||
// $FlowFixMe
|
||||
setDaemonSetting('ffmpeg_path', newDirectory.path);
|
||||
findFFmpeg();
|
||||
|
|
|
@ -103,6 +103,8 @@ function TxoList(props: Props) {
|
|||
params[TXO.TX_TYPE] = currentUrlParams.type;
|
||||
} else if (currentUrlParams.type === TXO.PUBLISH) {
|
||||
params[TXO.TX_TYPE] = TXO.STREAM;
|
||||
} else if (currentUrlParams.type === TXO.COLLECTION) {
|
||||
params[TXO.TX_TYPE] = currentUrlParams.type;
|
||||
}
|
||||
}
|
||||
if (currentUrlParams.active) {
|
||||
|
|
|
@ -186,3 +186,5 @@ export const MYSTERIES = 'Mysteries';
|
|||
export const TECHNOLOGY = 'Technology';
|
||||
export const EMOJI = 'Emoji';
|
||||
export const STICKER = 'Sticker';
|
||||
export const SIMPLE_EDITOR = 'SimpleEditor';
|
||||
export const ADVANCED_EDITOR = 'AdvancedEditor';
|
||||
|
|
|
@ -11,7 +11,8 @@ export const SUPPORT = 'support';
|
|||
export const CHANNEL = 'channel';
|
||||
export const PUBLISH = 'publish';
|
||||
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
|
||||
export const TIP = 'tip';
|
||||
export const PURCHASE = 'purchase';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'babel-polyfill';
|
||||
import '@babel/polyfill';
|
||||
import ErrorBoundary from 'component/errorBoundary';
|
||||
import App from 'component/app';
|
||||
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
|
||||
getMediaType: (contentType: ?string, fileName: ?string) => {
|
||||
if (fileName) {
|
||||
if (fileName && fileName.split('.').length > 1) {
|
||||
const formats = [
|
||||
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
|
||||
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Modal } from 'modal/modal';
|
|||
import { formatFileSystemPath } from 'util/url';
|
||||
|
||||
type Props = {
|
||||
upload: WebFile => void,
|
||||
upload: (File) => void,
|
||||
filePath: string,
|
||||
closeModal: () => void,
|
||||
showToast: ({}) => void,
|
||||
|
|
|
@ -5,7 +5,7 @@ import ModalConfirmThumbnailUpload from './view';
|
|||
|
||||
const perform = (dispatch) => ({
|
||||
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)),
|
||||
});
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import { Modal } from 'modal/modal';
|
|||
import { DOMAIN } from 'config';
|
||||
|
||||
type Props = {
|
||||
file: WebFile,
|
||||
upload: (WebFile, (string) => void) => void,
|
||||
file: FileWithPath,
|
||||
upload: (FileWithPath, (string) => void) => void,
|
||||
cb: (string) => void,
|
||||
closeModal: () => void,
|
||||
updatePublishForm: ({}) => void,
|
||||
|
@ -23,7 +23,7 @@ class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const { closeModal, file } = this.props;
|
||||
const filePath = file && (file.path || file.name);
|
||||
const filePath = file && file.path;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -9,12 +9,12 @@ import Button from 'component/button';
|
|||
import FileList from 'component/common/file-list';
|
||||
|
||||
type Props = {
|
||||
files: Array<WebFile>,
|
||||
files: Array<File>,
|
||||
hideModal: () => void,
|
||||
updatePublishForm: ({}) => void,
|
||||
history: {
|
||||
location: { pathname: string },
|
||||
push: string => void,
|
||||
push: (string) => void,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ const ModalFileSelection = (props: Props) => {
|
|||
navigateToPublish();
|
||||
}
|
||||
|
||||
const handleFileChange = (file?: WebFile) => {
|
||||
const handleFileChange = (file?: File) => {
|
||||
// $FlowFixMe
|
||||
setSelectedFile(file);
|
||||
};
|
||||
|
|
|
@ -14,10 +14,13 @@ type Props = {
|
|||
|
||||
function ModalImageUpload(props: Props) {
|
||||
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
|
||||
const filters = React.useMemo(() => [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg'] }]);
|
||||
|
||||
return (
|
||||
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
|
||||
<SelectAsset
|
||||
filters={filters}
|
||||
type="openFile"
|
||||
onUpdate={onUpdate}
|
||||
currentValue={currentValue}
|
||||
assetName={assetName}
|
||||
|
|
|
@ -15,7 +15,7 @@ import Icon from 'component/common/icon';
|
|||
import { NO_FILE } from 'redux/actions/publish';
|
||||
|
||||
type Props = {
|
||||
filePath: string | WebFile,
|
||||
filePath: ?File,
|
||||
isMarkdownPost: boolean,
|
||||
optimize: boolean,
|
||||
title: ?string,
|
||||
|
@ -104,16 +104,11 @@ const ModalPublishPreview = (props: Props) => {
|
|||
// @endif
|
||||
}
|
||||
|
||||
function getFilePathName(filePath: string | WebFile) {
|
||||
function getFilePathName(filePath: ?File) {
|
||||
if (!filePath) {
|
||||
return NO_FILE;
|
||||
}
|
||||
|
||||
if (typeof filePath === 'string') {
|
||||
return filePath;
|
||||
} else {
|
||||
return filePath.name;
|
||||
}
|
||||
return filePath.name;
|
||||
}
|
||||
|
||||
function createRow(label: string, value: any) {
|
||||
|
@ -127,7 +122,7 @@ const ModalPublishPreview = (props: Props) => {
|
|||
|
||||
const txFee = previewResponse ? previewResponse['total_fee'] : null;
|
||||
// $FlowFixMe add outputs[0] etc to PublishResponse type
|
||||
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
||||
const isOptimizeAvail = filePath && isVid && ffmpegStatus.available;
|
||||
let modalTitle;
|
||||
if (isStillEditing) {
|
||||
modalTitle = __('Confirm Edit');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
||||
import Page from 'component/page';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import FileTitleSection from 'component/fileTitleSection';
|
||||
|
@ -59,6 +60,7 @@ function FilePage(props: Props) {
|
|||
} = props;
|
||||
const cost = costInfo ? costInfo.cost : null;
|
||||
const hasFileInfo = fileInfo !== undefined;
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
const isMarkdown = renderMode === RENDER_MODES.MARKDOWN;
|
||||
const videoPlayedEnoughToResetPosition = React.useMemo(() => {
|
||||
const durationInSecs =
|
||||
|
@ -169,8 +171,10 @@ function FilePage(props: Props) {
|
|||
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||
<FileTitleSection uri={uri} isNsfwBlocked />
|
||||
</div>
|
||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||
<CollectionContent id={collectionId} uri={uri} />
|
||||
)}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -187,13 +191,17 @@ function FilePage(props: Props) {
|
|||
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
||||
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
|
||||
</div>
|
||||
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
{!collection && !isMarkdown && videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && videoTheaterMode && !isMediumScreen && (
|
||||
<CollectionContent id={collectionId} uri={uri} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||
<CollectionContent id={collectionId} uri={uri} />
|
||||
)}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||
{isMarkdown && (
|
||||
<div className="file-page__post-comments">
|
||||
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />}
|
||||
|
|
|
@ -62,7 +62,7 @@ class ReportPage extends React.Component {
|
|||
name="message"
|
||||
stretch
|
||||
value={this.state.message}
|
||||
onChange={event => {
|
||||
onChange={(event) => {
|
||||
this.onMessageChange(event);
|
||||
}}
|
||||
placeholder={__('Description of your issue or feature request')}
|
||||
|
@ -71,7 +71,7 @@ class ReportPage extends React.Component {
|
|||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={event => {
|
||||
onClick={(event) => {
|
||||
this.submitMessage(event);
|
||||
}}
|
||||
className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`}
|
||||
|
|
|
@ -458,32 +458,34 @@ export function doAnalyticsView(uri, timeToStart) {
|
|||
}
|
||||
|
||||
export function doAnalyticsBuffer(uri, bufferData) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
const user = selectUser(state);
|
||||
const {
|
||||
value: { video, audio, source },
|
||||
} = claim;
|
||||
const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
||||
const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
||||
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||
const fileSize = source.size; // size in bytes
|
||||
const fileSizeInBits = fileSize * 8;
|
||||
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||
const userId = user && user.id.toString();
|
||||
return () => {
|
||||
// return (dispatch, getState) => {
|
||||
// const state = getState();
|
||||
// const claim = selectClaimForUri(state, uri);
|
||||
// const user = selectUser(state);
|
||||
// const {
|
||||
// value: { video, audio, source },
|
||||
// } = claim;
|
||||
// const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
||||
// const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
||||
// const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||
// const fileSize = source.size; // size in bytes
|
||||
// const fileSizeInBits = fileSize * 8;
|
||||
// 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 (userId) {
|
||||
analytics.videoBufferEvent(claim, {
|
||||
timeAtBuffer,
|
||||
bufferDuration,
|
||||
bitRate,
|
||||
userId,
|
||||
duration: fileDurationInSeconds,
|
||||
playerPoweredBy: bufferData.playerPoweredBy,
|
||||
readyState: bufferData.readyState,
|
||||
});
|
||||
}
|
||||
// if (<condition>) {
|
||||
// STUB: any buffer events here
|
||||
// analytics.videoBufferEvent(claim, {
|
||||
// timeAtBuffer,
|
||||
// bufferDuration,
|
||||
// bitRate,
|
||||
// userId,
|
||||
// duration: fileDurationInSeconds,
|
||||
// playerPoweredBy: bufferData.playerPoweredBy,
|
||||
// readyState: bufferData.readyState,
|
||||
// });
|
||||
// }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import Lbry from 'lbry';
|
|||
import { isClaimNsfw } from 'util/claim';
|
||||
|
||||
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) => {
|
||||
dispatch(
|
||||
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
||||
|
@ -138,335 +138,327 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) =>
|
|||
data: { ...publishFormValue },
|
||||
});
|
||||
|
||||
export const doUploadThumbnail = (
|
||||
filePath?: string,
|
||||
thumbnailBlob?: File,
|
||||
fsAdapter?: any,
|
||||
fs?: any,
|
||||
path?: any,
|
||||
cb?: (string) => void
|
||||
) => (dispatch: Dispatch) => {
|
||||
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
||||
let thumbnail, fileExt, fileName, fileType;
|
||||
export const doUploadThumbnail =
|
||||
(filePath?: string, thumbnailBlob?: File, fsAdapter?: any, fs?: any, path?: any, cb?: (string) => void) =>
|
||||
(dispatch: Dispatch) => {
|
||||
const downMessage = __('Thumbnail upload service may be down, try again later.');
|
||||
let thumbnail, fileExt, fileName, fileType;
|
||||
|
||||
const makeid = () => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||
return text;
|
||||
};
|
||||
const makeid = () => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||
return text;
|
||||
};
|
||||
|
||||
const uploadError = (error = '') => {
|
||||
dispatch(
|
||||
batchActions(
|
||||
{
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
||||
thumbnail: '',
|
||||
nsfw: false,
|
||||
const uploadError = (error = '') => {
|
||||
dispatch(
|
||||
batchActions(
|
||||
{
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
||||
thumbnail: '',
|
||||
nsfw: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
doError(error)
|
||||
)
|
||||
);
|
||||
};
|
||||
doError(error)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
thumbnailError: undefined,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
thumbnailError: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const doUpload = (data) => {
|
||||
return fetch(SPEECH_PUBLISH, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((text) => (text.length ? JSON.parse(text) : {}))
|
||||
.then((json) => {
|
||||
if (!json.success) return uploadError(json.message || downMessage);
|
||||
if (cb) {
|
||||
cb(json.data.serveUrl);
|
||||
}
|
||||
return dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
||||
thumbnail: json.data.serveUrl,
|
||||
},
|
||||
});
|
||||
const doUpload = (data) => {
|
||||
return fetch(SPEECH_PUBLISH, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.catch((err) => {
|
||||
let message = err.message;
|
||||
.then((res) => res.text())
|
||||
.then((text) => (text.length ? JSON.parse(text) : {}))
|
||||
.then((json) => {
|
||||
if (!json.success) return uploadError(json.message || downMessage);
|
||||
if (cb) {
|
||||
cb(json.data.serveUrl);
|
||||
}
|
||||
return dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: {
|
||||
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
||||
thumbnail: json.data.serveUrl,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
let message = err.message;
|
||||
|
||||
// This sucks but ¯\_(ツ)_/¯
|
||||
if (message === 'Failed to fetch') {
|
||||
message = downMessage;
|
||||
}
|
||||
const userInput = [fileName, fileExt, fileType, thumbnail];
|
||||
uploadError(`${message}\nUser input: ${userInput.join(', ')}`);
|
||||
// This sucks but ¯\_(ツ)_/¯
|
||||
if (message === 'Failed to fetch') {
|
||||
message = downMessage;
|
||||
}
|
||||
const userInput = [fileName, fileExt, fileType, thumbnail];
|
||||
uploadError(`${message}\nUser input: ${userInput.join(', ')}`);
|
||||
});
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
|
||||
});
|
||||
|
||||
if (fsAdapter && fsAdapter.readFile && filePath) {
|
||||
fsAdapter.readFile(filePath, 'base64').then((base64Image) => {
|
||||
fileExt = 'png';
|
||||
fileName = 'thumbnail.png';
|
||||
fileType = 'image/png';
|
||||
|
||||
const data = new FormData();
|
||||
const name = makeid();
|
||||
data.append('name', name);
|
||||
// $FlowFixMe
|
||||
data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName });
|
||||
return doUpload(data);
|
||||
});
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
|
||||
});
|
||||
|
||||
if (fsAdapter && fsAdapter.readFile && filePath) {
|
||||
fsAdapter.readFile(filePath, 'base64').then((base64Image) => {
|
||||
fileExt = 'png';
|
||||
fileName = 'thumbnail.png';
|
||||
fileType = 'image/png';
|
||||
} else {
|
||||
if (filePath && fs && path) {
|
||||
thumbnail = fs.readFileSync(filePath);
|
||||
fileExt = path.extname(filePath);
|
||||
fileName = path.basename(filePath);
|
||||
fileType = `image/${fileExt.slice(1)}`;
|
||||
} else if (thumbnailBlob) {
|
||||
fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`;
|
||||
fileName = thumbnailBlob.name;
|
||||
fileType = thumbnailBlob.type;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
const name = makeid();
|
||||
const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType }));
|
||||
data.append('name', name);
|
||||
// $FlowFixMe
|
||||
data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName });
|
||||
data.append('file', file);
|
||||
return doUpload(data);
|
||||
});
|
||||
} else {
|
||||
if (filePath && fs && path) {
|
||||
thumbnail = fs.readFileSync(filePath);
|
||||
fileExt = path.extname(filePath);
|
||||
fileName = path.basename(filePath);
|
||||
fileType = `image/${fileExt.slice(1)}`;
|
||||
} else if (thumbnailBlob) {
|
||||
fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`;
|
||||
fileName = thumbnailBlob.name;
|
||||
fileType = thumbnailBlob.type;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
const name = makeid();
|
||||
const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType }));
|
||||
data.append('name', name);
|
||||
// $FlowFixMe
|
||||
data.append('file', file);
|
||||
return doUpload(data);
|
||||
}
|
||||
};
|
||||
|
||||
export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (
|
||||
dispatch: Dispatch
|
||||
) => {
|
||||
const { name, amount, value = {} } = claim;
|
||||
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
||||
const {
|
||||
author,
|
||||
description,
|
||||
// use same values as default state
|
||||
// fee will be undefined for free content
|
||||
fee = {
|
||||
amount: '0',
|
||||
currency: 'LBC',
|
||||
},
|
||||
languages,
|
||||
release_time,
|
||||
license,
|
||||
license_url: licenseUrl,
|
||||
thumbnail,
|
||||
title,
|
||||
tags,
|
||||
} = value;
|
||||
|
||||
const publishData: UpdatePublishFormData = {
|
||||
name,
|
||||
bid: Number(amount),
|
||||
contentIsFree: fee.amount === '0',
|
||||
author,
|
||||
description,
|
||||
fee,
|
||||
languages,
|
||||
releaseTime: release_time,
|
||||
releaseTimeEdited: undefined,
|
||||
thumbnail: thumbnail ? thumbnail.url : null,
|
||||
title,
|
||||
uri,
|
||||
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
||||
licenseUrl,
|
||||
nsfw: isClaimNsfw(claim),
|
||||
tags: tags ? tags.map((tag) => ({ name: tag })) : [],
|
||||
};
|
||||
|
||||
// Make sure custom licenses are mapped properly
|
||||
// If the license isn't one of the standard licenses, map the custom license and description/url
|
||||
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
||||
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
|
||||
export const doPrepareEdit =
|
||||
(claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (dispatch: Dispatch) => {
|
||||
const { name, amount, value = {} } = claim;
|
||||
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
|
||||
const {
|
||||
author,
|
||||
description,
|
||||
// use same values as default state
|
||||
// fee will be undefined for free content
|
||||
fee = {
|
||||
amount: '0',
|
||||
currency: 'LBC',
|
||||
},
|
||||
languages,
|
||||
release_time,
|
||||
license,
|
||||
license_url: licenseUrl,
|
||||
thumbnail,
|
||||
title,
|
||||
tags,
|
||||
} = value;
|
||||
|
||||
const publishData: UpdatePublishFormData = {
|
||||
name,
|
||||
bid: Number(amount),
|
||||
contentIsFree: fee.amount === '0',
|
||||
author,
|
||||
description,
|
||||
fee,
|
||||
languages,
|
||||
releaseTime: release_time,
|
||||
releaseTimeEdited: undefined,
|
||||
thumbnail: thumbnail ? thumbnail.url : null,
|
||||
title,
|
||||
uri,
|
||||
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
||||
licenseUrl,
|
||||
nsfw: isClaimNsfw(claim),
|
||||
tags: tags ? tags.map((tag) => ({ name: tag })) : [],
|
||||
};
|
||||
|
||||
// Make sure custom licenses are mapped properly
|
||||
// If the license isn't one of the standard licenses, map the custom license and description/url
|
||||
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
||||
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
|
||||
publishData.licenseType = license;
|
||||
} else if (license && !licenseUrl && license !== NONE) {
|
||||
publishData.licenseType = COPYRIGHT;
|
||||
} else {
|
||||
publishData.licenseType = OTHER;
|
||||
}
|
||||
|
||||
publishData.otherLicenseDescription = license;
|
||||
} else {
|
||||
publishData.licenseType = license;
|
||||
} else if (license && !licenseUrl && license !== NONE) {
|
||||
publishData.licenseType = COPYRIGHT;
|
||||
} else {
|
||||
publishData.licenseType = OTHER;
|
||||
}
|
||||
if (channelName) {
|
||||
publishData['channel'] = channelName;
|
||||
}
|
||||
|
||||
publishData.otherLicenseDescription = license;
|
||||
} else {
|
||||
publishData.licenseType = license;
|
||||
}
|
||||
if (channelName) {
|
||||
publishData['channel'] = channelName;
|
||||
}
|
||||
|
||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||
};
|
||||
|
||||
export const doPublish = (success: Function, fail: Function, preview: Function) => (
|
||||
dispatch: Dispatch,
|
||||
getState: () => {}
|
||||
) => {
|
||||
if (!preview) {
|
||||
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const myClaimForUri = selectMyClaimForUri(state);
|
||||
const myChannels = selectMyChannelClaims(state);
|
||||
// const myClaims = selectMyClaimsWithoutChannels(state);
|
||||
// get redux publish form
|
||||
const publishData = selectPublishFormValues(state);
|
||||
|
||||
// destructure the data values
|
||||
const {
|
||||
name,
|
||||
bid,
|
||||
filePath,
|
||||
description,
|
||||
language,
|
||||
releaseTimeEdited,
|
||||
// license,
|
||||
licenseUrl,
|
||||
useLBRYUploader,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
thumbnail,
|
||||
channel,
|
||||
title,
|
||||
contentIsFree,
|
||||
fee,
|
||||
tags,
|
||||
// locations,
|
||||
optimize,
|
||||
} = publishData;
|
||||
|
||||
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
||||
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
||||
|
||||
let publishingLicense;
|
||||
switch (licenseType) {
|
||||
case COPYRIGHT:
|
||||
case OTHER:
|
||||
publishingLicense = otherLicenseDescription;
|
||||
break;
|
||||
default:
|
||||
publishingLicense = licenseType;
|
||||
}
|
||||
|
||||
// get the claim id from the channel name, we will use that instead
|
||||
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
||||
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||
|
||||
const publishPayload: {
|
||||
name: ?string,
|
||||
bid: string,
|
||||
description?: string,
|
||||
channel_id?: string,
|
||||
file_path?: string,
|
||||
license_url?: string,
|
||||
license?: string,
|
||||
thumbnail_url?: string,
|
||||
release_time?: number,
|
||||
fee_currency?: string,
|
||||
fee_amount?: string,
|
||||
languages?: Array<string>,
|
||||
tags: Array<string>,
|
||||
locations?: Array<any>,
|
||||
blocking: boolean,
|
||||
optimize_file?: boolean,
|
||||
preview?: boolean,
|
||||
remote_url?: string,
|
||||
} = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
locations: [],
|
||||
bid: creditsToString(bid),
|
||||
languages: [language],
|
||||
tags: tags && tags.map((tag) => tag.name),
|
||||
thumbnail_url: thumbnail,
|
||||
blocking: true,
|
||||
preview: false,
|
||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||
};
|
||||
// Temporary solution to keep the same publish flow with the new tags api
|
||||
// Eventually we will allow users to enter their own tags on publish
|
||||
|
||||
if (publishingLicense) {
|
||||
publishPayload.license = publishingLicense;
|
||||
}
|
||||
export const doPublish =
|
||||
(success: Function, fail: Function, preview: Function) => (dispatch: Dispatch, getState: () => {}) => {
|
||||
if (!preview) {
|
||||
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||
}
|
||||
|
||||
if (licenseUrl) {
|
||||
publishPayload.license_url = licenseUrl;
|
||||
}
|
||||
const state = getState();
|
||||
const myClaimForUri = selectMyClaimForUri(state);
|
||||
const myChannels = selectMyChannelClaims(state);
|
||||
// const myClaims = selectMyClaimsWithoutChannels(state);
|
||||
// get redux publish form
|
||||
const publishData = selectPublishFormValues(state);
|
||||
|
||||
if (thumbnail) {
|
||||
publishPayload.thumbnail_url = thumbnail;
|
||||
}
|
||||
// destructure the data values
|
||||
const {
|
||||
name,
|
||||
bid,
|
||||
filePath,
|
||||
description,
|
||||
language,
|
||||
releaseTimeEdited,
|
||||
// license,
|
||||
licenseUrl,
|
||||
useLBRYUploader,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
thumbnail,
|
||||
channel,
|
||||
title,
|
||||
contentIsFree,
|
||||
fee,
|
||||
tags,
|
||||
// locations,
|
||||
optimize,
|
||||
} = publishData;
|
||||
|
||||
if (useLBRYUploader) {
|
||||
publishPayload.tags.push('lbry-first');
|
||||
}
|
||||
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
||||
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
||||
|
||||
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
||||
if (releaseTimeEdited) {
|
||||
publishPayload.release_time = releaseTimeEdited;
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
||||
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
||||
} else {
|
||||
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
||||
}
|
||||
let publishingLicense;
|
||||
switch (licenseType) {
|
||||
case COPYRIGHT:
|
||||
case OTHER:
|
||||
publishingLicense = otherLicenseDescription;
|
||||
break;
|
||||
default:
|
||||
publishingLicense = licenseType;
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
publishPayload.channel_id = channelId;
|
||||
}
|
||||
// get the claim id from the channel name, we will use that instead
|
||||
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
||||
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||
|
||||
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
||||
publishPayload.locations = myClaimForUriEditing.value.locations;
|
||||
}
|
||||
const publishPayload: {
|
||||
name: ?string,
|
||||
bid: string,
|
||||
description?: string,
|
||||
channel_id?: string,
|
||||
file_path?: string,
|
||||
license_url?: string,
|
||||
license?: string,
|
||||
thumbnail_url?: string,
|
||||
release_time?: number,
|
||||
fee_currency?: string,
|
||||
fee_amount?: string,
|
||||
languages?: Array<string>,
|
||||
tags: Array<string>,
|
||||
locations?: Array<any>,
|
||||
blocking: boolean,
|
||||
optimize_file?: boolean,
|
||||
preview?: boolean,
|
||||
remote_url?: string,
|
||||
} = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
locations: [],
|
||||
bid: creditsToString(bid),
|
||||
languages: [language],
|
||||
tags: tags && tags.map((tag) => tag.name),
|
||||
thumbnail_url: thumbnail,
|
||||
blocking: true,
|
||||
preview: false,
|
||||
};
|
||||
// Temporary solution to keep the same publish flow with the new tags api
|
||||
// Eventually we will allow users to enter their own tags on publish
|
||||
|
||||
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
||||
publishPayload.fee_currency = fee.currency;
|
||||
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||
}
|
||||
if (publishingLicense) {
|
||||
publishPayload.license = publishingLicense;
|
||||
}
|
||||
|
||||
if (optimize) {
|
||||
publishPayload.optimize_file = true;
|
||||
}
|
||||
if (licenseUrl) {
|
||||
publishPayload.license_url = licenseUrl;
|
||||
}
|
||||
|
||||
// Only pass file on new uploads, not metadata only edits.
|
||||
// The sdk will figure it out
|
||||
if (filePath) publishPayload.file_path = filePath;
|
||||
if (thumbnail) {
|
||||
publishPayload.thumbnail_url = thumbnail;
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
publishPayload.preview = true;
|
||||
publishPayload.optimize_file = false;
|
||||
if (useLBRYUploader) {
|
||||
publishPayload.tags.push('lbry-first');
|
||||
}
|
||||
|
||||
return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => {
|
||||
return preview(previewResponse);
|
||||
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
||||
if (releaseTimeEdited) {
|
||||
publishPayload.release_time = releaseTimeEdited;
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
||||
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
||||
} else {
|
||||
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
publishPayload.channel_id = channelId;
|
||||
}
|
||||
|
||||
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
||||
publishPayload.locations = myClaimForUriEditing.value.locations;
|
||||
}
|
||||
|
||||
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
||||
publishPayload.fee_currency = fee.currency;
|
||||
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||
}
|
||||
|
||||
if (optimize) {
|
||||
publishPayload.optimize_file = true;
|
||||
}
|
||||
|
||||
// Only pass file on new uploads, not metadata only edits.
|
||||
// The sdk will figure it out
|
||||
if (filePath) publishPayload.file_path = filePath;
|
||||
|
||||
if (preview) {
|
||||
publishPayload.preview = true;
|
||||
publishPayload.optimize_file = false;
|
||||
|
||||
return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => {
|
||||
return preview(previewResponse);
|
||||
}, fail);
|
||||
}
|
||||
|
||||
return Lbry.publish(publishPayload).then((response: PublishResponse) => {
|
||||
return success(response);
|
||||
}, fail);
|
||||
}
|
||||
|
||||
return Lbry.publish(publishPayload).then((response: PublishResponse) => {
|
||||
return success(response);
|
||||
}, fail);
|
||||
};
|
||||
};
|
||||
|
||||
// Calls file_list until any reflecting files are done
|
||||
export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetState) => {
|
||||
|
|
|
@ -161,18 +161,24 @@ function filterFileInfos(fileInfos, query) {
|
|||
export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) =>
|
||||
createSelector(selectFileInfosDownloaded, (fileInfos) => {
|
||||
const matchingFileInfos = filterFileInfos(fileInfos, query);
|
||||
if (!matchingFileInfos || !matchingFileInfos.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const start = (Number(page) - 1) * 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
|
||||
? matchingFileInfos.slice(start, end).map((fileInfo) =>
|
||||
buildURI({
|
||||
streamName: fileInfo.claim_name,
|
||||
channelName: fileInfo.channel_name,
|
||||
channelClaimId: fileInfo.channel_claim_id,
|
||||
})
|
||||
)
|
||||
: [];
|
||||
return sortedMatchedFileInfos.slice(start, end).map((fileInfo) =>
|
||||
buildURI({
|
||||
streamName: fileInfo.claim_name,
|
||||
channelName: fileInfo.channel_name,
|
||||
channelClaimId: fileInfo.channel_claim_id,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const makeSelectSearchDownloadUrlsCount = (query) =>
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
selectClaimForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { swapKeyAndValue } from 'util/swap-json';
|
||||
import { getChannelFromClaim } from 'util/claim';
|
||||
import { getChannelFromClaim, isChannelClaim } from 'util/claim';
|
||||
|
||||
// Returns the entire subscriptions state
|
||||
const selectState = (state) => state.subscriptions || {};
|
||||
|
@ -114,12 +114,18 @@ export const makeSelectChannelInSubscriptions = (uri) =>
|
|||
createSelector(selectSubscriptions, (subscriptions) => subscriptions.some((sub) => sub.uri === uri));
|
||||
|
||||
export const selectIsSubscribedForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectClaimForUri,
|
||||
selectSubscriptions,
|
||||
(claim, subscriptions) => {
|
||||
(uri, claim, subscriptions) => {
|
||||
const channelClaim = getChannelFromClaim(claim);
|
||||
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 false;
|
||||
|
|
|
@ -167,7 +167,7 @@ a.button--alt {
|
|||
.vjs-button--theater-mode.vjs-button {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
display: block;
|
||||
order: 1;
|
||||
background-repeat: no-repeat;
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
margin: 0px var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.button + .commentCreate {
|
||||
.button + .comment-create {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
@ -274,6 +274,21 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.select--slim {
|
||||
display: flex;
|
||||
margin-left: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
max-width: none;
|
||||
|
||||
select {
|
||||
padding: 0 var(--spacing-xs);
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card--enable-overflow {
|
||||
|
@ -318,6 +333,7 @@
|
|||
align-self: flex-start;
|
||||
.button--alt {
|
||||
padding-top: 2px;
|
||||
padding: 0 var(--spacing-s);
|
||||
}
|
||||
.comment__sort {
|
||||
.button--alt {
|
||||
|
@ -599,7 +615,7 @@
|
|||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: var(--font-small);
|
||||
|
||||
border-bottom: none;
|
||||
.button--link {
|
||||
font-size: var(--font-xsmall);
|
||||
margin: 0px;
|
||||
|
@ -642,3 +658,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button__selected-server {
|
||||
display: inline;
|
||||
float: right;
|
||||
select {
|
||||
width: 12rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
select {
|
||||
width: 8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button_refresh {
|
||||
display: inline;
|
||||
float: right;
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
|
|
|
@ -621,7 +621,7 @@
|
|||
@media (max-width: $breakpoint-xsmall) {
|
||||
-webkit-line-clamp: 2 !important;
|
||||
}
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
-webkit-line-clamp: 1 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
|
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate--reply {
|
||||
.comment-create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate--nestedReply {
|
||||
.comment-create--nestedReply {
|
||||
margin-top: var(--spacing-s);
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
max-width: 50%;
|
||||
|
||||
.comment-create__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
font-size: var(--font-xxsmall);
|
||||
|
@ -179,14 +192,14 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
|
||||
select {
|
||||
height: 1rem;
|
||||
margin: var(--spacing-xxs) 0px;
|
||||
}
|
||||
//select {
|
||||
// height: 1rem;
|
||||
// margin: var(--spacing-xxs) 0px;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__supportCommentPreview {
|
||||
.comment-create__supportCommentPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
|
@ -194,7 +207,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
|
||||
.commentCreate__supportCommentPreviewAmount {
|
||||
.comment-create__supportCommentPreviewAmount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
@ -223,8 +236,8 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreview {
|
||||
@extend .commentCreate;
|
||||
.comment-create__stickerPreview {
|
||||
@extend .comment-create;
|
||||
display: flex;
|
||||
background-color: var(--color-header-background);
|
||||
border-radius: var(--border-radius);
|
||||
|
@ -234,12 +247,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
width: 100%;
|
||||
height: 10rem;
|
||||
|
||||
.commentCreate__stickerPreviewInfo {
|
||||
.comment-create__stickerPreviewInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreviewImage {
|
||||
.comment-create__stickerPreviewImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: var(--spacing-m);
|
||||
|
|
|
@ -37,16 +37,18 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
|
||||
.comment__sort {
|
||||
margin-right: var(--spacing-s);
|
||||
display: inline-block;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-top: 0;
|
||||
display: inline;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__actions-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.comment {
|
||||
|
@ -505,7 +507,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
min-width: 40%;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
@ -547,7 +549,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
margin: 0 var(--spacing-xs);
|
||||
}
|
||||
|
||||
|
@ -562,7 +564,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
padding-left: var(--spacing-m);
|
||||
border-left: 4px solid var(--color-border);
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
margin-top: 0;
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,12 @@ select,
|
|||
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) {
|
||||
textarea {
|
||||
height: var(--height-input);
|
||||
|
@ -532,6 +537,7 @@ fieldset-group {
|
|||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
text-align: right;
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ body {
|
|||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
flex-direction: row;
|
||||
}
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
|
@ -461,7 +461,6 @@ body {
|
|||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
|
||||
> :first-child {
|
||||
|
@ -804,7 +803,7 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
> :first-child {
|
||||
width: calc(30% - var(--spacing-l));
|
||||
max-width: 25rem;
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
color: var(--color-brand-contrast) !important;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
overflow-y: hidden;
|
||||
justify-content: space-between;
|
||||
|
||||
|
@ -235,7 +235,7 @@
|
|||
@extend .navigation-link--highlighted;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
text-align: left;
|
||||
margin-bottom: 0;
|
||||
|
||||
|
|
|
@ -32,13 +32,17 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: var(--spacing-s);
|
||||
|
||||
.commentCreate__label {
|
||||
.comment-create__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.comment-create__header {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
textarea,
|
||||
select,
|
||||
.button:not(.button--file-action) {
|
||||
|
@ -81,7 +85,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate,
|
||||
.comment-create,
|
||||
.comment__content {
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.card__main-actions .commentCreate .MuiOutlinedInput-notchedOutline {
|
||||
.card__main-actions .comment-create .MuiOutlinedInput-notchedOutline {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
@ -104,7 +104,7 @@
|
|||
|
||||
textarea {
|
||||
border: none;
|
||||
margin: 9px 0px;
|
||||
padding: var(--spacing-xxs) var(--spacing-xxs);
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -320,7 +320,7 @@
|
|||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
.section__actions {
|
||||
.button {
|
||||
background-color: var(--color-header-button);
|
||||
|
|
|
@ -358,7 +358,7 @@
|
|||
max-width: unset;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
width: 40%;
|
||||
|
||||
.button,
|
||||
|
@ -375,7 +375,7 @@
|
|||
}
|
||||
|
||||
.settings__row--value--multirow {
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
width: 80%;
|
||||
margin-top: var(--spacing-l);
|
||||
|
||||
|
@ -389,7 +389,7 @@
|
|||
}
|
||||
|
||||
.settings__row--value--vertical-separator {
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
@media not all and (max-width: $breakpoint-medium) {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,9 @@ $spacing-width: 36px;
|
|||
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
||||
--option-select-width: 8rem;
|
||||
|
||||
--input-select-server-min-width: 100px;
|
||||
--input-select-server-max-width: 250px;
|
||||
|
||||
// Text
|
||||
--text-max-width: 660px;
|
||||
--text-link-padding: 4px;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// JSON parser
|
||||
const parseJson = (data, filters = []) => {
|
||||
const list = data.map(item => {
|
||||
const list = data.map((item) => {
|
||||
const temp = {};
|
||||
// Apply filters
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
|
@ -17,7 +17,7 @@ const parseJson = (data, filters = []) => {
|
|||
// https://gist.github.com/btzr-io/55c3450ea3d709fc57540e762899fb85
|
||||
const parseCsv = (data, filters = []) => {
|
||||
// Get items for header
|
||||
const getHeaders = item => {
|
||||
const getHeaders = (item) => {
|
||||
const list = [];
|
||||
// Apply filters
|
||||
Object.entries(item).forEach(([key]) => {
|
||||
|
@ -28,13 +28,16 @@ const parseCsv = (data, filters = []) => {
|
|||
};
|
||||
|
||||
// Get rows content
|
||||
const getData = list =>
|
||||
const getData = (list) =>
|
||||
list
|
||||
.map(item => {
|
||||
.map((item) => {
|
||||
const row = [];
|
||||
// Apply filters
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
if (!filters.includes(key)) row.push(value);
|
||||
if (!filters.includes(key)) {
|
||||
const sanitizedValue = '"' + value + '"';
|
||||
row.push(sanitizedValue);
|
||||
}
|
||||
});
|
||||
// return rows
|
||||
return row.join(',');
|
||||
|
@ -50,8 +53,8 @@ const parseData = (data, format, filters = []) => {
|
|||
const valid = data && data[0] && format;
|
||||
// Pick a format
|
||||
const formats = {
|
||||
csv: list => parseCsv(list, filters),
|
||||
json: list => parseJson(list, filters),
|
||||
csv: (list) => parseCsv(list, filters),
|
||||
json: (list) => parseJson(list, filters),
|
||||
};
|
||||
|
||||
// Return parsed data: JSON || CSV
|
||||
|
|
|
@ -95,7 +95,7 @@ let baseConfig = {
|
|||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
new webpack.IgnorePlugin({resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/}),
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV']),
|
||||
new DefinePlugin({
|
||||
__static: `"${path.join(__dirname, 'static').replace(/\\/g, '\\\\')}"`,
|
||||
|
|
Loading…
Reference in a new issue