Compare commits

...

45 commits

Author SHA1 Message Date
Kenneth C d14c9141db
Update readme (#7755)
Removed my name from the readme as I have no association with this project anymore, and have not touched the Flatpak in ages. 

RIP LBRY.
2023-04-10 12:17:11 -04:00
zeppi 06c350c4db v0.53.9 2023-02-08 15:29:43 -05:00
zeppi c3a9d9d002 fix changelog 2023-02-08 15:28:26 -05:00
zeppi aeada6dc74 v0.53.9 2023-02-08 14:38:35 -05:00
zeppi 6ba985fd28 lbrynet version 113, changelog 2023-02-08 14:32:16 -05:00
jessopb 2a0bc85738
bump lbrynet to 113 (#7747) 2023-02-08 10:36:14 -05:00
jessopb 523ea284a2
update signing certificate for 2023 (#7744) 2023-01-25 16:25:45 -05:00
jessopb a66d7534c2
add csc 2023-01-25 16:05:39 -05:00
zeppi 89ec07622f v0.53.8 2022-11-18 14:15:03 -05:00
zeppi 7e6ad31392 Revert "v0.53.8"
Incorrect Changelog
This reverts commit 4ab23f03fc.
2022-11-18 14:12:52 -05:00
zeppi 4ab23f03fc v0.53.8 2022-11-18 14:05:00 -05:00
zeppi 29cea5cc07 changelog 2022-11-18 14:02:37 -05:00
jessopb 8dd7150d67
fix unfollowing unpublished channels (#7737) 2022-11-18 10:22:32 -05:00
zeppi f1b1523017 v0.53.8-alpha.1 2022-11-17 13:26:53 -05:00
jessopb 88ac250fee
fix large file uploads (#7736)
* fix large file uploads

* changelog

* update github action xcode version

* update electronbuilder for macos

* try use_hard_links=false

* no USE_HARD_LINKS

* upgrate electron-builder 23_3_3

* revert to electron-builder 22_10_5
electron-builder/issues/6124 says regressions happen after this version.

* try mac install homebrew, python2

* typo and ln /usr/bin/python

* oops

* try sudo

* try PYTHON_PATH

* comment github action mac python hack
2022-11-17 13:20:01 -05:00
zeppi 0a5e9e87ed v0.53.7 2022-11-10 14:45:50 -05:00
zeppi 20413d79b6 changelog 2022-11-10 14:44:54 -05:00
zeppi ff9011e6ac v0.53.7-alpha.1 2022-11-04 12:37:02 -04:00
jessopb 802139d0a4
upgrade lbrynet to 112 (#7717) 2022-11-04 12:31:44 -04:00
zeppi 68d307fa50 changelog 2022-11-04 10:47:56 -04:00
jessopb d3900e39b6
fix comment area display (#7716) 2022-11-04 10:44:36 -04:00
jessopb 35769dede6
separate out advanced textarea, fix comment channel selector width, a… (#7634)
* separate out advanced textarea, fix comment channel selector width, add advanced text icon

* fix master conflicts

* fixes

* fix channel description edit
2022-11-04 08:42:36 -04:00
zeppi ae1e20d131 changelog 2022-11-03 17:46:08 -04:00
jessopb 051af8b6ad
fix post publish erased when confirmation (#7715) 2022-11-03 17:17:56 -04:00
jessopb 5d77b115f9
fix thumbnails disabling publish (#7714) 2022-11-03 15:55:46 -04:00
Byron Eric Perez 7dbeeac112
Add 'Collections' into txo filter (#7711) 2022-10-28 18:31:56 -04:00
zeppi de062c4aee yarn lock 2022-10-25 13:31:32 -04:00
dependabot[bot] 18a3336714
Bump @xmldom/xmldom from 0.7.5 to 0.7.6 (#7701)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.7.5 to 0.7.6.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.7.5...0.7.6)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-25 13:02:03 -04:00
jessopb ebf35a1df8
remove watchman (and errors!) (#7710) 2022-10-25 12:56:38 -04:00
jm-morani 28e168d5e5
Minor layout fixes depending on window shape (#7709)
Disable theatre mode styles for medium screen (or lower)
Fix the width of the cover in theater mode
Fix conflicting styles when width is 1150px
Remove duplicate from CHANGELOG

Co-authored-by: Jean-Michel Morani <probono+lbry@morani.org>
2022-10-25 10:03:20 -04:00
Byron Eric Perez 7ad66b99e7
Swap comment servers without going to settings page (#7670)
work in progress

added input to select custom and default server when creating comment

the problem of synchronizing the two commentServer of the application was solved

btn moved to the correct component and syncs correctly

Fixed why it didn't show comments

Aligned input

size and location of the refresh btn changed, and added maximum and minimum width for the comment server

margin removed

change using overflow

refresh
2022-10-24 12:36:30 -04:00
jahway603 7eb7c1a5ff
Updated README.md (#7695)
Fixed Arch AUR package name and current maintainer
2022-10-22 15:16:05 -04:00
zeppi b88e704e6b v0.53.6 2022-10-21 16:54:07 -04:00
zeppi 8d85af8064 changelog 2022-10-21 16:53:03 -04:00
zeppi f9d7340729 v0.53.6-alpha.1 2022-10-21 12:21:45 -04:00
zeppi d57300f785 changelog 2022-10-20 14:41:43 -04:00
jessopb 09baf1d9b9
fall back on contentType for extension on file view (#7704) 2022-10-20 14:17:43 -04:00
Franco Montenegro ce692d38ea
Upgrade to Electron 17.2.0. (#7703) 2022-10-20 14:03:12 -04:00
zeppi b1ca3b0183 bump electron 17.2 and lbrynet 0.111.0 2022-10-20 13:09:47 -04:00
Franco Montenegro a4c34d89e2
Sanitize values for CSV. (#7697)
* Sanitize values for CSV.

* Remove unnecessary escape sequence.
2022-10-17 11:07:33 -04:00
Franco Montenegro d7b9ca3391
7683 upgrade to electron 17 (#7691)
* Upgrade to electron 17.

* Remove unused dependencies.

* Update recommended node version in readme.

* Move all the dependencies back to devDependencies.

* Move dependencies back as they were.
2022-10-07 13:56:33 -04:00
jessopb 55a5c7b051
make thumbnail optional (#7690) 2022-09-19 18:39:45 -04:00
Franco Montenegro 0e2a9a1033
Better handling of uploaded files. (#7688)
* Better handling of uploaded files.

* Read file when uploading it so we can properly read metadata.
2022-09-19 16:42:16 -04:00
Franco Montenegro 3fd38be789
Sort downloads (show newest first) (#7684) 2022-09-05 16:21:01 -04:00
Franco Montenegro 329d434c83
Allow only images in modal image uploader. (#7672)
* Allow only images in modal image uploader.

* Set file path and mime in file selector.

* Refactor WebFile.

* Update get-file-from-path to work with folders; fix file-list component.

* Get rid of File | string for filePath property in components.

* Show instant preview while updating channel thumbnail.

* Fix publish.

* Add jpeg and svg to image filter.
2022-09-02 12:43:35 -04:00
76 changed files with 1580 additions and 1647 deletions

View file

@ -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
View file

0
.yarn/versions/5f1212ad.yml vendored Normal file
View file

0
.yarn/versions/6b35c994.yml vendored Normal file
View file

0
.yarn/versions/6be5ab70.yml vendored Normal file
View file

0
.yarn/versions/8e384637.yml vendored Normal file
View file

0
.yarn/versions/d1a18cef.yml vendored Normal file
View file

0
.yarn/versions/ec3a9ddf.yml vendored Normal file
View file

View file

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

View file

@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-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

Binary file not shown.

View file

@ -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
View 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
View 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,
}

View file

@ -1,6 +0,0 @@
// @flow
declare type WebFile = File & {
path?: string,
title?: string,
}

View file

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

View file

@ -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--"
}

View file

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

View file

@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import { FormField } 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')}

View file

@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
import { FormField } 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')}

View file

@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button';
import Icon from 'component/common/icon';
import { 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"

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

View file

@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
import { buildValidSticker } from 'util/comments';
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { 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>

View file

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

View file

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

View file

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

View file

@ -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 = () => {

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useIsMobile } 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={

View file

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

View file

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

View file

@ -92,7 +92,7 @@ function Page(props: Props) {
<div
className={classnames('main-wrapper__inner', {
'main-wrapper__inner--filepage': isOnFilePage,
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
'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}

View file

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

View file

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

View file

@ -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()}
</>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'],

View file

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

View file

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

View file

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

View file

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

View 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}

View file

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

View file

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

View file

@ -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' : ''}`}

View file

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

View file

@ -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) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, '\\\\')}"`,

1033
yarn.lock

File diff suppressed because it is too large Load diff